Skip to content
  • Yorick Peterse's avatar
    Implement Inko's compiler in Inko · b1dcad9b
    Yorick Peterse authored
    Inko's compiler is now written in Inko itself, and the source code is
    located in the std::compiler module tree.
    
    == "where" replaced with "when"
    
    The "where" keyword has been replaced with "when". The pattern matching
    syntax (discussed below) uses "when", and instead of having both "where"
    and "when" we opted to just go with "when".
    
    == Pattern matching
    
    We also introduce pattern matching. Pattern matching is introduced as it
    makes various parts of the compiler easier to write. For example,
    instead of using the visitor pattern the compiler can rely on pattern
    matching; resulting in less boilerplate code.
    
    The pattern matching implementation is deliberately kept simple, and
    inspired mostly by Kotlin's "when" expression. This means no support for
    destructuring input into separate variables, and no support for checking
    if an input type is a generic type (due to type erasure).
    
    Pattern matching is performed using the "match" keyword, parentheses
    around the expression to match are required:
    
        match(process.receive) {
          ...
        }
    
    We can check if the input is of a certain type using the "as" pattern:
    
        match(process.receive) {
          as String -> { ... }
        }
    
    When using this pattern, we can also supply an additional guard:
    
        match(process.receive) {
          as String when something -> { ... }
        }
    
    This is useful when the input value is bound to a variable, which can be
    done by using the "let" keyword _inside_ the parentheses. This allows
    for more specific checks:
    
        match(let message = process.receive) {
          as String when message == 'foo' -> { ... }
        }
    
    A fallback case is specified using the "else" keyword:
    
        match(let message = process.receive) {
          as String when message == 'foo' -> { ... }
          else -> { ... }
        }
    
    We can also match arbitrary expressions as patterns. This requires that
    the pattern we are looking for implements the std::operators::Match
    trait:
    
        let number = 4
    
        match(number) {
          1 -> { 'number one' }
          2..4 -> { 'between 2 and 4' }
          else -> { 'something else' }
        }
    
    When matching expressions, we can also specify a "when" guard:
    
        let number = 4
    
        match(number) {
          1 when some_condition -> { 'first' }
          1 -> { 'second' }
          else -> { 'third' }
        }
    
    We can also leave out the expression to match against, in which case
    "match" acts like an if-chain:
    
        match {
          foo? -> { 'foo'}
          bar? -> { 'bar' }
          else -> { 'else' }
        }
    
    When using this syntax, "as" patterns are not supported, and the
    expressions must produce a Boolean (instead of implementing the Match
    trait).
    
    The return type of a "match" expression is either the type of the first
    case (or of the "else" case if no patterns are present), or Dynamic if
    the cases return different types. This allows you to write patterns that
    return different types when you don't care about those types (e.g.  you
    never use the returned value). If all cases return a value of type "T",
    but the fallback case returns Nil, the type is inferred to "?T".
    
    == Local and non-local throw, return, and try expressions
    
    The keywords `return`, `throw`, and `try` now all operate on the method
    level. This means that `throw` for example will throw from the
    surrounding method, not just the surrounding closure. These are called
    non-local expressions, since they are not scoped to the surrounding
    closures.
    
    Local returns, throws, and try expressions are supported using the
    following keywords:
    
    * `local return`
    * `local throw`
    * `local try`
    
    These all unwind from/operate on the surrounding closure. These changes
    ensure that all these keywords operate consistently. Type compatibility
    checks have also been changed so that you can no longer assign a
    throwing closure to an argument or field that doesn't expect one. For
    example, this is no longer valid:
    
        def foo(block: do -> Integer) -> Integer {
          block.call
        }
    
        foo {
          local throw 10
          20
        }
    
    Here `local throw` results in the closure being inferred as
    `do !!  Integer -> Integer`, which is not compatible with
    `do -> Integer`.
    b1dcad9b