ALF 1.1 RTF Avatar
  1. OMG Issue

ALF11 — Multiplicity and typing of local names should be better tracked

  • Key: ALF11-77
  • Status: closed  
  • Source: Model Driven Solutions ( Mr. Ed Seidewitz)
  • Summary:

    Issue ALF11-44 points out that the multiplicity conformance rules currently in Alf allow a behavior or operation with a mandatory parameter (multiplicity lower bound greater than 1) to be called with a null value for that parameter. The result is not an error, but that the behavior or operation is not actually invoked – and that it does not produce any results on its output and return parameters, not even a null token. The proposed solution is to add a "null coalescing" operator to Alf and to then tighten the multiplicity conformance rules.

    While a null-coalescing operator would, indeed, be useful to handle the identified problem, it does this at the expense of being backward incompatible. The tightened multiplicity rules would make illegal many invocations that are now legal, especially since the default parameter multiplicity in UML is 1..1, but local names get a multiplicity lower bound of 0. This could require that usage of the null-coalescing operator be inserted in a lot of places, which would be annoying.

    An alternate approach would be to better track the statically known multiplicity and typing of local names, based both on their latest assignments and on certain statically analyzable Boolean expressions (such as checks for null and for type classification). Since, many times, calls involving mandatory parameters will already be surrounded by such statically analyzable checks, this would allow these calls to remain legal, and such typical paradigms to continue to be used, even with the tighter multiplicity rules.

  • Reported: ALF 1.0 — Mon, 1 Aug 2016 14:45 GMT
  • Disposition: Resolved — ALF 1.1
  • Disposition Summary:

    Adjust local names for multiplicity and typing

    The problem addressed in this issue can be highlighted by a simple example:

    activity GetNextLine() : String[0..1];
    
    activity WriteNextLine() {
        line = GetNextLine();
        WriteLine("Next line: " + line);
    }
    

    According to the rules of Alf 1.0.1, the local name line is given the multiplicity 0..1. This is allowable in the string concatenation but, since line has a multiplicity lower bound of 0, the derived multiplicity lower bound for the result of the concatenation is also 0. Again, this is currently statically allowable for an argument to the WriteLine activity, even though the declared multiplicity of the in parameter for that activity is 1..1.

    However, the return parameter of the GetNextLine is declared to be 0..1, meaning that it may return null. If it does, line is then assigned null, but there is no “null pointer exception” when line is used in the next statement. Instead, due to the UML semantics of null as meaning “no value”, the null value for line essentially gets propagated. Because the string concatenation operator receives no value ("null") for its second operand, it cannot execute, so the concatenation expression also does not produce a value. The call to WriteLine thus receives no input value and, because its input parameter has multiplicity 1..1, it cannot execute, either. The result is that nothing is printed, not “Next line:”, not even a carriage return. Most people seem to find this behavior unexpected.

    There are a number of possible ways to avoid this behavior by specifically testing whether line is null. Here are three of the possible alternatives:

    // Alternative 1
    activity WriteNextLine() {
        line = GetNextLine();
        if (line == null) {
            line = "<none>";
        }
        WriteLine("Next line: " + line);
    }
    
    // Alternative 2
    activity WriteNextLine() {
        line = GetNextLine();
        WriteLine("Next line: " + (line == null? "<none>": line));
    }
    
    // Alternative 3
    activity WriteNextLine() {
        line = GetNextLine();
        if (line == null) {
            WriteLine("No next line");
        } else {
            WriteLine("Next line: " + line);
        }
    }
    

    Each of these alternatives represents a common paradigm for avoiding this problem, similar to the approaches used to avoid a null pointer exception in other languages. However, since the original version produces subtle and unexpected behavior at runtime, rather than some obvious exception, it would be preferable to make the original version statically illegal by disallowing an expression with multiplicity lower bound of 0 from being an argument for a mandatory parameter. Unfortunately, this would also make all three of the above alternative versions illegal.

    A null-coalescing operator, as proposed in issue ALF11-44, could be used to make the above alternatives legal again. However, this would require modifying working code for no functional change. It would also be particularly pointless in the third alternative, in which an entirely different message is written if line is null.

    What would be more desirable instead would be to track more carefully the statically known multiplicity of a local name after each assignment/reassignment and based on simple tests such as line == null. In this case, it could be determined that, in each of the above alternatives, the second operand expression in the string concatenation expression could never actually be null at that point, and so that operand expression can be considered to have a multiplicity lower bound of 1. In this case, it would be possible to strengthen the multiplicity conformance rules such that the original version of this example would be illegal, while allowing each of the presented alternatives to remain legal.

    In addition, since casting in Alf "filters out" values that cannot be cast, rather than producing a cast exception, cast expressions can also result in null (if everything is filtered out). For example, consider the following:

    abstract class A;
    class B specializes A;
    class C specializes A;
    
    activity getA() : A;
    activity doSomethingB(in b: B);
    activity doSomethingC(in c: C);
    
    activity doSomething() {
        a = getA();
        if (a instanceof B) {
            doSomethingB((B)a);
        } else if (a instanceof C) {
            doSomethingC((C)a);
        }
    }
    

    This is currently legal, even though the cast expressions (B)a and (C)a have multiplicity lower bounds of 0 (because, e.g., (B)a would result in null if the value assigned to a was actually of type C), while the in parameters to doSomethingB and doSomethingC have multiplicity 1..1 (by default). But, with tighter multiplicity rules, both of the doSomethingB and doSomethingC calls would become illegal, unless it was also tracked that a is actually known to have the subtype B in the first clause of the if statement and subtype C in the second clause. Indeed, if this were tracked, then it would be possible to allow the use of a in each of the calls without having to do a cast at all.

    Finally, it should be possible to track local name multiplicity and type classification information based on compound conditions using, at least, conditional-logical and Boolean negation operators. For instance, it should be possible to deduce from a != null && a instanceof B both that a is not null and that a has the subtype B. (Note that the UML object classification action in UML presumes a non-null object input.) This should be a sufficient level of analytical capability for the language at this time. More sophisticated analysis could be added in the future, if experiences shows its worth.

  • Updated: Thu, 22 Jun 2017 16:40 GMT