Support circular types in the Ruby compiler

This adds very basic (and super hacky) support for circular types to the
Ruby compiler, which should be _just_ enough to allow us to continue
work on the self-hosting compiler. Circular types can arise as follows:

    object TypeParameter {
      @required_traits: Array!(TraitType)
    }

    object TraitType {
      @type_parameters: Array!(TypeParameter)
    }

Without support for circular types code like this won't compile.

The changes needed to make this work are a total hack, and in some cases
will lead to some duplicate work being done (with the results being
discarded). Since this code will only stick around briefly, this is fine
for now.
parent def89e1f
Pipeline #106690901 passed with stages
in 25 minutes and 53 seconds
......@@ -74,11 +74,13 @@ require 'inkoc/pass/track_module'
require 'inkoc/pass/add_implicit_import_symbols'
require 'inkoc/pass/compile_imported_modules'
require 'inkoc/pass/desugar_object'
require 'inkoc/pass/define_this_module_type'
require 'inkoc/pass/refine_module_type'
require 'inkoc/pass/define_import_types'
require 'inkoc/pass/define_this_module_type'
require 'inkoc/pass/define_type'
require 'inkoc/pass/define_type_signatures'
require 'inkoc/pass/refine_module_type'
require 'inkoc/pass/validate_throw'
require 'inkoc/pass/implement_traits'
require 'inkoc/pass/optimize_keyword_arguments'
require 'inkoc/pass/generate_tir'
require 'inkoc/pass/define_module_type'
......
......@@ -107,6 +107,10 @@ module Inkoc
false
end
def trait?
false
end
def reassign_attribute?
false
end
......@@ -114,6 +118,10 @@ module Inkoc
def attribute?
false
end
def trait_implementation?
false
end
end
end
end
......@@ -31,6 +31,10 @@ module Inkoc
def visitor_method
:on_trait
end
def trait?
true
end
end
end
end
......@@ -26,6 +26,10 @@ module Inkoc
def visitor_method
:on_trait_implementation
end
def trait_implementation?
true
end
end
end
end
......@@ -17,6 +17,8 @@ module Inkoc
Pass::RefineModuleType,
Pass::DefineThisModuleType,
Pass::DefineImportTypes,
Pass::DefineTypeSignatures,
Pass::ImplementTraits,
Pass::DefineType,
Pass::ValidateThrow,
Pass::OptimizeKeywordArguments,
......
......@@ -12,7 +12,6 @@ module Inkoc
def initialize(mod, state)
super
@constant_resolver = ConstantResolver.new(diagnostics)
@deferred_methods = []
end
......@@ -22,17 +21,6 @@ module Inkoc
end
end
def define_type_instance(node, scope, *extra)
type = define_type(node, scope, *extra)
unless type.type_instance?
type = type.new_instance
node.type = type
end
type
end
def on_module_body(node, scope)
type = define_type(node, scope)
......@@ -53,34 +41,6 @@ module Inkoc
typedb.string_type.new_instance
end
def on_constant(node, scope)
@constant_resolver.resolve(node, scope)
end
def on_type_name_reference(node, scope)
type = define_type(node.name, scope)
return type if type.error?
if same_type_parameters?(node, type)
wrap_optional_type(node, type)
else
TypeSystem::Error.new
end
end
def same_type_parameters?(node, type)
node_names = node.type_parameters.map(&:type_name)
type_names = type.type_parameters.map(&:name)
if node_names == type_names
true
else
diagnostics.invalid_type_parameters(type, node_names, node.location)
false
end
end
def on_block_type(node, scope)
proto = @state.typedb.block_type
type =
......@@ -111,72 +71,6 @@ module Inkoc
end
alias on_lambda_type on_block_type
def on_self_type_with_late_binding(node, _)
wrap_optional_type(node, TypeSystem::SelfType.new)
end
def on_self_type(node, scope)
self_type = scope.self_type
# When "Self" translates to a generic type, e.g. Array!(T), we want to
# return a type in the form of `Array!(T -> T)`, and not just `Array`.
# This ensures that any arguments passed to a method returning "Self"
# can properly initialise the type.
type_arguments =
self_type.generic_type? ? self_type.type_parameters.to_a : []
wrap_optional_type(node, self_type.new_instance(type_arguments))
end
def on_dynamic_type(node, _)
wrap_optional_type(node, TypeSystem::Dynamic.new)
end
def on_never_type(node, _)
wrap_optional_type(node, TypeSystem::Never.new)
end
def on_type_name(node, scope)
type = define_type(node.name, scope)
return type if type.error?
return wrap_optional_type(node, type) unless type.generic_type?
# When our type is a generic type we need to initialise it according to
# the passed type parameters.
type_arguments = node
.type_parameters
.zip(type.type_parameters)
.map do |param_node, param|
param_instance = define_type_instance(param_node, scope)
if param && !param_instance.type_compatible?(param, @state)
return diagnostics
.type_error(param, param_instance, param_node.location)
end
param_instance
end
num_given = type_arguments.length
num_expected = type.type_parameters.length
if num_given != num_expected
return diagnostics.type_parameter_count_error(
num_given,
num_expected,
node.location
)
end
# Simply referencing a constant should not lead to it being initialised,
# unless there are any type parameters to initialise.
wrap_optional_type(
node,
type.new_instance_for_reference(type_arguments)
)
end
def on_attribute(node, scope)
name = node.name
symbol = scope.self_type.lookup_attribute(name)
......@@ -599,35 +493,20 @@ module Inkoc
end
def on_object(node, scope)
type = typedb.new_object_type(node.name)
body_scope = scope_for_object_body(node)
define_object_name_attribute(type)
define_named_type(node, type, scope)
define_type(node.body, body_scope)
end
def on_trait(node, scope)
if (existing = scope.lookup_type(node.name))
extend_trait(existing, node, scope)
else
type = typedb.new_trait_type(node.name)
return extend_trait(node.type, node, scope) if node.redefines
define_object_name_attribute(type)
define_required_traits(node, type, scope)
define_named_type(node, type, scope)
end
body_scope = scope_for_object_body(node)
define_type(node.body, body_scope)
end
def extend_trait(trait, node, scope)
unless trait.empty?
return diagnostics.extend_trait_error(trait, node.location)
end
return TypeSystem::Error.new unless same_type_parameters?(node, trait)
node.redefines = true
define_required_traits(node, trait, scope)
body_type = TypeSystem::Block.closure(typedb.block_type)
body_scope = TypeScope
......@@ -642,38 +521,6 @@ module Inkoc
trait
end
def define_object_name_attribute(type)
type.define_attribute(
Config::OBJECT_NAME_INSTANCE_ATTRIBUTE,
typedb.string_type.new_instance
)
end
def define_required_traits(node, trait, scope)
node.required_traits.each do |req_node|
req = define_type_instance(req_node, scope)
trait.add_required_trait(req) unless req.error?
end
end
def define_named_type(node, new_type, scope)
body_type = TypeSystem::Block.closure(typedb.block_type)
body_scope = TypeScope
.new(new_type, body_type, @module, locals: node.body.locals)
body_scope.define_receiver_type
node.block_type = body_type
define_types(node.type_parameters, body_scope)
store_type(new_type, scope, node.location)
define_type(node.body, body_scope)
new_type
end
def on_reopen_object(node, scope)
type = on_type_name_reference(node.name, scope)
......@@ -719,10 +566,6 @@ module Inkoc
return trait if trait.error?
object.implement_trait(trait)
node.block_type = impl_block
define_type(node.body, impl_scope)
if trait_requirements_met?(object, trait, node.location)
......@@ -827,20 +670,6 @@ module Inkoc
end
end
def store_type(type, scope, location)
scope.self_type.define_attribute(type.name, type)
store_type_as_global(type.name, type, scope, location)
end
def store_type_as_global(name, type, scope, location)
if Config::RESERVED_CONSTANTS.include?(name)
diagnostics.redefine_reserved_constant_error(name, location)
elsif scope.module_scope?
@module.globals.define(name, type)
end
end
def on_block(node, scope, expected_block = nil)
block_type = TypeSystem::Block.closure(typedb.block_type)
locals = node.body.locals
......@@ -1917,14 +1746,6 @@ module Inkoc
end
# rubocop: enable Metrics/PerceivedComplexity
# rubocop: enable Metrics/CyclomaticComplexity
def wrap_optional_type(node, type)
if node.optional?
TypeSystem::Optional.wrap(type)
else
type
end
end
end
# rubocop: enable Metrics/ClassLength
end
......
# frozen_string_literal: true
module Inkoc
module Pass
# Compiler pass that defines the signatures of objects and traits.
class DefineTypeSignatures
include VisitorMethods
include TypePass
def on_body(node, scope)
node.expressions.each do |expr|
define_type(expr, scope) if expr.trait? || expr.object?
end
nil
end
def on_object(node, scope)
type = typedb.new_object_type(node.name)
define_object_name_attribute(type)
define_named_type(node, type, scope)
end
def on_trait(node, scope)
if (existing = scope.lookup_type(node.name))
return extend_trait(existing, node, scope)
end
type = typedb.new_trait_type(node.name)
define_object_name_attribute(type)
define_required_traits(node, type, scope)
define_named_type(node, type, scope)
end
def extend_trait(trait, node, scope)
unless trait.empty?
return diagnostics.extend_trait_error(trait, node.location)
end
return TypeSystem::Error.new unless same_type_parameters?(node, trait)
node.redefines = true
define_required_traits(node, trait, scope)
trait
end
def on_define_type_parameter(node, scope)
traits = define_types(node.required_traits, scope)
scope.self_type.define_type_parameter(node.name, traits)
end
def define_object_name_attribute(type)
type.define_attribute(
Config::OBJECT_NAME_INSTANCE_ATTRIBUTE,
typedb.string_type.new_instance
)
end
def define_named_type(node, new_type, scope)
body_type = TypeSystem::Block.closure(typedb.block_type)
body_scope = TypeScope
.new(new_type, body_type, @module, locals: node.body.locals)
body_scope.define_receiver_type
node.block_type = body_type
define_types(node.type_parameters, body_scope)
store_type(new_type, scope, node.location)
new_type
end
end
end
end
# frozen_string_literal: true
module Inkoc
module Pass
# A compiler pass that simply marks a trait as implement. Trait
# implementations are verified in a separate pass.
class ImplementTraits
include VisitorMethods
include TypePass
def on_body(node, scope)
node.expressions.each do |expr|
define_type(expr, scope) if expr.trait_implementation?
end
nil
end
def on_trait_implementation(node, scope)
object = on_type_name_reference(node.object_name, scope)
return object if object.error?
# The trait name has to be looked up in the context of the
# implementation. This ensures that a Self type refers to the type
# that the trait is implemented for, instead of referring to the type of
# the outer scope.
impl_block = TypeSystem::Block.closure(typedb.block_type)
impl_scope = TypeScope
.new(object, impl_block, @module, locals: node.body.locals)
impl_scope.define_receiver_type
trait = define_type(node.trait_name, impl_scope)
return trait if trait.error?
object.implement_trait(trait)
node.block_type = impl_block
nil
end
end
end
end
......@@ -5,6 +5,7 @@ module Inkoc
def initialize(mod, state)
@module = mod
@state = state
@constant_resolver = ConstantResolver.new(diagnostics)
end
def diagnostics
......@@ -25,8 +26,108 @@ module Inkoc
[ast]
end
def on_module_body(_node, _scope)
raise NotImplementedError
def on_module_body(node, scope)
define_type(node, scope)
nil
end
def on_body(node, scope)
define_types(node.expressions, scope)
nil
end
def on_constant(node, scope)
@constant_resolver.resolve(node, scope)
end
def on_dynamic_type(node, _)
wrap_optional_type(node, TypeSystem::Dynamic.new)
end
def on_never_type(node, _)
wrap_optional_type(node, TypeSystem::Never.new)
end
def on_self_type_with_late_binding(node, _)
wrap_optional_type(node, TypeSystem::SelfType.new)
end
def on_self_type(node, scope)
self_type = scope.self_type
# When "Self" translates to a generic type, e.g. Array!(T), we want to
# return a type in the form of `Array!(T -> T)`, and not just `Array`.
# This ensures that any arguments passed to a method returning "Self" can
# properly initialise the type.
type_arguments =
self_type.generic_type? ? self_type.type_parameters.to_a : []
wrap_optional_type(node, self_type.new_instance(type_arguments))
end
def on_type_name(node, scope)
type = define_type(node.name, scope)
return type if type.error?
return wrap_optional_type(node, type) unless type.generic_type?
# When our type is a generic type we need to initialise it according to
# the passed type parameters.
type_arguments = node
.type_parameters
.zip(type.type_parameters)
.map do |param_node, param|
param_instance = define_type_instance(param_node, scope)
if param && !param_instance.type_compatible?(param, @state)
return diagnostics
.type_error(param, param_instance, param_node.location)
end
param_instance
end
num_given = type_arguments.length
num_expected = type.type_parameters.length
if num_given != num_expected
return diagnostics.type_parameter_count_error(
num_given,
num_expected,
node.location
)
end
# Simply referencing a constant should not lead to it being initialised,
# unless there are any type parameters to initialise.
wrap_optional_type(
node,
type.new_instance_for_reference(type_arguments)
)
end
def on_type_name_reference(node, scope)
type = define_type(node.name, scope)
return type if type.error?
if same_type_parameters?(node, type)
wrap_optional_type(node, type)
else
TypeSystem::Error.new
end
end
def same_type_parameters?(node, type)
node_names = node.type_parameters.map(&:type_name)
type_names = type.type_parameters.map(&:name)
if node_names == type_names
true
else
diagnostics.invalid_type_parameters(type, node_names, node.location)
false
end
end
def define_type(node, scope, *extra)
......@@ -38,5 +139,53 @@ module Inkoc
def define_types(nodes, scope, *extra)
nodes.map { |n| define_type(n, scope, *extra) }
end
def define_type_instance(node, scope, *extra)
type = define_type(node, scope, *extra)
if type && !type.type_instance?
type = type.new_instance
node.type = type
end
type
end
def wrap_optional_type(node, type)
if node.optional?
TypeSystem::Optional.wrap(type)
else
type
end
end
def store_type(type, scope, location)
scope.self_type.define_attribute(type.name, type)