Skip to content

Introduce a simple request evaluator. #15917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 12, 2018
Merged

Conversation

DougGregor
Copy link
Member

Meant as a replacement for the barely-started iterative type checker,
introduce a simpler "evaluator" that can evaluate individual requests
(essentially, function objects with some additional API), caching
results as appropriate and detecting cycles. Start stubbing out what
is involved in replacing the iterative type checker with this infrastructure.

Copy link
Contributor

@graydon graydon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bunch of nits but looks good!

/// - Hashing support (hash_value)
/// - TypeID support (see swift/Basic/TypeID.h)
/// - Cycle diagnostics operations:
///
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you forget 'OutputType breakCycle() const' here? It looks like it's present in a similar comment down below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnyRequest itself doesn't need it, because we only call the breakCycle operation on a request before it's been type-erased. We can add it here if we need it later.

///
/// void diagnoseCycle(DiagnosticEngine &diags) const;
/// void noteCycleStep(DiagnosticEngine &diags) const;
/// OutputType breakCycle() Const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Const/const/

/// static const bool isEverCached;
///
/// When false, the request's result will never be cached. Note that this
/// also precludes cycle detection. When true, the result will be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does not-caching preclude cycle detection?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it doesn't have to. I'm using the cache to detect the presence of a cycle, but there's no reason I have to do that... I have the stack of results to be satisfied and can unique there. Thanks!

/// request.
void diagnoseCycle(const AnyRequest &request);

/// Retrieve the result of produced by evaluating a request that can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/result of/result/

return getResultUncached(request);
}

/// Retrieve the result of produced by evaluating a request that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/result of/result/

return getResultUncached(request);
}

/// Produce the result of the request without caching.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/result of/result/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, this one is actually correct ;)

typename Request::OutputType getResultUncached(const Request &request) {
// Push this request onto the active-requests stack while we
// evaluate it.
activeRequests.push_back(request);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why push/pop it if you can't / aren't going to detect cycles involving it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm diagnosing cycles based on the stack, but you're right (per above) that I should use this for cycle detection as well.


namespace swift {

/// Stores a value of any type satisfies a small set of requirements
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/any type satisfies/any type that satisfies/


/// Form a type ID given a zone and type value.
constexpr uint8_t formTypeID(uint8_t zone, uint8_t type) {
return (zone << 8) | type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably doing my own arithmetic wrong, here but isn't a uint8_t shifted left by 8 bits always going to be 0? Maybe this should be returning a uint64_t?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should absolutely be returning a uint64_t!

\
public: \
static const uint64_t value = \
(templateID << (depth * 8)) | TypeID<Arg1>::value; \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again here, I think the arithmetic is off, and you want to shift by (depth * 16). Assuming you mean to have [256 zones | 256 types] packed as a 16 bits in each of up-to-4 depths, in a 64-bit word.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny how it's the simple bit-manipulation math that I completely screwed up in this pull request. Thank you (and @huonw)

namespace swift {

/// Stores a value of any type satisfies a small set of requirements
/// (currenrly, just equatability).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currenrly -> currently.


/// Cast to a specific (known) type.
template<typename T>
const T &castTo() const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have an assert on the TypeId<T>::value.

\
public: \
static const uint64_t value = \
(templateID << (depth * 8)) | TypeID<Arg1>::value; \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be depth * 16. Also, could we could possibly avoid depth all together using (TypeID<Arg1>::value << 16) | templateID, assuming value is guaranteed to be non-zero (and/or static_assert that it's non-zero)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I've done this.


/// Form a type ID given a zone and type value.
constexpr uint8_t formTypeID(uint8_t zone, uint8_t type) {
return (zone << 8) | type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this looks like overflow; should this return uint16_t? (and are casts needed to stop the intermediate overflowing?)

SWIFT_TYPEID(C, float, 12);
SWIFT_TYPEID(C, double, 13);
SWIFT_TYPEID(C, bool, 14);
SWIFT_TYPEID(C, decltype(nullptr), 15);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird indentation?


struct TypeLoc;

/// Request the type from the ith entry in the inheritance clause for the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"index-th"

}
};

SWIFT_TYPEID(TypeChecker, InheritedTypeRequest, 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing these individually by hand seems prone to mistakes and problems with merge conflicts? Is it worth having (yet another) .def file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I will do something here. It's an annoying pile of preprocessor metaprogramming.

#include "swift/Basic/AnyValue.h"
using namespace swift;

AnyValue::HolderBase::~HolderBase() { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the second definition of this: it's also above in lib/AST/Evaluator.cpp https://github.com/apple/swift/pull/15917/files#diff-dfb37be769b2d50ad4c150591bf84c59R22 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That one is AnyRequest::HolderBase::~HolderBase().

/// When true, the request itself must provide an way to cache the
/// results, e.g., in some external data structure. External caching
/// should only be used when staging in the use of the evaluator into
/// existing mutable data structures; new computations should not depend
Copy link
Contributor

@huonw huonw Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm! Are we going to migrate to, say, every TypeLoc going through the Evaluator? If so, it kinda seems like we may want something with a more continuous memory layout? I'm thinking of basically doing an AoS-to-SoA transform, something like:

class Evaluator {
    // ...

    llvm::DenseMap<TypeId, AnyCache> caches;
    // ...
}

class AnyCache {
    // a type erased `llvm::DenseMap<Request, Request::OutputType>`
}

or, even, going fully-static/killing our compile times:

template<typename Request...>
class Evaluator {
    std::tuple<llvm::DenseMap<Request, Request::OutputType>...> caches;
    // or whatever the template magic to get a DenseMap for each request type is.
    // (plus extra magic to elide the map for externally cached requests.)
}

using SemaEvaluator = Evaluator< InheritedTypeRequest, SuperclassTypeRequest, ...>;

The getResultCached function would then first need to find the appropriate cache (a downside of the first approach is, of course, that this may be moderately expensive and so outweighs the benefits of that style vs. the [AnyRequest : AnyValue?] version), but I think the rest is the same.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fantastic idea! Not the fully-static one, which would kill our layering and our compile times, but the type-erased AnyCache would be much better.

@DougGregor
Copy link
Member Author

Some feedback addressed, but I haven't simplified TypeID definitions yet nor have I implemented @huonw 's AnyCache suggestion.

@DougGregor DougGregor force-pushed the evaluator branch 2 times, most recently from 3500f16 to 7eb6a97 Compare April 24, 2018 13:54
@huonw
Copy link
Contributor

huonw commented Apr 26, 2018

The double layer of X-macros for the zones is... slightly mind-bending, but makes sense.

//
// SWIFT_TYPEID_ZONE: The name of the Zone being defined, e.g.,
// SwiftAST for the Swift AST library's zone. All zones need to be defined
// a priori as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

....as....?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with the latest update, thanks

@DougGregor
Copy link
Member Author

Several goodies in the latest update:

  • All requests (and all values) are printable, so we can inspect anything

  • Added SimpleRequest, which takes care of defining a new request with a simple payload (list of types) and automates basically all of it.

  • Online dependency tracking

  • Printing of the dependencies for a given request (as a tree), along with cached results, e.g.,

 `--InternallyCachedEvaluationRule(Binary: product) -> 2.461145e+02
     `--InternallyCachedEvaluationRule(Binary: sum) -> 5.859870e+00
     |   `--InternallyCachedEvaluationRule(Literal: 3.141590e+00)
     |   `--InternallyCachedEvaluationRule(Literal: 2.718280e+00)
     `--InternallyCachedEvaluationRule(Literal: 4.200000e+01)
  • Printing all of the dependencies known to the evaluator as a GraphViz graph, along with known cached results, e.g.,
digraph Dependencies {
  request_0 -> request_1;
  request_0 -> request_4;
  request_1 -> request_3;
  request_1 -> request_2;
  request_5 -> request_6;
  request_5 -> request_9;
  request_6 -> request_8;
  request_6 -> request_7;
  request_10 -> request_11;
  request_10 -> request_14;
  request_11 -> request_13;
  request_11 -> request_12;

  request_0 [label="ExternallyCachedEvaluationRule(Binary: product)"];
  request_1 [label="ExternallyCachedEvaluationRule(Binary: sum)"];
  request_2 [label="ExternallyCachedEvaluationRule(Literal: 2.718280e+00)"];
  request_3 [label="ExternallyCachedEvaluationRule(Literal: 3.141590e+00)"];
  request_4 [label="ExternallyCachedEvaluationRule(Literal: 4.200000e+01)"];
  request_5 [label="InternallyCachedEvaluationRule(Binary: product) -> 2.461145e+02"];
  request_6 [label="InternallyCachedEvaluationRule(Binary: sum) -> 5.859870e+00"];
  request_7 [label="InternallyCachedEvaluationRule(Literal: 2.718280e+00)"];
  request_8 [label="InternallyCachedEvaluationRule(Literal: 3.141590e+00)"];
  request_9 [label="InternallyCachedEvaluationRule(Literal: 4.200000e+01)"];
  request_10 [label="UncachedEvaluationRule(Binary: product)"];
  request_11 [label="UncachedEvaluationRule(Binary: sum)"];
  request_12 [label="UncachedEvaluationRule(Literal: 2.718280e+00)"];
  request_13 [label="UncachedEvaluationRule(Literal: 3.141590e+00)"];
  request_14 [label="UncachedEvaluationRule(Literal: 4.200000e+01)"];
}

DougGregor added 13 commits June 1, 2018 08:56
Meant as a replacement for the barely-started iterative type checker,
introduce a simpler "evaluator" that can evaluate individual requests
(essentially, function objects with some additional API), caching
results as appropriate and detecting cycles.
…he cache.

Turn the `activeRequests` stack into a `SetVector` and use it to detect
cycles. This approach works for uncached requests, and lets us simplify
the external-caching protocol quite a bit because we no longer record
“in-flight” states. Simplify the cache as well.

Thanks to Graydon for the suggestion.
Simplify the static registration of types for use with TypeID by introducing
a more declarative approach. Each zone provides a .def file listing the
types and templates defined by that zone. The .def file is processed by
include/swift/Basic/DefineTypeIDZone.h with its zone number, which assigns
values to each of the types/templates and introduces the TypeID
specializations.
…ntable.

Introduce a CRTP base class, SimpleRequest, which simplifies the task of
defining a new request kind by handling the storage of the values (in a
std::tuple), their hashing, equality, printing, etc. The values are passed
along to the subclass’s operator() so they’re mostly treated as (const)
parameters, making mutation of the request state impossible.

Extend AnyValue and AnyRequest with printing logic, so we can print any
request for debugging purposes, and
Perform online tracking of the (direct) dependencies of each request made
to the evaluator, and introduce debugging code to print these dependencies
as a tree (from a particular request).
Introduce another form of debugging dump for the evaluator, rendering the
complete dependency graph using GraphViz, including all dependencies and
values cached within the evaluator.
…Request.

The arithmetic evaluator cannot really make use of the new diagnostics
machinery, but put it in place for other request kinds.
Simplify the definition of new request kinds by pulling the parameterization
of caching logic into SimpleRequest itself, so that the caching-related
parts of the contract cannot easily be forgotten.
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor DougGregor changed the title [WIP][Prototype] Introduce a simple request evaluator. Introduce a simple request evaluator. Jun 12, 2018
@DougGregor
Copy link
Member Author

@swift-ci please smoke test and merge

1 similar comment
@DougGregor
Copy link
Member Author

@swift-ci please smoke test and merge

@DougGregor DougGregor merged commit 0f2de23 into swiftlang:master Jun 12, 2018
@DougGregor DougGregor deleted the evaluator branch June 12, 2018 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants