Implement Inko's compiler in Inko

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`.
parent 35fb0ee7
Pipeline #172613500 passed with stages
in 57 minutes and 2 seconds
......@@ -37,7 +37,6 @@ require 'inkoc/ast/lambda'
require 'inkoc/ast/method'
require 'inkoc/ast/define_argument'
require 'inkoc/ast/define_type_parameter'
require 'inkoc/ast/pair'
require 'inkoc/ast/define_variable'
require 'inkoc/ast/define_attribute'
require 'inkoc/ast/attribute'
......@@ -58,6 +57,10 @@ require 'inkoc/ast/documentation'
require 'inkoc/ast/module_documentation'
require 'inkoc/ast/method_requirement'
require 'inkoc/ast/dereference'
require 'inkoc/ast/match'
require 'inkoc/ast/match_type'
require 'inkoc/ast/match_expression'
require 'inkoc/ast/match_else'
require 'inkoc/config'
require 'inkoc/prototype_id'
require 'inkoc/constant_resolver'
......@@ -111,9 +114,11 @@ require 'inkoc/tir/instruction/get_parent_local'
require 'inkoc/tir/instruction/set_parent_local'
require 'inkoc/tir/instruction/goto_next_block_if_true'
require 'inkoc/tir/instruction/goto_block_if_true'
require 'inkoc/tir/instruction/goto_block'
require 'inkoc/tir/instruction/skip_next_block'
require 'inkoc/tir/instruction/local_exists'
require 'inkoc/tir/instruction/return'
require 'inkoc/tir/instruction/throw'
require 'inkoc/tir/instruction/run_block'
require 'inkoc/tir/instruction/run_block_with_receiver'
require 'inkoc/tir/instruction/tail_call'
......
# frozen_string_literal: true
module Inkoc
module AST
class Match
include Predicates
include Inspect
include TypeOperations
attr_accessor :bind_to_symbol
attr_reader :expression, :bind_to, :arms, :match_else, :location
def initialize(expression, bind_to, arms, match_else, location)
@expression = expression
@bind_to = bind_to
@arms = arms
@match_else = match_else
@location = location
@bind_to_symbol = nil
end
def visitor_method
:on_match
end
end
end
end
......@@ -2,18 +2,21 @@
module Inkoc
module AST
class Pair
include TypeOperations
class MatchElse
include Predicates
include Inspect
include TypeOperations
attr_reader :key, :value, :location
attr_reader :body, :location
def initialize(key, value, location)
@key = key
@value = value
def initialize(body, location)
@body = body
@location = location
end
def visitor_method
:on_match_else
end
end
end
end
# frozen_string_literal: true
module Inkoc
module AST
class MatchExpression
include Predicates
include Inspect
include TypeOperations
attr_reader :patterns, :guard, :body, :location
def initialize(patterns, guard, body, location)
@patterns = patterns
@guard = guard
@body = body
@location = location
end
def visitor_method
:on_match_expression
end
end
end
end
# frozen_string_literal: true
module Inkoc
module AST
class MatchType
include Predicates
include Inspect
include TypeOperations
attr_reader :pattern, :guard, :body, :location
def initialize(pattern, guard, body, location)
@pattern = pattern
@guard = guard
@body = body
@location = location
end
def visitor_method
:on_match_type
end
end
end
end
......@@ -7,12 +7,11 @@ module Inkoc
include Predicates
include Inspect
attr_reader :value, :location
attr_reader :value, :location, :local
# value - The value to return, if any.
# location - The SourceLocation of the return statement.
def initialize(value, location)
def initialize(value, local, location)
@value = value
@local = local
@location = location
end
......
......@@ -7,12 +7,11 @@ module Inkoc
include Predicates
include Inspect
attr_reader :value, :location
attr_reader :value, :location, :local
# value - The value to throw
# location - The SourceLocation of the throw statement.
def initialize(value, location)
def initialize(value, local, location)
@value = value
@local = local
@location = location
end
......
......@@ -7,17 +7,14 @@ module Inkoc
include Predicates
include Inspect
attr_reader :expression, :else_argument, :else_body, :location
attr_reader :expression, :else_argument, :else_body, :location, :local
attr_accessor :try_block_type, :else_block_type
# expr - The expression that may throw an error.
# else_body - The body of the "else" statement.
# else_arg - The argument to store the error in, if any.
# location - The SourceLocation of the "try" statement.
def initialize(expr, else_body, else_arg, location)
def initialize(expr, else_body, else_arg, local, location)
@expression = expr
@else_argument = else_arg
@else_body = else_body
@local = local
@location = location
@try_block_type = nil
@else_block_type = nil
......
......@@ -26,6 +26,7 @@ module Inkoc
PRELUDE_MODULE = 'prelude'
MARKER_MODULE = 'std::marker'
OPERATORS_MODULE = 'std::operators'
OBJECT_CONST = 'Object'
TRAIT_CONST = 'Trait'
......@@ -43,6 +44,7 @@ module Inkoc
ARRAY_TYPE_PARAMETER = 'T'
OPTIONAL_CONST = 'Optional'
ANY_TRAIT_CONST = 'Any'
MATCH_CONST = 'Match'
MODULE_TYPE = 'Module'
SELF_TYPE = 'Self'
......@@ -71,6 +73,7 @@ module Inkoc
OBJECT_NAME_INSTANCE_ATTRIBUTE = '@_object_name'
IMPLEMENTED_TRAITS_INSTANCE_ATTRIBUTE = '@_implemented_traits'
INIT_MESSAGE = 'init'
MATCH_MESSAGE = '=~'
RESERVED_CONSTANTS = Set.new(
[
......
......@@ -255,6 +255,10 @@ module Inkoc
error("cannot throw a value of type #{tname} at the top-level", location)
end
def invalid_method_throw_error(location)
error('The "throw" keyword can only be used in a method', location)
end
def missing_throw_error(throw_type, location)
tname = throw_type.type_name.inspect
......@@ -361,6 +365,13 @@ module Inkoc
error('The "return" keyword can only be used inside a method', location)
end
def invalid_local_return_error(location)
error(
'The "local return" keyword can only be used inside a method, closure, or lambda',
location
)
end
def invalid_cast_error(from, to, location)
fname = from.type_name.inspect
tname = to.type_name.inspect
......@@ -406,5 +417,63 @@ module Inkoc
location
)
end
def pattern_match_dynamic(location)
error(
'Using an expression to match a Dynamic type is not supported',
location
)
TypeSystem::Error.new
end
def guard_return_type_error(type, location)
error(
'Match guards must return a Boolean, not a ' + type.type_name,
location
)
TypeSystem::Error.new
end
def pattern_matching_unavailable(location)
error(
'Pattern matching requires that std::operators is compiled first',
location
)
TypeSystem::Error.new
end
def invalid_match_pattern(type, location)
error(
"The type #{type.type_name.inspect} can't be used for pattern matching," \
" as it does not implement std::operators::Match",
location
)
TypeSystem::Error.new
end
def invalid_boolean_match_pattern(location)
error('This expression must produce a Boolean', location)
TypeSystem::Error.new
end
def match_type_test_unavailable(location)
error(
'Type tests are only available when match() is given an argument',
location
)
TypeSystem::Error.new
end
def generic_match_type(location)
error("Generic types can't be used when pattern matching", location)
TypeSystem::Error.new
end
end
end
......@@ -22,8 +22,10 @@ module Inkoc
'impl' => :impl,
'for' => :for,
'lambda' => :lambda,
'where' => :where,
'static' => :static
'static' => :static,
'match' => :match,
'when' => :when,
'local' => :local
}.freeze
SPECIALS = Set.new(
......@@ -373,6 +375,9 @@ module Inkoc
end
next
elsif char == "\n"
@line += 1
@column = 1
end
in_escape = false if in_escape
......@@ -485,7 +490,20 @@ module Inkoc
end
def assign_or_equal
operator(1, :assign, :equal)
advance = 1
token_type =
case @input[@position + 1]
when '='
advance = 2
:equal
when '~'
advance = 2
:match_equal
else
:assign
end
new_token(token_type, @position, @position += advance)
end
def not_equal_or_type_args_open_or_throws
......
......@@ -47,6 +47,8 @@ module Inkoc
try
do
lambda
match
match_equal
]
).freeze
......@@ -73,6 +75,7 @@ module Inkoc
trait
try
try_bang
match
]
).freeze
......@@ -99,6 +102,7 @@ module Inkoc
pow
inclusive_range
exclusive_range
match_equal
]
).freeze
......@@ -567,6 +571,7 @@ module Inkoc
when :do, :lambda then block(start, start.type)
when :let then let_define(start)
when :return then return_value(start)
when :local then local_return_or_throw(start)
when :attribute then attribute_or_reassign(start)
when :self then self_object(start)
when :throw then throw_value(start)
......@@ -575,6 +580,7 @@ module Inkoc
when :colon_colon then global(start)
when :paren_open then grouped_expression
when :documentation then documentation(start)
when :match then pattern_match(start)
else
raise ParseError, "A value can not start with a #{start.type.inspect}"
end
......@@ -892,7 +898,7 @@ module Inkoc
end
def optional_method_requirements
return [] unless @lexer.next_type_is?(:where)
return [] unless @lexer.next_type_is?(:when)
skip_one
......@@ -922,7 +928,7 @@ module Inkoc
required = []
loop do
required << type_name(advance_and_expect!(:constant))
required << type(advance!)
break unless @lexer.next_type_is?(:add)
......@@ -1042,17 +1048,17 @@ module Inkoc
name = advance_and_expect!(:constant)
targs = optional_type_parameter_definitions
required_traits =
required =
if @lexer.next_type_is?(:colon)
skip_one
trait_requirements
required_traits
else
[]
end
body = trait_body(advance_and_expect!(:curly_open))
AST::Trait.new(name.value, targs, required_traits, body, start.location)
AST::Trait.new(name.value, targs, required, body, start.location)
end
def trait_body(start)
......@@ -1065,20 +1071,6 @@ module Inkoc
AST::Body.new(nodes, start.location)
end
# Parses a list of traits that must be implemented by whatever implements
# the current trait.
def trait_requirements
required = []
while @lexer.next_type_is?(:constant)
required << constant(advance!)
advance! if @lexer.next_type_is?(:add)
end
required
end
# Parses the implementation of a trait or re-opening of an object.
#
# Example:
......@@ -1130,10 +1122,25 @@ module Inkoc
# Example:
#
# return 10
def return_value(start)
def return_value(start, local = false, location = start.location)
value = expression(advance!) if next_expression_is_argument?(start)
AST::Return.new(value, start.location)
AST::Return.new(value, local, location)
end
def local_return_or_throw(start)
next_token = advance!
case next_token.type
when :return
return_value(next_token, true, start.location)
when :throw
throw_value(next_token, true, start.location)
when :try
try(next_token, true, start.location)
else
raise ParseError, 'expected `throw`, `return`, or `try`'
end
end
def attribute_or_reassign(start)
......@@ -1222,10 +1229,8 @@ module Inkoc
# Example:
#
# throw Foo
def throw_value(start)
value = expression(advance!)
AST::Throw.new(value, start.location)
def throw_value(start, local = false, location = start.location)
AST::Throw.new(expression(advance!), local, location)
end
# Parses a "try" statement.
......@@ -1235,7 +1240,7 @@ module Inkoc
# try foo
# try foo else bar
# try foo else (error) { error }
def try(start)
def try(start, local = false, location = start.location)
expression = try_expression
else_arg = nil
......@@ -1250,7 +1255,7 @@ module Inkoc
AST::Body.new([], start.location)
end
AST::Try.new(expression, else_body, else_arg, start.location)
AST::Try.new(expression, else_body, else_arg, local, location)
end
# Parses a "try!" statement
......@@ -1258,7 +1263,7 @@ module Inkoc
expression = try_expression
else_arg, else_body = try_bang_else(start)
AST::Try.new(expression, else_body, else_arg, start.location)
AST::Try.new(expression, else_body, else_arg, false, start.location)
end
def try_expression
......@@ -1354,6 +1359,82 @@ module Inkoc
type
end
def pattern_match(start)
bind_to = nil
to_match = nil
match_arms = []
match_else = nil
if @lexer.next_type_is?(:paren_open)
advance_and_expect!(:paren_open)
if @lexer.next_type_is?(:let)
skip_one
bind_to = identifier_from_token(advance_and_expect!(:identifier))
advance_and_expect!(:assign)
end
to_match = expression(advance!)
advance_and_expect!(:paren_close)
end
advance_and_expect!(:curly_open)
while @lexer.peek.valid_but_not?(:curly_close)
token = @lexer.advance
case token.type
when :as
type = type_name(advance_and_expect!(:constant))
guard =
if @lexer.next_type_is?(:when)
skip_one
expression(advance!)
end
advance_and_expect!(:arrow)
body = block_body(advance_and_expect!(:curly_open))
match_arms << AST::MatchType.new(type, guard, body, token.location)
when :else
advance_and_expect!(:arrow)
else_body = block_body(advance_and_expect!(:curly_open))
match_else = AST::MatchElse.new(else_body, token.location)
# "else" must be last
break
else
tests = []
tests << expression(token)
while @lexer.next_type_is?(:comma)
skip_one
tests << expression(advance!)
end
guard =
if @lexer.next_type_is?(:when)
skip_one
expression(advance!)
end
advance_and_expect!(:arrow)