Skip to content
Cameron Purdy edited this page Jul 27, 2021 · 6 revisions

Overview

An expression is generally a language construct that has a type and yields a value. The number 42 is a LiteralExpression, for example, that has some numeric type (IntLiteral) and yields a value of that type that is the value 42. The cliché function or method call foo() is an InvocationExpression and yields whatever value(s) that the foo() function or method is declared to return.

In Ecstasy, while the most common case is that each expression yields a single value, it is possible for an expression to yield zero values (a void expression), or multiple values. The expression has a compile-time type for each value that it will yield. When an expression is required that yields n values, any expression that yields at least n values (and whose types are compatible) can be used, with any additional values being automatically discarded.

An expression may have an implicit type[1], which is the compile-time type of the value that the expression can be safely and obviously assumed to yield. An expression may also be able to produce (or convert to) another type, if it is requested to do so. In some cases, an expression may be able to determine the type that it should produce, based upon a requested type; this is called type inference, and the resulting type is the inferred type.

In common compiler terminology, an expression yields an L-Value and/or an R-Value. An L-Value represents a variable to which a value can be assigned, while an R-Value represents a value. Some expressions, such as a NameExpression (used for a variable name, among other things) or an ArrayAccessExpression, can represent either an L-Value or an R-Value, depending on how the expression is being used.

By default, an expression does not change the Variable Assignment State (VAS[2]): A variable is unassigned after an expression if it is unassigned before the expression; assigned after an expression if assigned before the expression; and assumed to be effectively final after the expression if assumed to be effectively final before the expression. For each expression form, any behavior that differs from this default will be documented.

An expression is said to be short-circuiting if the expression may arc (dislocate its execution) to a ground point, without yielding a value. The ground point is provided by an enclosing expression or statement, but not all expressions and statements provide a ground point, and thus in many cases in the source code, a short-circuiting expression is not permitted. To differentiate between compile-time and run-time behavior:

  • An expression may arc at runtime, which means that the short-circuit mechanism is actually triggered as part of the runtime evaluation of the expression; and
  • An expression is said to short-circuit iff the compiler determines that the expression has the potential to arc at runtime.

Syntactically, the Expression construct represents the trailing “else” (the “ground point”) for a short-circuiting expression:

    Expression:
        ElseExpression

Operator Precedence

Operator Description Level Associativity
& reference-of 1
++ post-increment 2 left to right
-- post-decrement
() invocation
^() invocation (async)
[] access array element
? conditional not-null
. access object member
.new virtual child creation
.as type assertion
.is type comparison
++ pre-increment 3 right to left
-- pre-decrement
+ unary plus
- unary minus
! logical not
~ bitwise not
?: conditional elvis 4 right to left
* multiplication 5 left to right
/ division
% modulo
/% division and modulo
+ addition 6 left to right
- subtraction
<< shift left 7 left to right
>> shift right
>>> unsigned shift right
& bit and
| bit or
^ bit xor
.. range/interval 8 left to right
< less than 9 left to right
<= less than or equal
> greater than
>= greater than or equal
<=> order ("space-ship")
== equality 10 left to right
!= inequality
&& logical and 11 left to right
|| logical or 12 left to right
^^ logical xor
? : conditional ternary 13 right to left
: conditional ELSE 14 right to left

Definite Assignment

There are several mechanisms that are used to identify potential “unassigned variable” errors at compile time, and to help the Ecstasy compiler produce the appropriate code for capabilities such as variable capture. The following terms are used through the language specification:

  • Definitely Assigned – given a variable, and given a specific point in the code, the variable has provably been assigned a value by the time that point in the code has been reached; it is a compile-time error and a runtime exception to access a variable that has not been assigned.
  • Definitely Unassigned – given a variable, and given a specific point in the code, the variable has provably never been assigned a value by the time that point in the code has been reached.
  • Effectively Final – given a variable, the variable is provably assigned once and only once for the lifetime of the variable.
  • Explicitly Final – a manner of declaring a variable such that it is a compile-time error and run-time exception for the variable to be assigned more than once.
  • Permissibly Unassigned – a manner of declaring a variable such that the definite assignment rules are ignored at compile time for that one variable.
  • Local Variable – a named variable declared within a method or function body, and which is created (if and as needed) each time that the method or function is executed.
  • Lexically Scoped – a local variable begins its lexical scope at the point in the source code at which it is declared, and continues to the end of the statement within which it is declared.
  • Structurally Scoped – a local variable begins its structural scope at the point in the source code at which it is declared; suspends its structural scope at any point that declares a nested class (including an anonymous inner class), property, or method/function (including a lambda); resumes its structural scope at the conclusion of any such declaration; and continues its structural scope to the end of the statement within which it is declared. (In other words, a variable’s structural scope is the portion of a variable’s lexical scope for which the compiler produces code for the body of the method/function that declared the local variable.)
  • Variable Capture – the mechanism by which access to a local variable is made possible from a lambda expression or an anonymous inner class, when that local variable is lexically scoped but not structurally scoped.

Definite assignment is enforced for constants, properties, and local variables:

  • It is relatively simple to prove definite assignment at compile time for constants, since they are required to have a value specified as part of their declaration.
  • Properties that have a field may have an initial value specified as part of their declaration, or may be initialized by a constructor. It is a compile-time error for a class to have a constructor that does not result in a value being assigned to each property that has a field. It is a runtime exception for an object to be instantiated in a manner that does not assign a value to each property that has a field.
  • Local variables must be definitely assigned before they are used. While local variables are typically assigned as part of their declaration, it is possible for a local variable to be declared without being assigned. Additionally, there are complexities associated with control flow mechanisms (loops, conditionals, etc.) that must be factored into the determination of definite assignment.

To this end, the compiler tracks three different attributes (definitely assigned, definitely unassigned, and effectively final) as it relates to each variable, and each statement and expression is specified in terms of how it affects these three attributes. These three attributes form the Variable Assignment State (VAS). Specifically, by tracking these three different attributes for every variable, the compiler is able to determine for each statement and expression:

  • Whether the variable is definitely unassigned before the statement or expression;
  • Whether the variable is definitely unassigned after the statement or expression;
  • Whether the variable is definitely assigned before the statement or expression;
  • Whether the variable is definitely assigned after the statement or expression.

For loops, conditional statements and expressions, and expressions of type Boolean, the compiler is able to further determine:

  • Whether the variable is definitely unassigned after the statement or expression when true;
  • Whether the variable is definitely unassigned after the statement or expression when false;
  • Whether the variable is definitely assigned after the statement or expression when true;
  • Whether the variable is definitely assigned after the statement or expression when false.

Lastly, the compiler is able to determine which local variable are effectively final.

The following acronyms are used:

  • VAS – the Variable Assignment State;
  • VAST – the Variable Assignment State when true; and
  • VASF – the Variable Assignment State when false.

[1]

Quite often, when the arity of the expression is not relevant to the point being discussed, the singular form will be used, instead of always saying “type or types”.

[2]

Vapid Acronym Syndrome (VAS): While the use of new acronyms is generally avoided in this specification, this term will be used extensively enough that the shortening to an ugly acronym will be well worth it. We promise.