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

throw and return statements should affect flow sensitive typing #891

Closed
sgalles opened this issue Jan 5, 2014 · 52 comments
Closed

throw and return statements should affect flow sensitive typing #891

sgalles opened this issue Jan 5, 2014 · 52 comments
Assignees
Milestone

Comments

@sgalles
Copy link

sgalles commented Jan 5, 2014

throw and return statements should affect flow sensitive typing :

Foo foo = ... ;
if (!is Bar foo) {
     throw FooBarException();
}
foo.bar(); //foo is a Bar here 

Same behavior expected if return is used instead of throw

@pthariensflame
Copy link
Contributor

I like this, but it might be undecidable. What do you think, @RossTate?

@loicrouchon
Copy link
Member

Might be linked with #536

@RossTate
Copy link
Member

It's pretty easy to decide. At any branch point in the CFG you refine the types of variables as appropriate given the branch condition. At any merge point in the CFG, you join the types of variables. If there's a problem, it's because of loops, but I think for this application we'd be fine. Note that I'm saying "in the CFG" rather than in the syntax, which addresses all the Ceylon issues I've seen around on this topic.

@gavinking
Copy link
Member

It's recently come to my attention that there is a whole class of people who vehemently prefer to write:

void fun(String? str, Integer int) {
    if (!exists str) { return; }
    if (str.empty) { return; }
    if (int<0) { return; }
    //do the real work here
}

Instead of, for example:

void fun(String? str, Integer int) {
    if (exists str, !str.empty) {
        if (int>=0) { 
            //do the real work here
        }
    }
}

There's even a whole name for former style: "guards". I almost never write code like this and in fact I actively dislike it.

Nevertheless, I don't think it's a good thing that Ceylon actually prevents you from using this style. So we should make stuff like if (!exists x) { return; } have the same effect that assert (exists x); has today.

@FroMage
Copy link
Member

FroMage commented Sep 30, 2014

Yahoo!!!! :)

@luolong
Copy link
Member

luolong commented Oct 1, 2014

I emphatically vote Yes! :)

@gavinking
Copy link
Member

So even though this looks not much more difficult than #74, sadly, it's actually significantly harder. It would require doing control flow analysis and type assignment at once in the same visitor, essentially merging ControlFlowVisitor with ExpressionVisitor into one big blob of waaay too many responsibilities.

@gavinking gavinking self-assigned this Nov 5, 2014
@FroMage
Copy link
Member

FroMage commented Nov 5, 2014

Why does control flow depend on type assignment?

@lucaswerkmeister
Copy link
Member

@FroMage: I tried swapping the phases in the typechecker, and it seems the following cause problems:

Anything returnsDefinitely1() {
    while (true) {
        return "";
    }
}
Anything returnsDefinitely2() {
    assert (false);
}

(possibly amongst other things)

@gavinking
Copy link
Member

Why does control flow depend on type assignment?

Because control flow analysis looks at things like if (true).

@FroMage
Copy link
Member

FroMage commented Nov 5, 2014

But that sounds like a very special case. Perhaps the part of flow-control that deals with special cases like this can be split into a later phase? Or are these affecting flow-sensitive typing?

@gavinking
Copy link
Member

No, it has to all be done at once. For example if I have:

Integer fun() {
    if (1==1) { assert (false); } else { return 0; }
}

The only way I can tell that this function definitely returns is by knowing the type of false.

@FroMage
Copy link
Member

FroMage commented Nov 5, 2014

Yes, of course, but what I meant to ask is: do we need to know if something definitely returns for flow typing? Or just a subset of flow visitor?

@gavinking
Copy link
Member

Yes, of course, but what I meant to ask is: do we need to know if something definitely returns for flow typing?

Well, yes, we do, that's the whole point of this issue!

@gavinking
Copy link
Member

String fun(String? str) {
    if (is Null str) { assert (false); } 
    return str;
}

@FroMage
Copy link
Member

FroMage commented Nov 5, 2014

Well, no, the point of the issue is that we handle returns, like:

void fun(String? str){
 if(is Null str){ return; }
 print(str);
}

The case of assert(false) is pathological, especially given that it should be written:

String fun(String? str) {
    assert (!is Null str);
    return str;
}

I don't care that assert(false) is not handled, it's such a special case. Now, perhaps there are more interesting cases that need typing for return flow, I just can't think of one at the moment.

@lucaswerkmeister
Copy link
Member

The only way I can tell that this function definitely returns is by knowing the type of false.

I don’t think it needs the type of false or true:

shared Anything f() {
    value myTrue = true;
    if (myTrue) {
        return nothing;
    }
    // error: does not definitely return
}

It seems to depend on the identity of the true / false value, which is also what the spec says (5.4.3):

A boolean condition is considered to be always satisfied if it is a value reference to true. A boolean condition is considered to be never satisfied if it is a value reference to false.

Perhaps this will change with #865, when we can denote the types \otrue and \ofalse, but right now I don’t think this needs type information: It only depends on imports and scoping. Perhaps it’s possible to insert a phase before flow control that only tracks references to true and false? (I. e., if a true / false BME refers to the language local values.)

@gavinking
Copy link
Member

@lucaswerkmeister resolving references and assigning types is something that has to happen at the same time as part of one phase. We can't resolve references and then later assign types to things.

@lucaswerkmeister
Copy link
Member

But we wouldn’t be resolving arbitrary references in that phase – we would just track two names and check if they’re shadowed by anything else (import alias, other import, toplevel value in package, value in surrounding scope). We don’t need to know any types, because only the original true and false count (see above), and we don’t need to inspect QMEs, and therefore don’t need the qualifying expression’s type, because any qualified member is never true or false (unless someone uses package.true in the language module itself).

Or does flow control need more type information than “always satisfied” and “never satisfied”?

@gavinking
Copy link
Member

I also have to take into account inherited members. It's nowhere near as simple as you're making out.

@lucaswerkmeister
Copy link
Member

Ohh, the superclass can also have a member true. Dangit. That probably makes the phase too complicated, and too close to full typechecking.

@gavinking
Copy link
Member

Note that the functionality which blocks this popular issue is #678 and #677, both of which have been around since M6, along with other related similar stuff in ControlFlowVisitor.

@gavinking
Copy link
Member

Given how difficult this would be to implement, we should consider an alternative, which would be to add a block to assert, for example:

Hell, I'm not even sure if the else keyword is really needed for readability here. How about just:

assert (nonempty list) { return null; }
assert (is Foo arg) { throw IllegalTypeException(); }
assert (!name.empty, length>0) { return ""; }

Or, if you guys think it's necessary for readability, we could require an else keyword here:

assert (nonempty list) else { return null; }
assert (is Foo arg) else { throw IllegalTypeException(); }
assert (!name.empty, length>0) else { return ""; }

This is much, much easier to implement, and doesn't actually seem worse to me.

@lucaswerkmeister
Copy link
Member

I think the else makes the meaning much more obvious to readers unfamiliar with the syntax. With else, I really like this idea (though I’d probably still want to have the original issue eventually).

@sgalles
Copy link
Author

sgalles commented Oct 3, 2015

Well I'm certainly not against it because https://groups.google.com/d/msg/ceylon-users/AbcLpWNrlCY/9Aa7XLXhPcQJ 😄
And yes, I also like the version with else

@jvasileff
Copy link
Member

I agree with @lucaswerkmeister's comment.

It may also make sense to allow break and continue.

@lukedegruchy
Copy link

FWIW, I vote for the else version.

@quintesse
Copy link
Member

@jvasileff you have a condition and a true-block and a false-block, to me that's the definition of an if statement.

@sgalles well, first I don't find that it makes code "super clear", but I'll admit it's useful. But an assert is an assert and an if is an if, one is used to throw an exception for the highly unanticipated situation where the condition isn't true while an if is used to make choices. The moment you give an assert an else-block and use it to make decisions it's become an if and you should be using an if in my opinion.

So you guys definitely should not have been writing code like that!

Well you can say that to us, but you just can't prevent the rest of the world from doing that, it's the closest thing Ceylon permits to what is an "early-exit" code style.

@gavinking
Copy link
Member

I have reimplemented this in a slightly less-crappy way.

lucaswerkmeister added a commit to lucaswerkmeister/ceylon-sdk that referenced this issue Oct 4, 2015
The second `if` no longer narrows the type, so use an else instead.
gavinking added a commit to eclipse-archived/ceylon-sdk that referenced this issue Oct 4, 2015
jvasileff added a commit to jvasileff/ceylon.ast that referenced this issue Oct 4, 2015
Blocks may now include synthetic `GuardedVariable`s, which should be ignored.
lucaswerkmeister added a commit to ceylon/ceylon.ast that referenced this issue Oct 5, 2015
@FroMage
Copy link
Member

FroMage commented Oct 5, 2015

So this works for what sort of conditions? is and exists for sure. Others? Multiple conditions?

Due to the semantics of is/exists wrt variables and transient values, it's not possible to get a new definition (virtual else var, after the if condition) that is not constant, which means that it doesn't matter that int is not evaluated three times here (contrary to how it looks):

            if (!exists int) {
                return 0;
            }
            return int+int;

In fact, ATM with my implementation it's evaluated twice instead of the once that would be possible. I assume that's not a problem?

@gavinking
Copy link
Member

  • is, exists, nonempty
  • No, as soon as you have multiple conditions, you can't apply this reasoning, since you have no idea which of the conditions failed.

@gavinking
Copy link
Member

Due to the semantics of is/exists wrt variables and transient values, it's not possible to get a new definition (virtual else var, after the if condition) that is not constant, which means that it doesn't matter that int is not evaluated three times here (contrary to how it looks):

Right. Exactly. @tombentley totally misunderstood the point I was making about what was wrong with the previous implementation. It does not matter if you re-evaluate the variable. It does matter if you re-evaluate the condition. Which was a problem with the previous implementation.

@FroMage
Copy link
Member

FroMage commented Oct 5, 2015

OK thanks.

@FroMage
Copy link
Member

FroMage commented Oct 5, 2015

Mmm, does that also work with destructuring?

@gavinking
Copy link
Member

@FroMage no, I don't think so.

@FroMage
Copy link
Member

FroMage commented Oct 5, 2015

OK.

@FroMage
Copy link
Member

FroMage commented Oct 5, 2015

Just keep in mind that I won't implement anything for what you say is not supported ;)

@FroMage
Copy link
Member

FroMage commented Oct 5, 2015

Done.

@FroMage FroMage closed this as completed Oct 5, 2015
@FroMage FroMage reopened this Oct 5, 2015
@FroMage
Copy link
Member

FroMage commented Oct 5, 2015

Sorry I meant to close the backend issue ;)

@gavinking gavinking modified the milestones: 1.2, 1.3 Oct 5, 2015
@gavinking
Copy link
Member

Well, it's done anyway, and moved to #1434.

@lucaswerkmeister
Copy link
Member

Shouldn’t we have a tracking issue for making this work even with assert (false); and while (true) (milestone “postponed indefinitely”)?

lucaswerkmeister added a commit to eclipse-archived/ceylon.formatter that referenced this issue Oct 28, 2015
Fallout from ceylon/ceylon-spec#891 that only occurs in the IDE (since
the CLI doesn’t run a typechecker).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests