Commit 34cc9860 authored by Yorick Peterse's avatar Yorick Peterse

More work on try/throw support & validation

parent 0487553c
......@@ -19,7 +19,7 @@ Metrics/BlockLength:
- spec/**/*
Metrics/AbcSize:
Max: 25
Max: 30
Metrics/ParameterLists:
Max: 10
......
......@@ -12,7 +12,7 @@ require 'inkoc/source_location'
require 'inkoc/token'
require 'inkoc/lexer'
require 'inkoc/parser'
require 'inkoc/scope'
require 'inkoc/type_scope'
require 'inkoc/module_paths_cache'
require 'inkoc/visitor_methods'
require 'inkoc/ast/type_operations'
......@@ -59,6 +59,7 @@ require 'inkoc/pass/insert_implicit_imports'
require 'inkoc/pass/track_module'
require 'inkoc/pass/compile_imported_modules'
require 'inkoc/pass/define_types'
require 'inkoc/pass/validate_throw'
require 'inkoc/pass/generate_tir'
require 'inkoc/pass/configure_module'
require 'inkoc/pass/code_generation'
......@@ -68,6 +69,7 @@ require 'inkoc/pass/dead_code'
require 'inkoc/pass/setup_symbol_tables'
require 'inkoc/compiler'
require 'inkoc/tir/module_config'
require 'inkoc/tir/catch_entry'
require 'inkoc/tir/code_object'
require 'inkoc/tir/basic_block'
require 'inkoc/tir/instruction/predicates'
......@@ -85,6 +87,7 @@ require 'inkoc/tir/instruction/get_toplevel'
require 'inkoc/tir/instruction/get_true'
require 'inkoc/tir/instruction/get_false'
require 'inkoc/tir/instruction/goto_next_block_if_true'
require 'inkoc/tir/instruction/skip_next_block'
require 'inkoc/tir/instruction/load_module'
require 'inkoc/tir/instruction/local_exists'
require 'inkoc/tir/instruction/return'
......@@ -99,6 +102,7 @@ require 'inkoc/tir/instruction/set_hash_map'
require 'inkoc/tir/instruction/set_integer'
require 'inkoc/tir/instruction/set_local'
require 'inkoc/tir/instruction/set_object'
require 'inkoc/tir/instruction/set_register'
require 'inkoc/tir/instruction/set_string'
require 'inkoc/tir/instruction/throw'
require 'inkoc/tir/instruction/try'
......@@ -119,7 +123,9 @@ require 'inkoc/type/dynamic'
require 'inkoc/type/trait'
require 'inkoc/type/optional'
require 'inkoc/type/self_type'
require 'inkoc/type/void'
require 'inkoc/codegen/compiled_code'
require 'inkoc/codegen/instruction'
require 'inkoc/codegen/literals'
require 'inkoc/codegen/serializer'
require 'inkoc/codegen/catch_entry'
......@@ -28,6 +28,10 @@ module Inkoc
def visitor_method
:on_block
end
def block_type
type
end
end
end
end
......@@ -22,8 +22,8 @@ module Inkoc
:on_body
end
def single_expression?
@expressions.length == 1
def multiple_expressions?
@expressions.length > 1
end
def last_expression
......
......@@ -8,7 +8,7 @@ module Inkoc
include Inspect
attr_reader :name, :location, :receiver
attr_accessor :return_type, :type_parameters, :required_traits, :optional
attr_accessor :return_type, :type_parameters, :optional
# name - The name of the constant as a String.
# location - The SourceLocation of the constant.
......@@ -19,7 +19,6 @@ module Inkoc
@location = location
@return_type = nil
@type_parameters = []
@required_traits = []
@optional = false
end
......@@ -35,6 +34,10 @@ module Inkoc
name == Config::SELF_TYPE
end
def dynamic_type?
name == Config::DYNAMIC_TYPE
end
def visitor_method
:on_constant
end
......
......@@ -9,11 +9,14 @@ module Inkoc
attr_reader :name, :location
attr_accessor :block_type
# name - The name of the identifier.
# location - The SourceLocation of the identifier.
def initialize(name, location)
@name = name
@location = location
@method_type = nil
end
def identifier?
......
......@@ -25,6 +25,7 @@ module Inkoc
@body = body
@location = loc
@required = required
@type = nil
end
def required?
......@@ -38,6 +39,10 @@ module Inkoc
def method?
true
end
def block_type
type
end
end
end
end
......@@ -34,6 +34,10 @@ module Inkoc
def keyword_argument?
false
end
def throw?
false
end
end
end
end
......@@ -9,7 +9,7 @@ module Inkoc
attr_reader :name, :receiver, :arguments, :location
attr_accessor :receiver_type
attr_accessor :receiver_type, :block_type
# name - The name of the message as a String.
# receiver - The object to send the message to.
......@@ -20,6 +20,8 @@ module Inkoc
@receiver = receiver
@arguments = arguments
@location = location
@receiver_type = nil
@method_type = nil
end
def visitor_method
......@@ -31,9 +33,8 @@ module Inkoc
end
def raw_instruction?
receiver &&
receiver.constant? &&
receiver.name == Config::RAW_INSTRUCTION_RECEIVER
receiver&.constant? &&
receiver&.name == Config::RAW_INSTRUCTION_RECEIVER
end
def raw_instruction_visitor_method
......
......@@ -19,6 +19,10 @@ module Inkoc
def visitor_method
:on_throw
end
def throw?
true
end
end
end
end
......@@ -20,6 +20,7 @@ module Inkoc
@object_name = object_name
@body = body
@location = location
@block_type = nil
end
def visitor_method
......
......@@ -8,21 +8,69 @@ module Inkoc
include Inspect
attr_reader :expression, :else_argument, :else_body, :location
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.
# else_body - The body of the "else" statement, if any.
# location - The SourceLocation of the "try" statement.
def initialize(expr, else_arg, else_body, location)
def initialize(expr, else_body, else_arg, location)
@expression = expr
@else_argument = else_arg
@else_body = else_body
@location = location
@try_block_type = nil
@else_block_type = nil
end
def visitor_method
:on_try
end
def explicit_block_for_else_body?
else_argument || else_body.multiple_expressions?
end
def empty_else?
else_body.empty?
end
def else_argument_name
else_argument&.name
end
def type_scope_for_else(self_type)
scope = TypeScope.new(self_type, else_block_type, else_body.locals)
scope.define_self_local
scope
end
def define_else_argument_type
return unless (arg_name = else_argument_name)
type = throw_type
else_block_type.define_required_argument(arg_name, type)
else_body.locals.define(arg_name, type)
end
def throw_type
btype =
if expression.throw?
try_block_type
else
expression.block_type
end
if btype&.physical_type?
# For method calls (e.g. "try foo") we want the thrown type to resolve
# to the type thrown my the "fo" method.
btype.throws
else
btype
end
end
end
end
end
......@@ -4,6 +4,10 @@ module Inkoc
module AST
module TypeOperations
attr_accessor :type
def block_type
Type::Void.new
end
end
end
end
# frozen_string_literal: true
module Inkoc
module Codegen
class CatchEntry
attr_reader :start, :stop, :jump_to, :register
def initialize(start, stop, jump_to, register)
@start = start
@stop = stop
@jump_to = jump_to
@register = register
end
end
end
end
......@@ -5,10 +5,10 @@ module Inkoc
class CompiledCode
include Inspect
attr_reader :name, :instructions, :literals, :code_objects, :catch_table
attr_reader :name, :instructions, :literals, :code_objects
attr_accessor :arguments, :required_arguments, :rest_argument, :locals,
:registers, :captures
:registers, :captures, :catch_table
def initialize(name, location)
@name = name
......@@ -22,7 +22,7 @@ module Inkoc
@instructions = []
@literals = Literals.new
@code_objects = Literals.new
@catch_table = nil
@catch_table = []
end
def file
......
......@@ -13,6 +13,7 @@ module Inkoc
Pass::CompileImportedModules,
Pass::SetupSymbolTables,
Pass::DefineTypes,
Pass::ValidateThrow,
Pass::GenerateTir,
Pass::DeadCode,
Pass::CodeGeneration,
......
......@@ -29,6 +29,7 @@ module Inkoc
MODULE_TYPE = 'Module'
SELF_TYPE = 'Self'
DYNAMIC_TYPE = 'Dynamic'
MODULES_ATTRIBUTE = 'Modules'
# The name of the constant to use as the receiver for raw instructions.
......@@ -37,7 +38,9 @@ module Inkoc
MODULE_GLOBAL = 'ThisModule'
SELF_LOCAL = 'self'
MODULE_SEPARATOR = '::'
BLOCK_NAME = '<block>'
BLOCK_NAME = 'do'
TRY_BLOCK_NAME = '<try>'
ELSE_BLOCK_NAME = '<else>'
IMPL_NAME = '<impl>'
DEFINE_REQUIRED_METHOD_MESSAGE = 'define_required_method'
IMPLEMENT_TRAIT_MESSAGE = 'implement_trait'
......
......@@ -110,8 +110,8 @@ module Inkoc
end
def type_error(expected, found, location)
exp_name = expected.type_name
found_name = found.type_name
exp_name = expected.type_name.inspect
found_name = found.type_name.inspect
error(
"Expected a value of type #{exp_name} instead of #{found_name}",
......@@ -120,8 +120,8 @@ module Inkoc
end
def return_type_error(expected, found, location)
exname = expected.type_name
fname = found.type_name
exname = expected.type_name.inspect
fname = found.type_name.inspect
error(
"Expected a value of type #{exname} to be returned instead of #{fname}",
......@@ -236,10 +236,20 @@ module Inkoc
tname = throw_type.type_name.inspect
error(
"this method is expected to throw a value of type #{tname} " \
"this block is expected to throw a value of type #{tname} " \
'but no value is ever thrown',
location
)
end
def missing_try_error(throw_type, location)
tname = throw_type.type_name.inspect
error(
"This message may throw a value of type #{tname} but the `try` " \
'statement is missing',
location
)
end
end
end
......@@ -808,7 +808,7 @@ module Inkoc
skip_one
type_name(advance_and_expect!(:constant))
type_name_or_optional_type(advance!)
end
# Parses a definition of an immutable variable.
......@@ -1071,21 +1071,24 @@ module Inkoc
# Examples:
#
# try foo
# try { foo }
# try foo else bar
# try foo else (error) { error }
def try(start)
body = block_with_optional_curly_braces
expression = expression(advance!)
else_arg = nil
else_body = nil
if @lexer.next_type_is?(:else)
skip_one
else_body =
if @lexer.next_type_is?(:else)
skip_one
else_arg = optional_else_arg
else_body = block_with_optional_curly_braces
end
else_arg = optional_else_arg
block_with_optional_curly_braces
else
AST::Body.new([], start.location)
end
AST::Try.new(body, else_arg, else_body, start.location)
AST::Try.new(expression, else_body, else_arg, start.location)
end
def block_with_optional_curly_braces
......
......@@ -39,6 +39,18 @@ module Inkoc
compiled_code.locals = code_object.local_variables_count
compiled_code.registers = code_object.registers_count
compiled_code.captures = false # TODO: implement capturing
set_catch_entries(compiled_code, code_object)
end
def set_catch_entries(compiled_code, code_object)
compiled_code.catch_table = code_object.catch_table.map do |entry|
start = entry.try_block.instruction_offset
stop = entry.try_block.instruction_end
jump_to = entry.else_block.instruction_offset
Codegen::CatchEntry.new(start, stop, jump_to, entry.register.id)
end
end
def on_get_array_prototype(tir_ins, compiled_code, *)
......@@ -138,6 +150,12 @@ module Inkoc
compiled_code.instruct(:GotoIfTrue, [index, register], tir_ins.location)
end
def on_skip_next_block(tir_ins, compiled_code, basic_block)
index = basic_block.next.next.instruction_offset
compiled_code.instruct(:Goto, [index], tir_ins.location)
end
def on_load_module(tir_ins, compiled_code, *)
register = tir_ins.register.id
path = tir_ins.path.id
......@@ -263,8 +281,11 @@ module Inkoc
compiled_code.instruct(:Throw, [reg], tir_ins.location)
end
def on_try(tir_ins, compiled_code, *)
# TODO: implement try
def on_set_register(tir_ins, compiled_code, *)
reg = tir_ins.register.id
src_reg = tir_ins.source_register.id
compiled_code.instruct(:SetRegister, [reg, src_reg], tir_ins.location)
end
end
end
......
......@@ -55,7 +55,7 @@ module Inkoc
@module.globals.define(Config::MODULE_GLOBAL, @module.type)
scope = Scope.new(@module.type, @module.body.type, locals)
scope = TypeScope.new(@module.type, @module.body.type, locals)
define_type(ast, scope)
end
......@@ -144,11 +144,11 @@ module Inkoc
name = node.name
loc = node.location
type =
rtype, block_type =
if (local_type = scope.type_of_local(name))
local_type
elsif scope.self_type.responds_to_message?(name)
send_object_message(self_type, name, [], scope, loc)
send_object_message(scope.self_type, name, [], scope, loc)
elsif @module.responds_to_message?(name)
send_object_message(@module.type, name, [], scope, loc)
elsif (global_type = @module.type_of_global(name))
......@@ -158,7 +158,9 @@ module Inkoc
Type::Dynamic.new
end
type.resolve_type(scope.self_type)
node.block_type = block_type if block_type
rtype.resolve_type(scope.self_type)
end
def on_global(node, *)
......@@ -175,13 +177,15 @@ module Inkoc
end
def on_send(node, scope)
send_object_message(
rtype, node.block_type = send_object_message(
receiver_type(node, scope),
node.name,
node.arguments,
scope,
node.location
)
rtype
end
def on_keyword_argument(node, scope)
......@@ -204,8 +208,10 @@ module Inkoc
verify_send_arguments(receiver, method_type, args, location)
method_type
rtype = method_type
.initialized_return_type(receiver, [scope.self_type, *arg_types])
[rtype, method_type]
end
def verify_send_arguments(receiver_type, type, arguments, location)
......@@ -341,42 +347,55 @@ module Inkoc
def on_throw(node, scope)
throw_type = define_type(node.value, scope)
infer_and_validate_throw_type(throw_type, scope, node.location)
# For block types we infer the throw type so one doesn't have to
# annotate every block with an explicit type.
scope.block_type.throws ||= throw_type if scope.closure?
throw_type
typedb.void_type
end
def infer_and_validate_throw_type(throw_type, scope, location)
block_throws = scope.block_type.throws
scope.block_type.contains_throw = true
def on_try(node, scope)
node.try_block_type =
block_type_with_self(Config::TRY_BLOCK_NAME, scope.self_type)
if scope.block_type == @module.body.type
diagnostics.throw_at_top_level_error(throw_type, location)
return
end
node.else_block_type =
block_type_with_self(Config::ELSE_BLOCK_NAME, scope.self_type)
# For block types we infer the throw type so one doesn't have
# to annotate every block with an explicit type.
if block_throws
unless block_throws.type_compatible?(throw_type)
diagnostics.type_error(block_throws, throw_type, location)
end
elsif scope.closure?
scope.block_type.throws ||= throw_type
elsif scope.method?
diagnostics.throw_without_throw_defined_error(throw_type, location)
try_scope =
TypeScope.new(scope.self_type, node.try_block_type, scope.locals)
try_type =
node.try_block_type.returns =
define_type(node.expression, try_scope)
else_scope = node.type_scope_for_else(scope.self_type)
node.define_else_argument_type
else_type = else_type_for_try(node, else_scope)
if try_type.physical_type? &&
else_type.physical_type? &&
!else_type.type_compatible?(try_type)
diagnostics.type_error(try_type, else_type, node.else_body.location)
end
end
def on_try(node, scope)
exp_type = define_type(node.expression, scope)
else_type = define_type(node.else_body, scope) if node.else_body
try_type.if_physical_or_else { else_type }
end
if else_type && !else_type.type_compatible?(exp_type)
diagnostics.type_error(exp_type, else_type, node.else_body.location)
def else_type_for_try(node, scope)
if node.else_body.empty?
node.else_body.type = Type::Void.new
else
define_type(node.else_body, scope)
end
end
exp_type
def block_type_with_self(name, self_type)
type = Type::Block.new(name, typedb.block_prototype)
type.define_self_argument(self_type)
type
end
def on_object(node, scope)
......@@ -396,7 +415,7 @@ module Inkoc
)
block_type = define_block_type_for_object(node, type)
new_scope = Scope.new(type, block_type, node.body.locals)
new_scope = TypeScope.new(type, block_type, node.body.locals)
define_type_parameters(node.type_parameters, type)
store_type(type, scope.self_type, node.location)
......@@ -423,16 +442,14 @@ module Inkoc
define_type_parameters(node.type_parameters, type)
node.required_traits.each do |trait|
trait_type =
resolve_type(trait, scope.self_type, [scope.self_type, @module])
trait_type = resolve_module_type(trait, scope.self_type)
type.required_traits << trait_type if trait_type.trait?
end
block_type = define_block_type_for_object(node, type)
new_scope = Scope.new(type, block_type, node.body.locals)
new_scope = TypeScope.new(type, block_type, node.body.locals)
store_type(type, ssope.self_type, node.location)
store_type(type, scope.self_type, node.location)
define_type(node.body, new_scope)
type
......@@ -442,11 +459,13 @@ module Inkoc
self_type = scope.self_type
loc = node.location
trait = resolve_type(node.trait_name, self_type, [self_type, @module])
object = resolve_type(node.object_name, self_type, [self_type, @module])
trait = resolve_module_type(node.trait_name, self_type)
object = resolve_module_type(node.object_name, self_type)
define_type(node.body, object, node.body.locals)
define_block_type_for_object(node, object)
block_type = define_block_type_for_object(node, object)
new_scope = TypeScope.new(object, block_type, node.body.locals)
define_type(node.body, new_scope)
traits_implemented = required_traits_implemented?(object, trait, loc)
methods_implemented = required_methods_implemented?(object, trait, loc)
......@@ -487,7 +506,7 @@ module Inkoc
type = Type::Block
.new(node.name, typedb.block_prototype, block_type: :method)
new_scope = Scope.new(self_type, type, node.body.locals)
new_scope = TypeScope.new(self_type, type, node.body.locals)
block_signature(node, type, scope)
......@@ -512,10 +531,6 @@ module Inkoc
define_type(body, method.scope)
if node.type.missing_throw?
diagnostics.missing_throw_error(node.type.throws, node.location)
end
expected_type = node.type
.return_type
.resolve_type(method.scope.self_type)
......@@ -530,13 +545,13 @@ module Inkoc
def on_block(node, scope)
type = Type::Block.new(Config::BLOCK_NAME, typedb.block_prototype)
new_scope = Scope.new(scope.self_type, type, node.body.locals)
new_scope = TypeScope.new(scope.self_type, type, node.body.locals)
block_signature(node, type, new_scope)
define_type(node.body, new_scope)
rtype = node.body.type
exp = type.resolve_type(scope.self_type)
exp = type.return_type.resolve_type(scope.self_type)
type.returns = rtype if type.returns.dynamic?
......@@ -551,6 +566,19 @@ module Inkoc
callback = node.variable.define_variable_visitor_method
vtype = define_type(node.value, scope)
if node.value_type
exp_type = resolve_module_type(node.value_type, scope.self_type)
# If an explicit type is given and the inferred type is compatible we
# want to use the _explicit type_ as _the_ type, instead of the