Skip to content

[Const evaluator] Add support to "skip" instructions in step-wise evaluation #24113

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 1 commit into from
May 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/swift/AST/DiagnosticsSIL.def
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ NOTE(constexpr_witness_call_with_no_target_found, none,
NOTE(constexpr_witness_call_found_here, none,
"witness method call found here", ())

NOTE(constexpr_unknown_control_flow_due_to_skip,none, "branch depends on "
"non-constant value produced by an unevaluated instructions", ())
NOTE(constexpr_returned_by_unevaluated_instruction,none,
"return value of an unevaluated instruction is not a constant", ())
NOTE(constexpr_mutated_by_unevaluated_instruction,none, "value mutable by an "
"unevaluated instruction is not a constant", ())

Choose a reason for hiding this comment

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

"skipped instruction" doesn't seem like a concept that should be exposed to the user.

In context of the os_log client, users might understand something like "result of an unrecognized operation" / "mutated by an unrecognized operation". What do you think about changing the diagnostic messages to those?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That right. It is not a user concept. It added this note for use in tests. It is not a part of the os_log client. Ideally, I would like to move all diagnostics to clients (like #assert client, tester client etc.) and specialize them as needed. In some sense, they are a part of the client model on how they handle errors in evaluation, as the interpreter itself is not a user-level concept, as of now.

ERROR(non_physical_addressof,none,
"addressof only works with purely physical lvalues; "
"use `withUnsafePointer` or `withUnsafeBytes` unless you're implementing "
Expand Down
10 changes: 10 additions & 0 deletions include/swift/SIL/SILConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ enum class UnknownReason {
/// A top-level value has multiple writers. This is only relevant in the
/// non-flow-sensitive evaluation mode, which is used by #assert.
MutipleTopLevelWriters,

/// Indicates the return value of an instruction that was not evaluated during
/// interpretation.
ReturnedByUnevaluatedInstruction,

/// Indicates that the value was possibly modified by an instruction
/// that was not evaluated during the interpretation.
MutatedByUnevaluatedInstruction,
};

/// An abstract class that exposes functions for allocating symbolic values.
Expand Down Expand Up @@ -416,6 +424,8 @@ class SymbolicValue {
/// reason, we fall back to using the specified location.
void emitUnknownDiagnosticNotes(SILLocation fallbackLoc);

bool isUnknownDueToUnevaluatedInstructions();

/// Clone this SymbolicValue into the specified Allocator and return the new
/// version. This only works for valid constants.
SymbolicValue cloneInto(SymbolicValueAllocator &allocator) const;
Expand Down
59 changes: 53 additions & 6 deletions include/swift/SILOptimizer/Utils/ConstExpr.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ class ConstExprEvaluator {

/// A constant-expression evaluator that can be used to step through a control
/// flow graph (SILFunction body) by evaluating one instruction at a time.
/// This evaluator can also "skip" instructions without evaluating them and
/// only track constant values of variables whose values could be computed.
class ConstExprStepEvaluator {
private:
ConstExprEvaluator evaluator;
Expand All @@ -97,10 +99,6 @@ class ConstExprStepEvaluator {
/// evaluation.
SmallPtrSet<SILBasicBlock *, 8> visitedBlocks;

Optional<SymbolicValue>
incrementStepsAndCheckLimit(SILInstruction *inst,
bool includeInInstructionLimit);

ConstExprStepEvaluator(const ConstExprEvaluator &) = delete;
void operator=(const ConstExprEvaluator &) = delete;

Expand All @@ -122,12 +120,61 @@ class ConstExprStepEvaluator {
/// Second element is None, if the evaluation is successful.
/// Otherwise, is an unknown symbolic value that contains the error.
std::pair<Optional<SILBasicBlock::iterator>, Optional<SymbolicValue>>
evaluate(SILBasicBlock::iterator instI,
bool includeInInstructionLimit = true);
evaluate(SILBasicBlock::iterator instI);

/// Skip the instruction without evaluating it and conservatively account for
/// the effects of the instruction on the internal state. This operation
/// resets to an unknown symbolic value any portion of a
/// SymbolicValueMemoryObject that could possibly be mutated by the given
/// instruction. This function preserves the soundness of the interpretation.
/// \param instI instruction to be skipped.
/// \returns a pair where the first and second elements are defined as
/// follows:
/// The first element, if is not None, is the iterator to the next
/// instruction from the where the evaluation must continue.
/// The first element is None if the next instruction from where the
/// evaluation must continue cannot be determined.
/// This would be the case if `instI` is a branch like a `condbr`.
///
/// Second element is None if skipping the instruction is successful.
/// Otherwise, it is an unknown symbolic value containing the error.
std::pair<Optional<SILBasicBlock::iterator>, Optional<SymbolicValue>>
skipByMakingEffectsNonConstant(SILBasicBlock::iterator instI);

/// Try evaluating an instruction and if the evaluation fails, skip the
/// instruction and make it effects non constant. Note that it may not always
/// be possible to skip an instruction whose evaluation failed and
/// continue evalution (e.g. a conditional branch).
/// See `evaluate` and `skipByMakingEffectsNonConstant` functions for their
/// semantics.
/// \param instI instruction to be evaluated in the current interpreter state.
/// \returns a pair where the first and second elements are defined as
/// follows:
/// The first element, if is not None, is the iterator to the next
/// instruction from the where the evaluation must continue.
/// The first element is None iff both `evaluate` and `skip` functions
/// failed to determine the next instruction to continue evaluation from.
///
/// Second element is None if the evaluation is successful.
/// Otherwise, it is an unknown symbolic value containing the error.
std::pair<Optional<SILBasicBlock::iterator>, Optional<SymbolicValue>>
tryEvaluateOrElseMakeEffectsNonConstant(SILBasicBlock::iterator instI);

Optional<SymbolicValue> lookupConstValue(SILValue value);

bool isKnownFunction(SILFunction *fun);

/// Returns true if and only if `errorVal` denotes an error that requires
/// aborting interpretation and returning the error. Skipping an instruction
/// that produces such errors is not a valid behavior.
bool isFailStopError(SymbolicValue errorVal);

/// Return the number of instructions evaluated for the last `evaluate`
/// operation. This could be used by the clients to limit the number of
/// instructions that should be evaluated by the step-wise evaluator.
/// Note that 'skipByMakingEffectsNonConstant' operation is not considered
/// as an evaluation.
unsigned instructionsEvaluatedByLastEvaluation() { return stepsEvaluated; }
};

} // end namespace swift
Expand Down
12 changes: 12 additions & 0 deletions lib/SIL/SILConstants.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,12 @@ SymbolicValue SymbolicValue::lookThroughSingleElementAggregates() const {
}
}

bool SymbolicValue::isUnknownDueToUnevaluatedInstructions() {
auto unknownReason = getUnknownReason();
return (unknownReason == UnknownReason::ReturnedByUnevaluatedInstruction ||
unknownReason == UnknownReason::MutatedByUnevaluatedInstruction);
}

/// Given that this is an 'Unknown' value, emit diagnostic notes providing
/// context about what the problem is. Specifically, point to interesting
/// source locations and function calls in the call stack.
Expand Down Expand Up @@ -676,6 +682,12 @@ void SymbolicValue::emitUnknownDiagnosticNotes(SILLocation fallbackLoc) {
if (emitTriggerLocInDiag)
diagnose(ctx, *triggerLoc, diag::constexpr_witness_call_found_here);
return;
case UnknownReason::ReturnedByUnevaluatedInstruction:
diagnose(ctx, diagLoc, diag::constexpr_returned_by_unevaluated_instruction);
break;
case UnknownReason::MutatedByUnevaluatedInstruction:
diagnose(ctx, diagLoc, diag::constexpr_mutated_by_unevaluated_instruction);
break;
}
// TODO: print the call-stack in a controlled way if needed.
}
Expand Down
48 changes: 43 additions & 5 deletions lib/SILOptimizer/UtilityPasses/ConstantEvaluatorTester.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//

#define DEBUG_TYPE "sil-constant-evaluation-tester"
#include "swift/AST/DiagnosticsSIL.h"
#include "swift/SIL/SILConstants.h"
#include "swift/SILOptimizer/PassManager/Passes.h"
#include "swift/SILOptimizer/PassManager/Transforms.h"
Expand All @@ -20,6 +21,12 @@ using namespace swift;

namespace {

template <typename... T, typename... U>
static InFlightDiagnostic diagnose(ASTContext &Context, SourceLoc loc,
Diag<T...> diag, U &&... args) {
return Context.Diags.diagnose(loc, diag, std::forward<U>(args)...);
}

/// A compiler pass for testing constant evaluator in the step-wise evaluation
/// mode. The pass evaluates SIL functions whose names start with "interpret"
/// and outputs the constant value returned by the function or diagnostics if
Expand All @@ -31,6 +38,18 @@ class ConstantEvaluatorTester : public SILFunctionTransform {
return fun->getName().startswith("interpret");
}

bool shouldSkipInstruction(SILInstruction *inst) {
auto *applyInst = dyn_cast<ApplyInst>(inst);
if (!applyInst)
return false;

auto *callee = applyInst->getReferencedFunction();
if (!callee)
return false;

return callee->getName().startswith("skip");
}

void run() override {
SILFunction *fun = getFunction();

Expand All @@ -52,6 +71,7 @@ class ConstantEvaluatorTester : public SILFunctionTransform {
if (!returnVal) {
llvm::errs() << "Returns unknown"
<< "\n";
break;
}
llvm::errs() << "Returns " << returnVal.getValue() << "\n";
break;
Expand All @@ -60,15 +80,33 @@ class ConstantEvaluatorTester : public SILFunctionTransform {
Optional<SILBasicBlock::iterator> nextInstOpt;
Optional<SymbolicValue> errorVal;

std::tie(nextInstOpt, errorVal) = stepEvaluator.evaluate(currI);
if (errorVal.hasValue()) {
// Diagnose the error.
assert(errorVal->getKind() == SymbolicValue::Unknown);
// If the instruction is marked as skip, skip it and make its effects
// non-constant. Otherwise, try evaluating the instruction and if the
// evaluation fails due to a previously skipped instruction,
// skip the current instruction.
if (shouldSkipInstruction(inst)) {
std::tie(nextInstOpt, errorVal) =
stepEvaluator.skipByMakingEffectsNonConstant(currI);
} else {
std::tie(nextInstOpt, errorVal) =
stepEvaluator.tryEvaluateOrElseMakeEffectsNonConstant(currI);
}

// Diagnose errors in the evaluation. Unknown symbolic values produced
// by skipping instructions are not considered errors.
if (errorVal.hasValue() &&
!errorVal->isUnknownDueToUnevaluatedInstructions()) {
errorVal->emitUnknownDiagnosticNotes(inst->getLoc());
break;
}

if (!nextInstOpt) {
diagnose(fun->getASTContext(), inst->getLoc().getSourceLoc(),
diag::constexpr_unknown_control_flow_due_to_skip);
errorVal->emitUnknownDiagnosticNotes(inst->getLoc());
break;
}

assert(nextInstOpt);
currI = nextInstOpt.getValue();
}
}
Expand Down
143 changes: 125 additions & 18 deletions lib/SILOptimizer/Utils/ConstExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,11 @@ SymbolicValue ConstExprFunctionState::computeConstantValue(SILValue value) {

for (unsigned i = 0, e = inst->getNumOperands(); i != e; ++i) {
auto val = getConstantValue(inst->getOperand(i));
if (!val.isConstant())
if (!val.isConstant() && !val.isUnknownDueToUnevaluatedInstructions())
return val;
// Unknown values due to unevaluated instructions can be assigned to
// struct properties as they are not indicative of a fatal error or
// trap.
elts.push_back(val);
}

Expand Down Expand Up @@ -1528,28 +1531,132 @@ ConstExprStepEvaluator::ConstExprStepEvaluator(SymbolicValueAllocator &alloc,

ConstExprStepEvaluator::~ConstExprStepEvaluator() { delete internalState; }

Optional<SymbolicValue> ConstExprStepEvaluator::incrementStepsAndCheckLimit(
SILInstruction *inst, bool includeInInstructionLimit) {
if (includeInInstructionLimit && ++stepsEvaluated > ConstExprLimit) {
// Note that there is no call stack to associate with the unknown value
// as the interpreter is at the top-level here.
return SymbolicValue::getUnknown(inst, UnknownReason::TooManyInstructions,
{}, evaluator.getAllocator());
std::pair<Optional<SILBasicBlock::iterator>, Optional<SymbolicValue>>
ConstExprStepEvaluator::evaluate(SILBasicBlock::iterator instI) {
// Reset `stepsEvaluated` to zero.
stepsEvaluated = 0;
return internalState->evaluateInstructionAndGetNext(instI, visitedBlocks);
}

std::pair<Optional<SILBasicBlock::iterator>, Optional<SymbolicValue>>
ConstExprStepEvaluator::skipByMakingEffectsNonConstant(
SILBasicBlock::iterator instI) {
SILInstruction *inst = &(*instI);

// Set all constant state that could be mutated by the instruction
// to an unknown symbolic value.
for (auto &operand : inst->getAllOperands()) {
auto constValOpt = lookupConstValue(operand.get());
if (!constValOpt) {
continue;
}
auto constVal = constValOpt.getValue();
auto constKind = constVal.getKind();

// Skip can only be invoked on value types or addresses of value types.
// Note that adding a new kind of symbolic value may require handling its
// side-effects, especially if that symbolic value does not represent a
// value type.
assert(constKind == SymbolicValue::Address ||
constKind == SymbolicValue::Unknown ||
constKind == SymbolicValue::Metatype ||
constKind == SymbolicValue::Function ||
constKind == SymbolicValue::Integer ||
constKind == SymbolicValue::String ||
constKind == SymbolicValue::Aggregate ||
constKind == SymbolicValue::Enum ||
constKind == SymbolicValue::EnumWithPayload ||
constKind == SymbolicValue::UninitMemory);

if (constKind != SymbolicValue::Address) {
continue;
}

// If the address is only used @in_guaranteed or @in_constant, there
// can be no mutation through this address. Therefore, ignore it.
if (ApplyInst *applyInst = dyn_cast<ApplyInst>(inst)) {
ApplySite applySite(applyInst);
SILArgumentConvention convention =
applySite.getArgumentConvention(operand);
if (convention == SILArgumentConvention::Indirect_In_Guaranteed ||
convention == SILArgumentConvention::Indirect_In_Constant) {
continue;
}
}

// Write an unknown value into the address.
SmallVector<unsigned, 4> accessPath;
auto *memoryObject = constVal.getAddressValue(accessPath);
auto unknownValue = SymbolicValue::getUnknown(
inst, UnknownReason::MutatedByUnevaluatedInstruction, {},
evaluator.getAllocator());

auto memoryContent = memoryObject->getValue();
if (memoryContent.getKind() == SymbolicValue::Aggregate) {
memoryObject->setIndexedElement(accessPath, unknownValue,
evaluator.getAllocator());
} else {
memoryObject->setValue(unknownValue);
}
}

// Map the results of this instruction to unknown values.
for (auto result : inst->getResults()) {
internalState->setValue(
result, SymbolicValue::getUnknown(
inst, UnknownReason::ReturnedByUnevaluatedInstruction, {},
evaluator.getAllocator()));
}

// If we have a next instruction in the basic block return it.
// Otherwise, return None for the next instruction.
// Note that we can find the next instruction in the case of unconditional
// branches. But, there is no real need to do that as of now.
if (!isa<TermInst>(inst)) {
return {++instI, None};
}
return {None, None};
}

bool ConstExprStepEvaluator::isFailStopError(SymbolicValue errorVal) {
assert(errorVal.isUnknown());

switch (errorVal.getUnknownReason()) {
case UnknownReason::TooManyInstructions:
case UnknownReason::Loop:
case UnknownReason::Overflow:
case UnknownReason::Trap:
return true;
default:
return false;
}
return None;
}

std::pair<Optional<SILBasicBlock::iterator>, Optional<SymbolicValue>>
ConstExprStepEvaluator::evaluate(SILBasicBlock::iterator instI,
bool includeInInstructionLimit) {
// Diagnose whether evaluating this instruction exceeds the instruction
// limit, unless asked to not include this instruction in the limit.
auto limitError =
incrementStepsAndCheckLimit(&(*instI), includeInInstructionLimit);
if (limitError) {
return {None, limitError.getValue()};
ConstExprStepEvaluator::tryEvaluateOrElseMakeEffectsNonConstant(
SILBasicBlock::iterator instI) {

auto evaluateResult = evaluate(instI);
Optional<SILBasicBlock::iterator> nextI = evaluateResult.first;
Optional<SymbolicValue> errorVal = evaluateResult.second;

if (!errorVal) {
assert(nextI);
return evaluateResult;
}
return internalState->evaluateInstructionAndGetNext(instI, visitedBlocks);
assert(!nextI);

if (isFailStopError(*errorVal)) {
return evaluateResult;
}

// Evaluation cannot fail on unconditional branches.
assert(!isa<BranchInst>(&(*instI)));

// Since the evaluation has failed, make the effects of this instruction
// unknown.
auto result = skipByMakingEffectsNonConstant(instI);
return {result.first, errorVal};
}

Optional<SymbolicValue>
Expand Down
Loading