aboutsummaryrefslogtreecommitdiff
path: root/boot/type_check.cc
diff options
context:
space:
mode:
Diffstat (limited to 'boot/type_check.cc')
-rw-r--r--boot/type_check.cc476
1 files changed, 445 insertions, 31 deletions
diff --git a/boot/type_check.cc b/boot/type_check.cc
index cdc78df..ab22a1c 100644
--- a/boot/type_check.cc
+++ b/boot/type_check.cc
@@ -114,6 +114,51 @@ namespace elna::boot
}, this->payload);
}
+ generic_error::generic_error(const source_position position, const std::string& generic_name,
+ payload_type payload)
+ : diagnostic(position), generic_name(generic_name), payload(std::move(payload))
+ {
+ }
+
+ std::string generic_error::what() const
+ {
+ return std::visit([this](auto&& payload) -> std::string
+ {
+ using T = std::decay_t<decltype(payload)>;
+
+ if constexpr (std::is_same_v<T, not_generic>)
+ {
+ return "'" + this->generic_name + "' is not generic and cannot take type arguments";
+ }
+ else if constexpr (std::is_same_v<T, unused_parameter>)
+ {
+ return "Type parameter '" + payload.parameter + "' of '"
+ + this->generic_name + "' is not used";
+ }
+ else if constexpr (std::is_same_v<T, argument_not_pointer>)
+ {
+ return "Type '" + payload.actual.to_string() + "' cannot be a type argument of '"
+ + this->generic_name + "', it is not a pointer type";
+ }
+ else if constexpr (std::is_same_v<T, constant_argument>)
+ {
+ return "Constant type '" + payload.actual.to_string()
+ + "' cannot be a type argument of '" + this->generic_name + "'";
+ }
+ else if constexpr (std::is_same_v<T, uninferrable_parameter>)
+ {
+ return "Type parameter '" + payload.parameter + "' of '" + this->generic_name
+ + "' cannot be inferred, give the type arguments explicitly";
+ }
+ else if constexpr (std::is_same_v<T, shape_mismatch>)
+ {
+ return "Cannot infer the type arguments of '" + this->generic_name
+ + "': parameter type '" + payload.declared.to_string()
+ + "' does not match argument type '" + payload.actual.to_string() + "'";
+ }
+ }, this->payload);
+ }
+
/// The program body is the only callable without a name.
static std::string describe_callable(const std::string& identifier)
{
@@ -228,6 +273,12 @@ namespace elna::boot
case opaque_arithmetic:
return "Opaque type '" + this->actual.to_string()
+ "' cannot be used as an element type in pointer arithmetic";
+ case dereference_of_parameter:
+ return "Type parameter '" + this->actual.to_string()
+ + "' cannot be dereferenced, its pointee is unknown";
+ case parameter_arithmetic:
+ return "Type parameter '" + this->actual.to_string()
+ + "' cannot be used in pointer arithmetic, its element size is unknown";
case zero_sized:
return "Zero-sized type '" + this->actual.to_string()
+ "' cannot be declared";
@@ -261,8 +312,20 @@ namespace elna::boot
case array:
entity = "array initializer '" + this->applicand + "'";
break;
+ case generic:
+ entity = "generic '" + this->applicand + "'";
+ break;
+ }
+ std::string noun = "arguments";
+
+ if (this->m_kind == kind::array)
+ {
+ noun = "elements";
+ }
+ else if (this->m_kind == kind::generic)
+ {
+ noun = "type arguments";
}
- const std::string noun = this->m_kind == kind::array ? "elements" : "arguments";
const std::string quantifier = actual > expected ? "many" : "few";
return "Too " + quantifier + " " + noun + " for " + entity
@@ -348,17 +411,25 @@ namespace elna::boot
}
/*
- * Scaling an offset needs the pointee size, which an opaque type doesn't
- * have. Only a pointer_type scales; the generic Pointer and procedure
- * values advance byte-wise.
+ * Scaling an offset needs the pointee size, which neither an opaque type
+ * nor a type parameter has. Only a pointer_type scales; the generic
+ * Pointer and procedure values advance byte-wise.
*/
- static std::optional<type> find_opaque_arithmetic(binary_operator operation,
+ static std::optional<type> find_arithmetic_violation(binary_operator operation,
const type& lhs, const type& rhs)
{
if (operation != binary_operator::sum && operation != binary_operator::subtraction)
{
return std::nullopt;
}
+ if (lhs.get<parameter_type>() != nullptr)
+ {
+ return lhs;
+ }
+ if (rhs.get<parameter_type>() != nullptr)
+ {
+ return rhs;
+ }
const bool lhs_integral = is_integral_type(lhs);
if (lhs_integral == is_integral_type(rhs))
@@ -444,19 +515,6 @@ namespace elna::boot
*
* Base record type must not be null. Derived record type may be null.
*/
- static bool is_base_of(const std::shared_ptr<record_type>& base,
- const std::shared_ptr<record_type>& derived)
- {
- if (derived != nullptr)
- {
- if (auto current_record = resolve_underlying_type(derived->base).get<record_type>())
- {
- return current_record == base || is_base_of(base, current_record);
- }
- }
- return false;
- }
-
assign_check::verdict assign_check::guard_const_laundering() const
{
if (!is_primitive_type(ctx.aliased_assignee, "Pointer"))
@@ -519,10 +577,11 @@ namespace elna::boot
return verdict::reject;
}
// A pointer to a record can be assigned to a pointer to its base type.
- if (auto assignee_record = resolve_underlying_type(assignee_ptr->base).get<record_type>())
+ // An instantiation is terminal in resolution, so the erased view is
+ // what decides whether a record is in play at all.
+ if (erase_generic(assignee_ptr->base).get<record_type>() != nullptr)
{
- return is_base_of(assignee_record,
- resolve_underlying_type(assignment_ptr->base).get<record_type>())
+ return is_base_of(assignee_ptr->base, assignment_ptr->base)
? verdict::accept : verdict::pass;
}
return verdict::pass;
@@ -631,6 +690,10 @@ namespace elna::boot
}
walking_visitor::visit(declaration);
+ check_parameters_used(declaration->position(), declaration->identifier.name(),
+ this->current_procedure->parameters,
+ type(std::make_shared<procedure_type>(this->current_procedure->symbol)));
+
std::size_t parameter_index = 0;
for (const type& parameter : this->current_procedure->symbol.parameters)
@@ -870,6 +933,13 @@ namespace elna::boot
{
walking_visitor::visit(declaration);
auto unresolved_type = this->bag.lookup(declaration->identifier.name())->is_type()->symbol.get<alias_type>();
+
+ if (auto generic = unresolved_type->referent.get<generic_type>())
+ {
+ check_parameters_used(declaration->position(), declaration->identifier.name(),
+ generic->parameters, generic->referent);
+ return;
+ }
const type referent = resolve_aliases(unresolved_type->referent);
if (auto record = referent.get<record_type>())
@@ -948,7 +1018,7 @@ namespace elna::boot
}
else
{
- type const base_type = resolve_underlying_type(base_symbol->is_type()->symbol);
+ type const base_type = erase_generic(base_symbol->is_type()->symbol);
if (base_type.get<record_type>() == nullptr)
{
add_error<type_requirement_error>(expression->position(),
@@ -959,11 +1029,343 @@ namespace elna::boot
walking_visitor::visit(expression);
}
+ /*
+ * Aliases are not entered: a parameter is only in scope in its own
+ * declaration, so another declaration can never mention it. Instantiations
+ * contribute their arguments and not their generic, which is what makes
+ * the walk terminate on a recursive generic.
+ */
+ static bool occurs_in(const type& parameter, const type& subject)
+ {
+ if (subject.get<parameter_type>() == parameter.get<parameter_type>())
+ {
+ return true;
+ }
+ if (auto pointer = subject.get<pointer_type>())
+ {
+ return occurs_in(parameter, pointer->base);
+ }
+ if (auto slice = subject.get<slice_type>())
+ {
+ return occurs_in(parameter, slice->base);
+ }
+ if (auto array = subject.get<array_type>())
+ {
+ return occurs_in(parameter, array->base);
+ }
+ if (auto qualified = subject.get<constant_type>())
+ {
+ return occurs_in(parameter, qualified->unqualified);
+ }
+ if (auto record = subject.get<record_type>())
+ {
+ for (const auto& [field_name, field_type] : record->fields)
+ {
+ if (occurs_in(parameter, field_type))
+ {
+ return true;
+ }
+ }
+ return !record->base.empty() && occurs_in(parameter, record->base);
+ }
+ if (auto procedure = subject.get<procedure_type>())
+ {
+ for (const type& parameter_type : procedure->parameters)
+ {
+ if (occurs_in(parameter, parameter_type))
+ {
+ return true;
+ }
+ }
+ return !procedure->return_type.proper_type.empty()
+ && occurs_in(parameter, procedure->return_type.proper_type);
+ }
+ if (auto instance = subject.get<instantiated_type>())
+ {
+ for (const type& argument : instance->arguments)
+ {
+ if (occurs_in(parameter, argument))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ void type_analysis_visitor::check_parameters_used(const source_position position,
+ const std::string& name, const std::vector<type>& parameters, const type& heading)
+ {
+ for (const type& parameter : parameters)
+ {
+ if (!occurs_in(parameter, heading))
+ {
+ add_error<generic_error>(position, name,
+ generic_error::unused_parameter{ parameter.to_string() });
+ }
+ }
+ }
+
+ /*
+ * Walks a declared parameter type and an actual argument type in parallel,
+ * binding a parameter the first time it is met and leaving every later
+ * occurrence to the ordinary assignability check. Both sides are resolved
+ * at every step, so a transparent instantiation is seen through and only a
+ * record-referent one is compared as an instantiation.
+ *
+ * Returns false when the walk fails on shape rather than on type.
+ */
+ static bool bind_arguments(const std::vector<type>& parameters, std::vector<type>& bindings,
+ const type& declared, const type& actual)
+ {
+ const type left = resolve_aliases(declared);
+ const type right = resolve_aliases(actual);
+
+ if (auto parameter = left.get<parameter_type>())
+ {
+ for (std::size_t i = 0; i < parameters.size(); ++i)
+ {
+ if (parameters[i].get<parameter_type>() == parameter && bindings[i].empty())
+ {
+ // The actual type is recorded as it stands, which inside a
+ // generic body may itself be an abstract parameter.
+ bindings[i] = actual;
+ }
+ }
+ return true;
+ }
+ if (auto left_pointer = left.get<pointer_type>())
+ {
+ auto right_pointer = right.get<pointer_type>();
+
+ return right_pointer != nullptr
+ && bind_arguments(parameters, bindings, left_pointer->base, right_pointer->base);
+ }
+ if (auto left_slice = left.get<slice_type>())
+ {
+ auto right_slice = right.get<slice_type>();
+
+ return right_slice != nullptr
+ && bind_arguments(parameters, bindings, left_slice->base, right_slice->base);
+ }
+ if (auto left_array = left.get<array_type>())
+ {
+ auto right_array = right.get<array_type>();
+
+ return right_array != nullptr
+ && bind_arguments(parameters, bindings, left_array->base, right_array->base);
+ }
+ if (auto left_constant = left.get<constant_type>())
+ {
+ auto right_constant = right.get<constant_type>();
+
+ return right_constant != nullptr
+ && bind_arguments(parameters, bindings,
+ left_constant->unqualified, right_constant->unqualified);
+ }
+ if (auto left_instance = left.get<instantiated_type>())
+ {
+ for (type candidate = right; !candidate.empty(); candidate = base_of(candidate))
+ {
+ auto right_instance = resolve_aliases(candidate).get<instantiated_type>();
+
+ if (right_instance == nullptr
+ || !(left_instance->generic == right_instance->generic)
+ || left_instance->arguments.size() != right_instance->arguments.size())
+ {
+ continue;
+ }
+ for (std::size_t i = 0; i < left_instance->arguments.size(); ++i)
+ {
+ if (!bind_arguments(parameters, bindings,
+ left_instance->arguments.at(i), right_instance->arguments.at(i)))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ if (auto left_procedure = left.get<procedure_type>())
+ {
+ auto right_procedure = right.get<procedure_type>();
+
+ if (right_procedure == nullptr
+ || left_procedure->parameters.size() != right_procedure->parameters.size())
+ {
+ return false;
+ }
+ for (std::size_t i = 0; i < left_procedure->parameters.size(); ++i)
+ {
+ if (!bind_arguments(parameters, bindings,
+ left_procedure->parameters.at(i), right_procedure->parameters.at(i)))
+ {
+ return false;
+ }
+ }
+ return left_procedure->return_type.proper_type.empty()
+ || bind_arguments(parameters, bindings,
+ left_procedure->return_type.proper_type,
+ right_procedure->return_type.proper_type);
+ }
+ return true;
+ }
+
+ void type_analysis_visitor::check_arguments(const named_expression& reference,
+ const std::vector<type>& arguments)
+ {
+ for (const type& argument : arguments)
+ {
+ const type resolved = resolve_aliases(argument);
+
+ if (resolved.get<constant_type>() != nullptr)
+ {
+ add_error<generic_error>(reference.position(), reference.name,
+ generic_error::constant_argument{ argument });
+ }
+ else if (!argument.empty() && !is_any_pointer_type(resolve_underlying_type(argument)))
+ {
+ add_error<generic_error>(reference.position(), reference.name,
+ generic_error::argument_not_pointer{ argument });
+ }
+ }
+ }
+
+ void type_analysis_visitor::visit(named_expression *expression)
+ {
+ walking_visitor::visit(expression);
+
+ // A generic procedure keeps its parameters on the symbol rather than in
+ // its type, which erasure has already substituted away.
+ std::shared_ptr<info> const symbol = this->bag.lookup(expression->name);
+ std::shared_ptr<procedure_info> const procedure =
+ symbol == nullptr ? nullptr : symbol->is_procedure();
+
+ if (procedure != nullptr && !procedure->parameters.empty())
+ {
+ // An omitted list is inferred at the call, and only there: in value
+ // position there is nothing to infer from.
+ const bool omitted = expression->arguments.empty() && this->in_callable_position;
+
+ if (!omitted && expression->arguments.size() != procedure->parameters.size())
+ {
+ add_error<argument_count_error>(expression->position(),
+ argument_count_error::kind::generic, expression->name,
+ procedure->parameters.size(), expression->arguments.size());
+ }
+ else if (!expression->arguments.empty())
+ {
+ check_arguments(*expression, expression->argument_types);
+ }
+ return;
+ }
+ const type denoted = expression->type_decoration;
+
+ if (auto instance = denoted.get<instantiated_type>())
+ {
+ std::shared_ptr<generic_type> const generic = unwrap_generic(instance->generic);
+
+ if (generic == nullptr)
+ {
+ add_error<generic_error>(expression->position(), expression->name,
+ generic_error::not_generic{});
+ }
+ else if (instance->arguments.size() != generic->parameters.size())
+ {
+ add_error<argument_count_error>(expression->position(),
+ argument_count_error::kind::generic, expression->name,
+ generic->parameters.size(), instance->arguments.size());
+ }
+ else
+ {
+ check_arguments(*expression, expression->argument_types);
+ }
+ }
+ else if (std::shared_ptr<generic_type> const generic = unwrap_generic(denoted))
+ {
+ // A bare generic is not a type at all, which is simply zero
+ // arguments where some were expected.
+ add_error<argument_count_error>(expression->position(),
+ argument_count_error::kind::generic, expression->name,
+ generic->parameters.size(), 0);
+ }
+ else if (procedure != nullptr && !expression->arguments.empty())
+ {
+ add_error<generic_error>(expression->position(), expression->name,
+ generic_error::not_generic{});
+ }
+ }
+
+ type type_analysis_visitor::infer_call(procedure_call& call, const named_expression& reference,
+ const procedure_info& procedure)
+ {
+ std::vector<type> bindings(procedure.parameters.size());
+ auto argument_iterator = std::cbegin(call.arguments);
+ auto declared_iterator = std::cbegin(procedure.symbol.parameters);
+
+ while (argument_iterator != std::cend(call.arguments)
+ && declared_iterator != std::cend(procedure.symbol.parameters))
+ {
+ if (!bind_arguments(procedure.parameters, bindings,
+ *declared_iterator, (*argument_iterator)->type_decoration))
+ {
+ add_error<generic_error>((*argument_iterator)->position(), reference.name,
+ generic_error::shape_mismatch{
+ .declared = *declared_iterator,
+ .actual = (*argument_iterator)->type_decoration
+ });
+ }
+ ++argument_iterator;
+ ++declared_iterator;
+ }
+ for (std::size_t i = 0; i < bindings.size(); ++i)
+ {
+ if (bindings.at(i).empty())
+ {
+ add_error<generic_error>(call.position(), reference.name,
+ generic_error::uninferrable_parameter{
+ procedure.parameters.at(i).to_string()
+ });
+ return type();
+ }
+ }
+ check_arguments(reference, bindings);
+
+ return substitute(type(std::make_shared<procedure_type>(procedure.symbol)),
+ procedure.parameters, bindings);
+ }
+
void type_analysis_visitor::visit(procedure_call *call)
{
+ const named_expression *reference = call->callable().is_named();
+
+ this->in_callable_position = reference != nullptr;
call->callable().accept(this);
+ this->in_callable_position = false;
+
+ type callable_type = call->callable().type_decoration;
+ std::shared_ptr<procedure_info> inferred;
+
+ if (reference != nullptr && reference->arguments.empty())
+ {
+ std::shared_ptr<info> const symbol = this->bag.lookup(reference->name);
+ std::shared_ptr<procedure_info> const procedure =
+ symbol == nullptr ? nullptr : symbol->is_procedure();
- if (auto procedure = call->callable().type_decoration.get<procedure_type>())
+ if (procedure != nullptr && !procedure->parameters.empty())
+ {
+ // Inference reads the argument types, so they are decorated
+ // before the parameter types they will be checked against exist.
+ for (expression *argument : call->arguments)
+ {
+ argument->accept(this);
+ }
+ inferred = procedure;
+ callable_type = infer_call(*call, *reference, *procedure);
+ }
+ }
+ if (auto procedure = callable_type.get<procedure_type>())
{
std::vector<expression *>::const_iterator argument_iterator = std::cbegin(call->arguments);
std::vector<type>::const_iterator type_iterator = std::cbegin(procedure->parameters);
@@ -971,7 +1373,10 @@ namespace elna::boot
while (argument_iterator != std::cend(call->arguments)
&& type_iterator != std::cend(procedure->parameters))
{
- (*argument_iterator)->accept(this);
+ if (inferred == nullptr)
+ {
+ (*argument_iterator)->accept(this);
+ }
if (!is_assignable_from(*type_iterator, (*argument_iterator)->type_decoration))
{
add_error<type_mismatch_error>(
@@ -998,18 +1403,18 @@ namespace elna::boot
}
}
}
- else if (!call->callable().type_decoration.empty())
+ else if (inferred == nullptr && !call->callable().type_decoration.empty())
{
add_error<type_mismatch_error>(call->position(),
call->callable().type_decoration,
type_mismatch_error::expected_type{ type(std::make_shared<procedure_type>()) });
}
- // else callable is not declared which is already reported.
+ // else callable is not declared, or inference failed and reported.
}
void type_analysis_visitor::visit(record_constructor_expression *expression)
{
- auto record = resolve_underlying_type(expression->type_decoration).get<record_type>();
+ auto record = erase_generic(expression->type_decoration).get<record_type>();
if (record == nullptr)
{
@@ -1139,7 +1544,14 @@ namespace elna::boot
{
walking_visitor::visit(expression);
- if (resolve_underlying_type(expression->base().type_decoration).get<pointer_type>() == nullptr)
+ const type resolved_base = resolve_underlying_type(expression->base().type_decoration);
+
+ if (resolved_base.get<parameter_type>() != nullptr)
+ {
+ add_error<type_requirement_error>(expression->position(), expression->base().type_decoration,
+ type_requirement_error::kind::dereference_of_parameter);
+ }
+ else if (resolved_base.get<pointer_type>() == nullptr)
{
add_error<type_requirement_error>(expression->position(), expression->base().type_decoration,
type_requirement_error::kind::dereference_of_non_pointer);
@@ -1265,10 +1677,12 @@ namespace elna::boot
};
add_error<type_mismatch_error>(expression->position(), expression->lhs().type_decoration, binary_error);
}
- else if (auto opaque = find_opaque_arithmetic(operation, lhs_resolved, rhs_resolved))
+ else if (auto violation = find_arithmetic_violation(operation, lhs_resolved, rhs_resolved))
{
- add_error<type_requirement_error>(expression->position(),
- opaque.value(), type_requirement_error::kind::opaque_arithmetic);
+ add_error<type_requirement_error>(expression->position(), violation.value(),
+ violation->get<parameter_type>() != nullptr
+ ? type_requirement_error::kind::parameter_arithmetic
+ : type_requirement_error::kind::opaque_arithmetic);
}
}