Statements as Expressions

Wednesday, April 6, 2011
I decided to take a break from writing docs, and add a language feature that I've been wanting for a while: The ability for certain statements to act as expressions:

// If statement as an expression
let a = if x { 10 } else { 20 };

// Switch statement as an expression
let a = switch x {
  case 1 { "Hello" }
  case 2 { "World" }
  else   { "!" }

// Match statement as an expression
let a = match x {
  as s:String { "String" }
  as w:Widget { "Widget" }
  else        { "Object" }

Note that the type inference engine is able to dive deep into the nested block structure to determine the result type of the expression - as in the second example, the type of 'a' is determined to be a String, since the result of each of the three case blocks is a String. As usual, type constraints flow in both directions:

// Explicitly declare 'a' as type String. The result
// values from each case block will get coerced into
// Strings.
let a:String = match x {
  as s:String { 1 }
  as w:Widget { 2.0 }
  else        { "else" }

There are lots of other subtleties here. For example, a case block might contain a return statement, or other non-local exit, in which case the variable never gets assigned:

let a = match x {
  as s:String { doSomething(); 1 } // The last expression in the block is the result.
  as w:Widget { return false }
  else        { throw IllegalArgumentError() }

I have unit tests for all of the above cases and dozens more edge cases. For example, what happens if the different branches return different types, and the result type is unconstrained? Well, the compiler tries to find a common type that all of the other types can be converted into. If there is no solution, or more than one possible solution, then the compiler complains.

The way this was done was to introduce a new type - I call it the "PHI" type, inspired by LLVM's "PHI" nodes - that represents a set of possible types that could be assigned to a variable. I then added code to the type solver to deal with PHI types. The type solver attempts to fill in the unknown types in an expression tree by finding the best possible fit, given the constraints (where constraints are things like "expression A must be convertible into type B", or "type A will be the return type of one of these five overloaded methods", and so on.) In the case of PHI types, the ranking for the PHI type is equal to the worst ranking for any of its members types. In case you were wondering, the rankings are:

enum ConversionRank {
  Incompatible,      // let x:Exception = 1  No conversion possible
  Truncation,        // let x:ubyte = 256    Value will be truncated
  SignedUnsigned,    // let x:uint = -1      Signed / unsigned mismatch
  PrecisionLoss,     // let x:int = 1.2      Loss of decimal precision
  IntegerToBool,     // let x:bool = 256     Exact but costly
  NonPreferred,      // let x:int = 1.0      Requires transformation
  ExactConversion,   // let x:byte = int(1)  Lossless conversion
  IdenticalTypes,    // let x:int = int(1)   No conversion, same type

I have not yet made "try/catch" usable in an expression context, but doing so will be relatively easy now that I have the infrastructure in place. Note that none of the other statement types - for, while, break, continue, and so on - make sense as expressions, so try/catch is the only one left to do.


Post a Comment