Skip to content
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
39 changes: 39 additions & 0 deletions include/slang/ast/Compilation.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
//------------------------------------------------------------------------------
#pragma once

#include <deque>
#include <memory>
#include <mutex>

#include "slang/ast/ASTDiagMap.h"
#include "slang/ast/InstanceCacheKey.h"
Expand Down Expand Up @@ -749,6 +751,29 @@ class SLANG_EXPORT Compilation : public BumpAllocator {
return constantAllocator.emplace(std::move(value));
}

/// @}
/// @name Constant function call memoization
/// @{

/// Looks up the memoized result of an earlier top-level call of the constant
/// function @a subroutine with identical @a args and evaluation flags.
/// On a hit, copies the result to @a result, sets @a steps to the number of
/// evaluation steps the original call took, and returns true.
bool findConstCallResult(const SubroutineSymbol& subroutine, uint8_t evalFlags,
uint64_t astFlags, std::span<const ConstantValue> args,
size_t argsHash, ConstantValue& result, uint32_t& steps) const;

/// Records the result of a top-level call of the constant function @a subroutine,
/// for later lookup via @a findConstCallResult. The caller guarantees that the
/// evaluation produced no diagnostics and did not depend on its call site.
void addConstCallResult(const SubroutineSymbol& subroutine, uint8_t evalFlags,
uint64_t astFlags, std::span<const ConstantValue> args, size_t argsHash,
const ConstantValue& result, uint32_t steps);

/// @}
/// @name Allocation functions
/// @{

/// Allocates a symbol map.
SymbolMap* allocSymbolMap() {
SLANG_ASSERT(!isFrozen());
Expand Down Expand Up @@ -896,6 +921,20 @@ class SLANG_EXPORT Compilation : public BumpAllocator {
flat_hash_map<std::tuple<std::string_view, SymbolKind>, std::shared_ptr<SystemSubroutine>>
methodMap;

// Memoized results of top-level constant function calls, keyed by a hash of
// the subroutine, the evaluation flags and the argument values.
struct ConstCallEntry {
const SubroutineSymbol* subroutine;
uint8_t evalFlags;
uint64_t astFlags;
std::vector<ConstantValue> args;
ConstantValue result;
uint32_t steps;
};
mutable std::mutex constCallMutex;
std::deque<ConstCallEntry> constCallEntries;
flat_hash_map<size_t, SmallVector<const ConstCallEntry*, 2>> constCallMap;

// Map from instance bodies to side effects applied by them.
flat_hash_map<const Symbol*, std::unique_ptr<InstanceSideEffects>> instanceSideEffectMap;

Expand Down
27 changes: 27 additions & 0 deletions include/slang/ast/EvalContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,31 @@ class SLANG_EXPORT EvalContext {
/// Returns nullptr if there is no queue target active.
const ConstantValue* getQueueTarget() const { return queueTarget; }

/// Gets the number of steps (statements) executed so far.
uint32_t getSteps() const { return steps; }

/// Accounts for @a count steps executed elsewhere on behalf of this context,
/// e.g. by a memoized constant function call. The caller must have checked
/// that the step limit is not reached.
void addSteps(uint32_t count) { steps += count; }

/// Gets the depth of the call stack.
size_t getStackDepth() const { return stack.size(); }

/// Gets the total number of diagnostics ever recorded by this context.
/// Unlike the stored diagnostics, this count never decreases.
uint32_t getDiagCount() const { return diagCount; }

/// Returns true if there are any lvalues on the lvalue stack.
bool hasLValues() const { return !lvalStack.empty(); }

/// Records that the evaluation of the outermost function call on the stack
/// depends on the lookup location of its call site.
void noteCallSiteDependence() { callSiteDependent = true; }

/// Gets and clears the call site dependence flag (see @a noteCallSiteDependence).
bool takeCallSiteDependence() { return std::exchange(callSiteDependent, false); }

/// Dumps the contents of the call stack to a string for debugging.
std::string dumpStack() const;

Expand Down Expand Up @@ -191,7 +216,9 @@ class SLANG_EXPORT EvalContext {
Diagnostics diags;
Diagnostics warnings;
SourceRange disableRange;
uint32_t diagCount = 0;
bool backtraceReported = false;
bool callSiteDependent = false;
};

} // namespace slang::ast
4 changes: 4 additions & 0 deletions include/slang/ast/expressions/CallExpression.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ class SLANG_EXPORT CallExpression final : public Expression {
static bool checkConstant(EvalContext& context, const SubroutineSymbol& subroutine,
SourceRange range);

bool isMemoizableCall(const EvalContext& context, const SubroutineSymbol& symbol) const;
ConstantValue evalCall(EvalContext& context, const SubroutineSymbol& symbol,
std::span<const ConstantValue> args) const;

const Expression* thisClass_;
std::span<const Expression*> arguments_;
LookupLocation lookupLocation;
Expand Down
89 changes: 89 additions & 0 deletions source/ast/Compilation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "ElabVisitors.h"
#include "builtins/Builtins.h"
#include <bit>
#include <fmt/core.h>
#include <mutex>

Expand Down Expand Up @@ -1693,6 +1694,94 @@ Diagnostic& Compilation::addDiag(Diagnostic diag) {
return result;
}

// Value identity for memoization keys: unlike operator==, integers must also
// agree on width and signedness, and reals on their exact bit patterns.
static bool identicalValues(const ConstantValue& lhs, const ConstantValue& rhs) {
auto& lv = lhs.getVariant();
auto& rv = rhs.getVariant();
if (lv.index() != rv.index())
return false;

auto identicalRange = [](auto&& l, auto&& r) {
return std::ranges::equal(l, r, [](const ConstantValue& a, const ConstantValue& b) {
return identicalValues(a, b);
});
};

if (lhs.isInteger()) {
auto& l = lhs.integer();
auto& r = rhs.integer();
return l.getBitWidth() == r.getBitWidth() && l.isSigned() == r.isSigned() &&
l.hasUnknown() == r.hasUnknown() && exactlyEqual(l, r);
}
if (lhs.isReal())
return std::bit_cast<uint64_t>(double(lhs.real())) ==
std::bit_cast<uint64_t>(double(rhs.real()));
if (lhs.isShortReal())
return std::bit_cast<uint32_t>(float(lhs.shortReal())) ==
std::bit_cast<uint32_t>(float(rhs.shortReal()));
if (lhs.isUnpacked())
return identicalRange(lhs.elements(), rhs.elements());
if (lhs.isString())
return lhs.str() == rhs.str();
if (lhs.isMap()) {
auto& l = *lhs.map();
auto& r = *rhs.map();
return l.size() == r.size() && identicalValues(l.defaultValue, r.defaultValue) &&
std::ranges::equal(l, r, [](auto& a, auto& b) {
return identicalValues(a.first, b.first) && identicalValues(a.second, b.second);
});
}
if (lhs.isQueue()) {
auto& l = *lhs.queue();
auto& r = *rhs.queue();
return l.maxBound == r.maxBound && identicalRange(l, r);
}
if (lhs.isUnion()) {
auto& l = *lhs.unionVal();
auto& r = *rhs.unionVal();
return l.activeMember == r.activeMember && identicalValues(l.value, r.value);
}

// Bad, null and unbounded values carry no payload.
return true;
}

bool Compilation::findConstCallResult(const SubroutineSymbol& subroutine, uint8_t evalFlags,
uint64_t astFlags, std::span<const ConstantValue> args,
size_t argsHash, ConstantValue& result,
uint32_t& steps) const {
std::unique_lock<std::mutex> lock(constCallMutex);
auto it = constCallMap.find(argsHash);
if (it == constCallMap.end())
return false;

for (auto entry : it->second) {
if (entry->subroutine == &subroutine && entry->evalFlags == evalFlags &&
entry->astFlags == astFlags && std::ranges::equal(entry->args, args, identicalValues)) {
result = entry->result;
steps = entry->steps;
return true;
}
}
return false;
}

void Compilation::addConstCallResult(const SubroutineSymbol& subroutine, uint8_t evalFlags,
uint64_t astFlags, std::span<const ConstantValue> args,
size_t argsHash, const ConstantValue& result, uint32_t steps) {
// Bound the memory spent on the memo; the limit only costs speed, never results.
static constexpr size_t MaxEntries = 1 << 16;

std::unique_lock<std::mutex> lock(constCallMutex);
if (constCallEntries.size() >= MaxEntries)
return;

auto& entry = constCallEntries.emplace_back(ConstCallEntry{
&subroutine, evalFlags, astFlags, {args.begin(), args.end()}, result, steps});
constCallMap[argsHash].push_back(&entry);
}

AssertionInstanceDetails* Compilation::allocAssertionDetails() {
SLANG_ASSERT(!isFrozen());
return assertionDetailsAllocator.emplace();
Expand Down
4 changes: 4 additions & 0 deletions source/ast/EvalContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ void EvalContext::reset() {
diags.clear();
warnings.clear();
disableRange = {};
diagCount = 0;
backtraceReported = false;
callSiteDependent = false;
}

ConstantValue* EvalContext::createLocal(const ValueSymbol* symbol, ConstantValue value) {
Expand Down Expand Up @@ -133,13 +135,15 @@ Diagnostics EvalContext::getAllDiagnostics() const {
}

Diagnostic& EvalContext::addDiag(DiagCode code, SourceLocation location) {
diagCount++;
const bool isError = getDefaultSeverity(code) >= DiagnosticSeverity::Error;
auto& diag = isError ? diags.add(code, location) : warnings.add(code, location);
reportStack(diag);
return diag;
}

Diagnostic& EvalContext::addDiag(DiagCode code, SourceRange range) {
diagCount++;
const bool isError = getDefaultSeverity(code) >= DiagnosticSeverity::Error;
auto& diag = isError ? diags.add(code, range) : warnings.add(code, range);
reportStack(diag);
Expand Down
72 changes: 72 additions & 0 deletions source/ast/expressions/CallExpression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,78 @@ ConstantValue CallExpression::evalImpl(EvalContext& context) const {
args.emplace_back(std::move(v));
}

// A top-level call (e.g. from a parameter initializer) of a function that is
// repeatedly called with the same arguments -- typically once per instance of
// the same module -- is evaluated only once; see isMemoizableCall.
if (isMemoizableCall(context, symbol)) {
auto& comp = context.getCompilation();
const uint8_t evalFlags = (context.flags & ~EvalFlags::CacheResults).bits();
const uint64_t astFlags = context.astCtx.flags.bits();
size_t argsHash = size_t(&symbol);
hash_combine(argsHash, evalFlags, astFlags);
for (auto& arg : args)
hash_combine(argsHash, arg.hash());

ConstantValue memoResult;
uint32_t memoSteps = 0;
if (comp.findConstCallResult(symbol, evalFlags, astFlags, args, argsHash, memoResult,
memoSteps)) {
// Replaying the step count keeps the step limit exact. If the call
// would hit the limit, evaluate it for real so that it fails the same way.
if (uint64_t(context.getSteps()) + memoSteps < comp.getOptions().maxConstexprSteps) {
context.addSteps(memoSteps);
return memoResult;
}
return evalCall(context, symbol, args);
}

const uint32_t stepsBefore = context.getSteps();
const uint32_t diagsBefore = context.getDiagCount();
context.takeCallSiteDependence();

ConstantValue result = evalCall(context, symbol, args);

// Only remember calls that are expensive enough to be worth it and whose
// evaluation is fully determined by the subroutine and argument values:
// no diagnostics (those must be issued at every call site) and no
// dependence on the call site's lookup location.
static constexpr uint32_t MinMemoSteps = 64;
const uint32_t steps = context.getSteps() - stepsBefore;
if (result && steps >= MinMemoSteps && context.getDiagCount() == diagsBefore &&
!context.takeCallSiteDependence()) {
comp.addConstCallResult(symbol, evalFlags, astFlags, args, argsHash, result, steps);
}
return result;
}

return evalCall(context, symbol, args);
}

bool CallExpression::isMemoizableCall(const EvalContext& context,
const SubroutineSymbol& symbol) const {
// Only calls made outside of any function body, with no evaluation state
// that the callee could observe.
if (context.inFunction() || context.getQueueTarget() || context.hasLValues())
return false;

// Class methods can read static class properties; tasks and subroutines with
// output arguments have effects beyond their return value.
if (symbol.subroutineKind != SubroutineKind::Function || symbol.thisVar ||
symbol.hasOutputArgs() ||
symbol.getParentScope()->asSymbol().kind == SymbolKind::ClassType) {
return false;
}

// A parameter referenced by the function must be declared before its call
// site (see NamedValueExpression::checkConstant). That holds at every call
// site for package parameters, unless the call is made from within a package;
// any other parameter reference marks the evaluation as call site dependent.
auto scope = lookupLocation.getScope();
return scope && scope->asSymbol().kind != SymbolKind::Package;
}

ConstantValue CallExpression::evalCall(EvalContext& context, const SubroutineSymbol& symbol,
std::span<const ConstantValue> args) const {
// Push a new stack frame, push argument values as locals.
if (!context.pushFrame(symbol, sourceRange.start(), lookupLocation))
return nullptr;
Expand Down
9 changes: 8 additions & 1 deletion source/ast/expressions/MiscExpressions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,16 @@ bool NamedValueExpression::checkConstant(EvalContext& context) const {
// of the constant function. If the two locations are not in the same compilation
// unit, assume that it's ok. Also if the reference is via a package import
// that's fine too.
auto scope = symbol.getParentScope();
if (context.getStackDepth() == 1 &&
(!scope || scope->asSymbol().kind != SymbolKind::Package)) {
// The outcome depends on where the outermost call was made, so its
// result can't be reused at other call sites (see CallExpression::evalImpl).
context.noteCallSiteDependence();
}

auto compare = symbol.isDeclaredBefore(frame.lookupLocation);
if (!compare.value_or(true)) {
auto scope = symbol.getParentScope();
if (!scope || scope->asSymbol().kind != SymbolKind::Package ||
scope == frame.lookupLocation.getScope()) {

Expand Down
Loading
Loading