mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 21:40:12 +01:00
This switches `DCHECK` and `FATAL` as well. The goal is to reduce the code size impact of these assertions so that we can keep more of them enabled. Currently, the largest cost I see from `CHECK` is not the actual check or the cold code itself, but actually the failure to inline trivial functions due to the presence of the cold code. This means that our goal isn't to reduce apparent code size in the final binary but the LLVM IR cost assessed for these routines in the inliner, which closely correlates with code size but is a bit different. As discussed in #4283, experimentation shows that a single function call with a minimal number of arguments is the lowest cost model for these. This is easily achieved with a format-string API that internally uses `llvm::formatv`. This PR is essentially the `CHECK` version of #4283. However, the check macros are substantially harder to make work with both format strings and streaming because they also take a condition. Also, unexpectedly, I was very successful at devising a regular expression based automated rewrite from the streaming to the format string form with only low 10s of manual fixes. This includes compacting strings broken up across lines, etc. Given how well that went, I've prepared this PR which just directly switches to the format string API and migrate everything to use it. One nice side-effect is that the format string approach ends up greatly simplifying the implementation here as well. This is ... *shockingly* effective. Parsing speeds up by more than 3% with just this change. And checking speeds up by **8%** with this change alone: ``` BM_CompileAPIFileDenseDecls<Phase::Parse>/256 86.3µs ± 1% 82.9µs ± 1% -3.94% (p=0.000 n=17+19) BM_CompileAPIFileDenseDecls<Phase::Parse>/1024 431µs ± 1% 415µs ± 1% -3.76% (p=0.000 n=18+19) BM_CompileAPIFileDenseDecls<Phase::Parse>/4096 1.77ms ± 1% 1.71ms ± 1% -3.18% (p=0.000 n=18+19) BM_CompileAPIFileDenseDecls<Phase::Parse>/16384 7.44ms ± 1% 7.17ms ± 2% -3.56% (p=0.000 n=18+20) BM_CompileAPIFileDenseDecls<Phase::Parse>/65536 30.7ms ± 1% 29.7ms ± 1% -3.15% (p=0.000 n=18+20) BM_CompileAPIFileDenseDecls<Phase::Parse>/262144 131ms ± 1% 127ms ± 1% -2.81% (p=0.000 n=18+18) BM_CompileAPIFileDenseDecls<Phase::Check>/256 878µs ± 2% 800µs ± 1% -8.91% (p=0.000 n=19+20) BM_CompileAPIFileDenseDecls<Phase::Check>/1024 1.88ms ± 2% 1.72ms ± 1% -8.56% (p=0.000 n=19+20) BM_CompileAPIFileDenseDecls<Phase::Check>/4096 5.78ms ± 2% 5.28ms ± 1% -8.70% (p=0.000 n=20+18) BM_CompileAPIFileDenseDecls<Phase::Check>/16384 21.9ms ± 1% 20.1ms ± 1% -8.02% (p=0.000 n=18+20) BM_CompileAPIFileDenseDecls<Phase::Check>/65536 90.4ms ± 2% 83.1ms ± 1% -8.04% (p=0.000 n=19+20) BM_CompileAPIFileDenseDecls<Phase::Check>/262144 381ms ± 2% 352ms ± 1% -7.79% (p=0.000 n=19+19) ``` --------- Co-authored-by: Richard Smith <richard@metafoo.co.uk> Co-authored-by: josh11b <15258583+josh11b@users.noreply.github.com>
446 lines
18 KiB
C++
446 lines
18 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "explorer/interpreter/impl_scope.h"
|
|
|
|
#include "explorer/ast/value.h"
|
|
#include "explorer/interpreter/type_checker.h"
|
|
#include "llvm/ADT/StringExtras.h"
|
|
#include "llvm/Support/Casting.h"
|
|
|
|
using llvm::cast;
|
|
using llvm::dyn_cast;
|
|
|
|
namespace Carbon {
|
|
|
|
void ImplScope::Add(Nonnull<const Value*> iface, Nonnull<const Value*> type,
|
|
Nonnull<const Witness*> witness,
|
|
const TypeChecker& type_checker) {
|
|
Add(iface, {}, type, {}, witness, type_checker);
|
|
}
|
|
|
|
void ImplScope::Add(Nonnull<const Value*> iface,
|
|
llvm::ArrayRef<Nonnull<const GenericBinding*>> deduced,
|
|
Nonnull<const Value*> type,
|
|
llvm::ArrayRef<Nonnull<const ImplBinding*>> impl_bindings,
|
|
Nonnull<const Witness*> witness,
|
|
const TypeChecker& type_checker,
|
|
std::optional<TypeStructureSortKey> sort_key) {
|
|
if (const auto* constraint = dyn_cast<ConstraintType>(iface)) {
|
|
CARBON_CHECK(!sort_key,
|
|
"should only be given a sort key for an impl of an interface");
|
|
// The caller should have substituted `.Self` for `type` already.
|
|
Add(constraint->impls_constraints(), deduced, impl_bindings, witness,
|
|
type_checker);
|
|
// A parameterized impl declaration doesn't contribute any equality
|
|
// constraints to the scope. Instead, we'll resolve the equality
|
|
// constraints by resolving a witness when needed.
|
|
if (deduced.empty()) {
|
|
for (const auto& equality_constraint :
|
|
constraint->equality_constraints()) {
|
|
equalities_.push_back(&equality_constraint);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
ImplFact new_impl = {.interface = cast<InterfaceType>(iface),
|
|
.deduced = deduced,
|
|
.type = type,
|
|
.impl_bindings = impl_bindings,
|
|
.witness = witness,
|
|
.sort_key = std::move(sort_key)};
|
|
|
|
// Find the first impl that's more specific than this one, and place this
|
|
// impl right before it. This keeps the impls with the same type structure
|
|
// sorted in lexical order, which is important for `match_first` semantics.
|
|
auto insert_pos =
|
|
std::upper_bound(impl_facts_.begin(), impl_facts_.end(), new_impl,
|
|
[](const ImplFact& a, const ImplFact& b) {
|
|
return a.sort_key < b.sort_key;
|
|
});
|
|
|
|
impl_facts_.insert(insert_pos, std::move(new_impl));
|
|
}
|
|
|
|
void ImplScope::Add(llvm::ArrayRef<ImplsConstraint> impls_constraints,
|
|
llvm::ArrayRef<Nonnull<const GenericBinding*>> deduced,
|
|
llvm::ArrayRef<Nonnull<const ImplBinding*>> impl_bindings,
|
|
Nonnull<const Witness*> witness,
|
|
const TypeChecker& type_checker) {
|
|
for (size_t i = 0; i != impls_constraints.size(); ++i) {
|
|
ImplsConstraint impl = impls_constraints[i];
|
|
Add(impl.interface, deduced, impl.type, impl_bindings,
|
|
type_checker.MakeConstraintWitnessAccess(witness, i), type_checker);
|
|
}
|
|
}
|
|
|
|
// Diagnose that `a_evaluated != b_evaluated` for the purpose of an equality
|
|
// constraint.
|
|
static auto DiagnoseUnequalValues(SourceLocation source_loc,
|
|
Nonnull<const Value*> a_written,
|
|
Nonnull<const Value*> a_evaluated,
|
|
Nonnull<const Value*> b_written,
|
|
Nonnull<const Value*> b_evaluated,
|
|
Nonnull<const EqualityContext*> equality_ctx)
|
|
-> Error {
|
|
CARBON_CHECK(!ValueEqual(a_evaluated, b_evaluated, equality_ctx),
|
|
"expected unequal values");
|
|
auto error = ProgramError(source_loc);
|
|
error << "constraint requires that " << *a_written;
|
|
if (!ValueEqual(a_written, a_evaluated, std::nullopt)) {
|
|
error << " (with value " << *a_evaluated << ")";
|
|
}
|
|
error << " == " << *b_written;
|
|
if (!ValueEqual(b_written, b_evaluated, std::nullopt)) {
|
|
error << " (with value " << *b_evaluated << ")";
|
|
}
|
|
error << ", which is not known to be true";
|
|
return std::move(error);
|
|
}
|
|
|
|
auto ImplScope::Resolve(Nonnull<const Value*> constraint_type,
|
|
Nonnull<const Value*> impl_type,
|
|
SourceLocation source_loc,
|
|
const TypeChecker& type_checker,
|
|
const Bindings& bindings) const
|
|
-> ErrorOr<Nonnull<const Witness*>> {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
std::optional<Nonnull<const Witness*>> witness,
|
|
TryResolve(constraint_type, impl_type, source_loc, type_checker, bindings,
|
|
/*diagnose_missing_impl=*/true));
|
|
CARBON_CHECK(witness, "should have diagnosed missing impl");
|
|
return *witness;
|
|
}
|
|
|
|
auto ImplScope::TryResolve(Nonnull<const Value*> constraint_type,
|
|
Nonnull<const Value*> impl_type,
|
|
SourceLocation source_loc,
|
|
const TypeChecker& type_checker,
|
|
const Bindings& bindings,
|
|
bool diagnose_missing_impl) const
|
|
-> ErrorOr<std::optional<Nonnull<const Witness*>>> {
|
|
if (const auto* iface_type = dyn_cast<InterfaceType>(constraint_type)) {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
iface_type,
|
|
type_checker.SubstituteCast<InterfaceType>(bindings, iface_type));
|
|
return TryResolveInterface(iface_type, impl_type, source_loc, type_checker,
|
|
diagnose_missing_impl);
|
|
}
|
|
if (const auto* constraint = dyn_cast<ConstraintType>(constraint_type)) {
|
|
std::vector<Nonnull<const Witness*>> witnesses;
|
|
for (auto impl : constraint->impls_constraints()) {
|
|
// Note that later impls constraints can refer to earlier impls
|
|
// constraints via impl bindings. For example, in
|
|
// `C where .Self.AssocType impls D`,
|
|
// ... the `.Self.AssocType impls D` constraint refers to the
|
|
// `.Self impls C` constraint when naming `AssocType`. So incrementally
|
|
// build up a partial constraint witness as we go.
|
|
std::optional<Nonnull<const Witness*>> witness;
|
|
if (constraint->self_binding()->impl_binding()) {
|
|
// Note, this is a partial impl binding covering only the impl
|
|
// constraints that we've already seen. Earlier impls constraints should
|
|
// not be able to refer to impl bindings for later impls constraints.
|
|
witness = type_checker.MakeConstraintWitness(witnesses);
|
|
}
|
|
Bindings local_bindings = bindings;
|
|
local_bindings.Add(constraint->self_binding(), impl_type, witness);
|
|
|
|
CARBON_ASSIGN_OR_RETURN(const auto* subst_interface,
|
|
type_checker.SubstituteCast<InterfaceType>(
|
|
local_bindings, impl.interface));
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Nonnull<const Value*> subst_type,
|
|
type_checker.Substitute(local_bindings, impl.type));
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
std::optional<Nonnull<const Witness*>> result,
|
|
TryResolveInterface(subst_interface, subst_type, source_loc,
|
|
type_checker, diagnose_missing_impl));
|
|
if (!result) {
|
|
return {std::nullopt};
|
|
}
|
|
witnesses.push_back(*result);
|
|
}
|
|
|
|
// Check that all intrinsic, equality, and rewrite constraints
|
|
// are satisfied in this scope.
|
|
llvm::ArrayRef<IntrinsicConstraint> intrinsics =
|
|
constraint->intrinsic_constraints();
|
|
llvm::ArrayRef<EqualityConstraint> equals =
|
|
constraint->equality_constraints();
|
|
llvm::ArrayRef<RewriteConstraint> rewrites =
|
|
constraint->rewrite_constraints();
|
|
if (!intrinsics.empty() || !equals.empty() || !rewrites.empty()) {
|
|
std::optional<Nonnull<const Witness*>> witness;
|
|
if (constraint->self_binding()->impl_binding()) {
|
|
witness = type_checker.MakeConstraintWitness(witnesses);
|
|
}
|
|
Bindings local_bindings = bindings;
|
|
local_bindings.Add(constraint->self_binding(), impl_type, witness);
|
|
SingleStepEqualityContext equality_ctx(this);
|
|
for (const auto& intrinsic : intrinsics) {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Nonnull<const Value*> type,
|
|
type_checker.Substitute(local_bindings, intrinsic.type));
|
|
IntrinsicConstraint converted(type, intrinsic.kind, {});
|
|
converted.arguments.reserve(intrinsic.arguments.size());
|
|
for (Nonnull<const Value*> argument : intrinsic.arguments) {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Nonnull<const Value*> subst_arg,
|
|
type_checker.Substitute(local_bindings, argument));
|
|
converted.arguments.push_back(subst_arg);
|
|
}
|
|
CARBON_ASSIGN_OR_RETURN(bool intrinsic_satisfied,
|
|
type_checker.IsIntrinsicConstraintSatisfied(
|
|
source_loc, converted, *this));
|
|
if (!intrinsic_satisfied) {
|
|
if (!diagnose_missing_impl) {
|
|
return {std::nullopt};
|
|
}
|
|
return ProgramError(source_loc)
|
|
<< "constraint requires that " << converted;
|
|
}
|
|
}
|
|
for (const auto& equal : equals) {
|
|
auto it = equal.values.begin();
|
|
CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> first,
|
|
type_checker.Substitute(local_bindings, *it++));
|
|
for (; it != equal.values.end(); ++it) {
|
|
CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> current,
|
|
type_checker.Substitute(local_bindings, *it));
|
|
if (!ValueEqual(first, current, &equality_ctx)) {
|
|
if (!diagnose_missing_impl) {
|
|
return {std::nullopt};
|
|
}
|
|
return DiagnoseUnequalValues(source_loc, equal.values.front(),
|
|
first, *it, current, &equality_ctx);
|
|
}
|
|
}
|
|
}
|
|
for (const auto& rewrite : rewrites) {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Nonnull<const Value*> constant,
|
|
type_checker.Substitute(local_bindings, rewrite.constant));
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Nonnull<const Value*> value,
|
|
type_checker.Substitute(local_bindings,
|
|
rewrite.converted_replacement));
|
|
if (!ValueEqual(constant, value, &equality_ctx)) {
|
|
if (!diagnose_missing_impl) {
|
|
return {std::nullopt};
|
|
}
|
|
return DiagnoseUnequalValues(source_loc, rewrite.constant, constant,
|
|
rewrite.converted_replacement, value,
|
|
&equality_ctx);
|
|
}
|
|
}
|
|
}
|
|
return {type_checker.MakeConstraintWitness(std::move(witnesses))};
|
|
}
|
|
CARBON_FATAL("expected a constraint, not {0}", *constraint_type);
|
|
}
|
|
|
|
auto ImplScope::VisitEqualValues(
|
|
Nonnull<const Value*> value,
|
|
llvm::function_ref<bool(Nonnull<const Value*>)> visitor) const -> bool {
|
|
for (Nonnull<const EqualityConstraint*> eq : equalities_) {
|
|
if (!eq->VisitEqualValues(value, visitor)) {
|
|
return false;
|
|
}
|
|
}
|
|
return !parent_scope_ || (*parent_scope_)->VisitEqualValues(value, visitor);
|
|
}
|
|
|
|
auto ImplScope::TryResolveInterface(Nonnull<const InterfaceType*> iface_type,
|
|
Nonnull<const Value*> type,
|
|
SourceLocation source_loc,
|
|
const TypeChecker& type_checker,
|
|
bool diagnose_missing_impl) const
|
|
-> ErrorOr<std::optional<Nonnull<const Witness*>>> {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
std::optional<ResolveResult> result,
|
|
TryResolveInterfaceRecursively(iface_type, type, source_loc, *this,
|
|
type_checker));
|
|
if (!result.has_value() && diagnose_missing_impl) {
|
|
return ProgramError(source_loc) << "could not find implementation of "
|
|
<< *iface_type << " for " << *type;
|
|
}
|
|
return result ? std::optional(result->witness) : std::nullopt;
|
|
}
|
|
|
|
// Do these two witnesses refer to `impl` declarations in the same
|
|
// `match_first` block?
|
|
static auto InSameMatchFirst(Nonnull<const Witness*> a,
|
|
Nonnull<const Witness*> b) -> bool {
|
|
const auto* impl_a = dyn_cast<ImplWitness>(a);
|
|
const auto* impl_b = dyn_cast<ImplWitness>(b);
|
|
if (!impl_a || !impl_b) {
|
|
return false;
|
|
}
|
|
|
|
// TODO: Once we support an impl being declared more than once, we will need
|
|
// to check this more carefully.
|
|
return impl_a->declaration().match_first() &&
|
|
impl_a->declaration().match_first() ==
|
|
impl_b->declaration().match_first();
|
|
}
|
|
|
|
// Determine whether this result is definitely right -- that there can be no
|
|
// specialization that would give a better match.
|
|
static auto IsEffectivelyFinal(ImplScope::ResolveResult result) -> bool {
|
|
// TODO: Once we support 'final', check whether this is a final impl
|
|
// declaration if it's parameterized.
|
|
return result.impl->deduced.empty();
|
|
}
|
|
|
|
// Combines the results of two impl lookups. In the event of a tie, arbitrarily
|
|
// prefer `a` over `b`.
|
|
static auto CombineResults(Nonnull<const InterfaceType*> iface_type,
|
|
Nonnull<const Value*> type,
|
|
SourceLocation source_loc,
|
|
std::optional<ImplScope::ResolveResult> a,
|
|
std::optional<ImplScope::ResolveResult> b)
|
|
-> ErrorOr<std::optional<ImplScope::ResolveResult>> {
|
|
// If only one lookup succeeded, return that.
|
|
if (!b) {
|
|
return a;
|
|
}
|
|
if (!a) {
|
|
return b;
|
|
}
|
|
|
|
// If exactly one of them is effectively final, prefer that result.
|
|
bool a_is_final = IsEffectivelyFinal(*a);
|
|
bool b_is_final = IsEffectivelyFinal(*b);
|
|
if (a_is_final && !b_is_final) {
|
|
return a;
|
|
} else if (b_is_final && !a_is_final) {
|
|
return b;
|
|
}
|
|
|
|
const auto* impl_a = dyn_cast<ImplWitness>(a->witness);
|
|
const auto* impl_b = dyn_cast<ImplWitness>(b->witness);
|
|
|
|
// If both are effectively final, prefer an impl declaration over a
|
|
// symbolic ImplBinding, because we get more information from the impl
|
|
// declaration. If they're both symbolic, arbitrarily pick a.
|
|
if (a_is_final && b_is_final) {
|
|
if (!impl_b) {
|
|
return a;
|
|
}
|
|
if (!impl_a) {
|
|
return b;
|
|
}
|
|
}
|
|
CARBON_CHECK(impl_a && impl_b, "non-final impl should not be symbolic");
|
|
|
|
// At this point, we're comparing two `impl` declarations, and either they're
|
|
// both final or neither of them is.
|
|
// TODO: We should reject the case where both are final when checking their
|
|
// declarations, but we don't do so yet, so for now we report it as an
|
|
// ambiguity.
|
|
//
|
|
// If they refer to the same `impl` declaration, it doesn't matter which one
|
|
// we pick, so we pick `a`.
|
|
// TODO: Compare the identities of the `impl`s, not the declarations.
|
|
if (&impl_a->declaration() == &impl_b->declaration()) {
|
|
return a;
|
|
}
|
|
|
|
return ProgramError(source_loc)
|
|
<< "ambiguous implementations of " << *iface_type << " for " << *type;
|
|
}
|
|
|
|
auto ImplScope::TryResolveInterfaceRecursively(
|
|
Nonnull<const InterfaceType*> iface_type, Nonnull<const Value*> type,
|
|
SourceLocation source_loc, const ImplScope& original_scope,
|
|
const TypeChecker& type_checker) const
|
|
-> ErrorOr<std::optional<ResolveResult>> {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
std::optional<ResolveResult> result,
|
|
TryResolveInterfaceHere(iface_type, type, source_loc, original_scope,
|
|
type_checker));
|
|
if (parent_scope_) {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
std::optional<ResolveResult> parent_result,
|
|
(*parent_scope_)
|
|
->TryResolveInterfaceRecursively(iface_type, type, source_loc,
|
|
original_scope, type_checker));
|
|
CARBON_ASSIGN_OR_RETURN(result, CombineResults(iface_type, type, source_loc,
|
|
result, parent_result));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
auto ImplScope::TryResolveInterfaceHere(
|
|
Nonnull<const InterfaceType*> iface_type, Nonnull<const Value*> impl_type,
|
|
SourceLocation source_loc, const ImplScope& original_scope,
|
|
const TypeChecker& type_checker) const
|
|
-> ErrorOr<std::optional<ResolveResult>> {
|
|
std::optional<ResolveResult> result = std::nullopt;
|
|
for (const ImplFact& impl : impl_facts_) {
|
|
// If we've passed the final impl with a sort key matching our best impl,
|
|
// all further are worse and don't need to be checked.
|
|
if (result && result->impl->sort_key < impl.sort_key) {
|
|
break;
|
|
}
|
|
|
|
// If this impl appears later in the same match_first block as our best
|
|
// result, we should not consider it.
|
|
//
|
|
// TODO: This should apply transitively: if we have
|
|
// match_first { impl a; impl b; }
|
|
// match_first { impl b; impl c; }
|
|
// then we should not consider c once we match a. For now, because each
|
|
// impl is only declared once, this is not a problem.
|
|
if (result && InSameMatchFirst(result->impl->witness, impl.witness)) {
|
|
continue;
|
|
}
|
|
|
|
// Try matching this impl against our query.
|
|
CARBON_ASSIGN_OR_RETURN(std::optional<Nonnull<const Witness*>> witness,
|
|
type_checker.MatchImpl(*iface_type, impl_type, impl,
|
|
original_scope, source_loc));
|
|
if (witness) {
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
result,
|
|
CombineResults(iface_type, impl_type, source_loc, result,
|
|
ResolveResult{.impl = &impl, .witness = *witness}));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// TODO: Add indentation when printing the parents.
|
|
void ImplScope::Print(llvm::raw_ostream& out) const {
|
|
llvm::ListSeparator sep(",\n ");
|
|
out << " "
|
|
<< "[";
|
|
for (const ImplFact& impl : impl_facts_) {
|
|
out << sep << "`" << *(impl.type) << "` as `" << *(impl.interface) << "`";
|
|
if (impl.sort_key) {
|
|
out << " " << *impl.sort_key;
|
|
}
|
|
}
|
|
for (Nonnull<const EqualityConstraint*> eq : equalities_) {
|
|
out << sep;
|
|
llvm::ListSeparator equal(" == ");
|
|
for (Nonnull<const Value*> value : eq->values) {
|
|
out << equal << "`" << *value << "`";
|
|
}
|
|
}
|
|
out << "]\n";
|
|
if (parent_scope_) {
|
|
out << **parent_scope_;
|
|
}
|
|
}
|
|
|
|
auto SingleStepEqualityContext::VisitEqualValues(
|
|
Nonnull<const Value*> value,
|
|
llvm::function_ref<bool(Nonnull<const Value*>)> visitor) const -> bool {
|
|
return impl_scope_->VisitEqualValues(value, visitor);
|
|
}
|
|
|
|
} // namespace Carbon
|