-
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`.