diff --git a/frontend/include/chpl/framework/ID.h b/frontend/include/chpl/framework/ID.h index 1e29e499c9eb..da288eda6cd0 100644 --- a/frontend/include/chpl/framework/ID.h +++ b/frontend/include/chpl/framework/ID.h @@ -97,6 +97,13 @@ class ID final { */ int postOrderId() const { return postOrderId_; } + /** + Returns 'true' if this symbol has a 'postOrderId()' value of == -1, + which means this is an ID for something that defines a new symbol + scope. + */ + inline bool isSymbolDefiningScope() const { return postOrderId_ == -1; } + /** Some IDs are introduced during compilation and don't represent something that is directly contained within the source code. diff --git a/frontend/include/chpl/parsing/parsing-queries.h b/frontend/include/chpl/parsing/parsing-queries.h index b94899063c42..96a6a253d5a7 100644 --- a/frontend/include/chpl/parsing/parsing-queries.h +++ b/frontend/include/chpl/parsing/parsing-queries.h @@ -352,6 +352,11 @@ uast::AstTag idToTag(Context* context, ID id); */ bool idIsParenlessFunction(Context* context, ID id); +/** + Returns true if the ID is a nested function. + */ +bool idIsNestedFunction(Context* context, ID id); + /** Returns true if the ID refers to a private declaration. */ diff --git a/frontend/include/chpl/resolution/resolution-queries.h b/frontend/include/chpl/resolution/resolution-queries.h index 2ee2710284e5..a4d459c9deed 100644 --- a/frontend/include/chpl/resolution/resolution-queries.h +++ b/frontend/include/chpl/resolution/resolution-queries.h @@ -233,6 +233,9 @@ ApplicabilityResult instantiateSignature(Context* context, Compute a ResolvedFunction given a TypedFnSignature. Checks the generic cache for potential for reuse. When reuse occurs, the ResolvedFunction might point to a different TypedFnSignature. + + This function will resolve a nested function if it does not refer to + any outer variables. */ const ResolvedFunction* resolveFunction(Context* context, const TypedFnSignature* sig, @@ -260,6 +263,12 @@ const ResolvedFunction* resolveConcreteFunction(Context* context, ID id); */ const ResolvedFunction* scopeResolveFunction(Context* context, ID id); +/** + Compute the set of outer variables referenced by this function. Will return + 'nullptr' if there are no outer variables. + */ +const OuterVariables* computeOuterVariables(Context* context, ID id); + /* * Scope-resolve an AggregateDecl's fields, along with their type expressions * and initialization expressions. diff --git a/frontend/include/chpl/resolution/resolution-types.h b/frontend/include/chpl/resolution/resolution-types.h index 5423f5685ee6..d742c939637c 100644 --- a/frontend/include/chpl/resolution/resolution-types.h +++ b/frontend/include/chpl/resolution/resolution-types.h @@ -341,6 +341,175 @@ class UntypedFnSignature { /// \endcond DO_NOT_DOCUMENT }; +/** + This type represents the outer variables used in a function. It stores + the variables and all their mentions in lexical order. It presents the + concept of a 'reaching variable', which is a reference to an outer + variable that is not defined in the symbol's immediate parent. +*/ +// TODO: We can drop some of this state if we decide we don't care about +// preserving lexical ordering or mentions at all (not 100% sure yet). +class OuterVariables { + + // Record all outer variables used in lexical order. + std::vector variables_; + + // Record all mentions of variables in lexical order. A variable may have + // zero mentions if it was only ever referenced by a child function. In + // this case, we still record the variable so that we can know to propagate + // it into our parent's state. + std::vector mentions_; + + using VarAndMentionIndices = std::pair>; + using IdToVarAndMentionIndices = std::unordered_map; + + // Enables lookup of variables and their mentions given just an ID. The + // first part of the pair is the index of the variable, and the second + // component is the list of mention indices. + IdToVarAndMentionIndices idToVarAndMentionIndices_; + + // The number of outer variables that are defined in distant (not our + // immediate) parents. Only variables defined by a function's most + // immediate parents need to be recorded into its 'TypedFnSignature'. + int numReachingVariables_ = 0; + + // The function that owns this instance. + ID symbol_; + + // The immediate parent of 'symbol_'. So that we can detect if a variable + // is 'reaching' without needing the compiler context. + ID parent_; + + template + static inline bool inBounds(const std::vector v, size_t idx) { + return 0 <= idx && idx < v.size(); + } + +public: + OuterVariables(Context* context, ID symbol) + : symbol_(std::move(symbol)), + parent_(symbol_.parentSymbolId(context)) { + } + + ~OuterVariables() = default; + + bool operator==(const OuterVariables& other) const { + return variables_ == other.variables_ && + mentions_ == other.mentions_ && + idToVarAndMentionIndices_ == other.idToVarAndMentionIndices_ && + numReachingVariables_ == other.numReachingVariables_ && + symbol_ == other.symbol_ && + parent_ == other.parent_; + } + + bool operator!=(const OuterVariables& other) const { + return !(*this == other); + } + + void swap(OuterVariables& other) { + std::swap(variables_, other.variables_); + std::swap(mentions_, other.mentions_); + std::swap(idToVarAndMentionIndices_, other.idToVarAndMentionIndices_); + std::swap(numReachingVariables_, other.numReachingVariables_); + std::swap(symbol_, other.symbol_); + std::swap(parent_, other.parent_); + } + + void mark(Context* context) const { + for (auto& v : variables_) v.mark(context); + for (auto& id : mentions_) id.mark(context); + for (auto& p : idToVarAndMentionIndices_) p.first.mark(context); + symbol_.mark(context); + parent_.mark(context); + } + + static inline bool update(owned& keep, + owned& addin) { + return defaultUpdateOwned(keep, addin); + } + + // Mutating method used to build up state. + void add(Context* context, ID mention, ID var); + + /** Returns 'true' if there are no outer variables. */ + bool isEmpty() const { return numVariables() == 0; } + + /** The total number of outer variables. */ + int numVariables() const { return variables_.size(); } + + /** The number of outer variables declared in our immediate parent. */ + int numImmediateVariables() const { + return numVariables() - numReachingVariables_; + } + + /** The number of outer variables declared in our non-immediate parents. */ + int numReachingVariables() const { return numReachingVariables_; } + + /** The number of outer variable mentions in this symbol's body. */ + int numMentions() const { return mentions_.size(); } + + /** Get the number of mentions for 'var' in this symbol. */ + int numMentions(const ID& var) const { + auto it = idToVarAndMentionIndices_.find(var); + return it != idToVarAndMentionIndices_.end() + ? it->second.second.size() + : 0; + } + + /** Returns 'true' if there is at least one mention of 'var'. */ + bool mentions(const ID& var) const { return numMentions(var) > 0; } + + /** Returns 'true' if this contains an entry for 'var'. */ + bool contains(const ID& var) const { + return idToVarAndMentionIndices_.find(var) != + idToVarAndMentionIndices_.end(); + } + + /** Get the i'th outer variable or the empty ID if 'idx' was out of bounds. */ + ID variable(size_t idx) const { + return inBounds(variables_, idx) ? variables_[idx] : ID(); + } + + /** A reaching variable is declared in a non-immediate parent(s). */ + bool isReachingVariable(const ID& var) const { + auto it = idToVarAndMentionIndices_.find(var); + if (it != idToVarAndMentionIndices_.end()) { + auto& var = variables_[it->second.first]; + return !parent_.contains(var); + } + return false; + } + + /** A reaching variable is declared in a non-immediate parent(s). */ + bool isReachingVariable(size_t idx) const { + if (auto id = variable(idx)) return isReachingVariable(id); + return false; + } + + /** Get the i'th mention in this function. */ + ID mention(size_t idx) const { + return inBounds(mentions_, idx) ? mentions_[idx] : ID(); + } + + /** Get the i'th mention for 'var' within this function, or the empty ID. */ + ID mention(const ID& var, size_t idx) const { + auto it = idToVarAndMentionIndices_.find(var); + if (it == idToVarAndMentionIndices_.end()) return {}; + return inBounds(it->second.second, idx) + ? mentions_[it->second.second[idx]] + : ID(); + } + + /** Get the first mention of 'var', or the empty ID. */ + ID firstMention(const ID& var) const { return mention(var, 0); } + + /** Get the ID of the symbol this instance was created for. */ + const ID& symbol() const { return symbol_; } + + /** Get the ID of the owning symbol's parent. */ + const ID& parent() const { return parent_; } +}; + /** CallInfoActual */ class CallInfoActual { private: diff --git a/frontend/lib/parsing/parsing-queries.cpp b/frontend/lib/parsing/parsing-queries.cpp index 2037058de5c0..9eeb002cf72a 100644 --- a/frontend/lib/parsing/parsing-queries.cpp +++ b/frontend/lib/parsing/parsing-queries.cpp @@ -857,6 +857,14 @@ bool idIsParenlessFunction(Context* context, ID id) { return idIsFunction(context, id) && idIsParenlessFunctionQuery(context, id); } +bool idIsNestedFunction(Context* context, ID id) { + if (id.isEmpty() || !idIsFunction(context, id)) return false; + if (auto up = id.parentSymbolId(context)) { + return idIsFunction(context, up); + } + return false; +} + bool idIsFunction(Context* context, ID id) { // Functions always have their own ID symbol scope, // and if it's not a function, we can return false @@ -984,6 +992,7 @@ const ID& idToParentId(Context* context, ID id) { } const uast::AstNode* parentAst(Context* context, const uast::AstNode* node) { + if (node == nullptr) return nullptr; auto parentId = idToParentId(context, node->id()); if (parentId.isEmpty()) return nullptr; return idToAst(context, parentId); diff --git a/frontend/lib/resolution/Resolver.cpp b/frontend/lib/resolution/Resolver.cpp index 241139ce17c4..3b6f20663b10 100644 --- a/frontend/lib/resolution/Resolver.cpp +++ b/frontend/lib/resolution/Resolver.cpp @@ -226,11 +226,13 @@ Resolver::createForInitializer(Context* context, Resolver Resolver::createForScopeResolvingFunction(Context* context, const Function* fn, - ResolutionResultByPostorderID& byId) { + ResolutionResultByPostorderID& byId, + owned outerVars) { auto ret = Resolver(context, fn, byId, nullptr); ret.typedSignature = nullptr; // re-set below ret.signatureOnly = true; // re-set below ret.scopeResolveOnly = true; + ret.outerVars = std::move(outerVars); ret.fnBody = fn->body(); ret.byPostorder.setupForFunction(fn); @@ -429,6 +431,46 @@ types::QualifiedType Resolver::typeErr(const uast::AstNode* ast, return t; } +static bool isOuterVariable(Resolver* rv, ID target) { + if (target.isEmpty()) return false; + + // E.g., a function, or a class/record/union/enum. We don't need to track + // this, and shouldn't, because its current instantiation may not make any + // sense in this context. As well, these things have an "infinite lifetime", + // and are always reachable. + if (target.isSymbolDefiningScope()) return false; + + + auto parentSymbolId = target.parentSymbolId(rv->context); + + // No match if there is no parent or if the parent is the resolver symbol. + if (parentSymbolId.isEmpty()) return false; + if (rv->symbol && parentSymbolId == rv->symbol->id()) return false; + + switch (parsing::idToTag(rv->context, parentSymbolId)) { + case asttags::Function: return true; + + // Module-scope variables are not considered outer-variables. However, + // variables declared in a module initializer statement can be, e.g., + /** + module M { + if someCondition { + var someVar = 42; + proc f() { writeln(someVar); } + f(); + } + } + */ + case asttags::Module: { + auto targetParentId = parsing::idToParentId(rv->context, target); + return parentSymbolId != targetParentId; + } break; + default: break; + } + + return false; +} + /** Find scopes for superclasses of a class. The passed ID should refer to a Class declaration node. If not, this function will return an empty vector. @@ -2782,6 +2824,13 @@ void Resolver::resolveIdentifier(const Identifier* ident, maybeEmitWarningsForId(this, type, ident, id); + // Record uses of outer variables. + if (isOuterVariable(this, id) && outerVars) { + const ID& mention = ident->id(); + const ID& var = id; + outerVars->add(context, mention, var); + } + if (type.kind() == QualifiedType::TYPE) { // now, for a type that is generic with defaults, // compute the default version when needed. e.g. @@ -2974,10 +3023,33 @@ bool Resolver::enter(const NamedDecl* decl) { } void Resolver::exit(const NamedDecl* decl) { - if (decl->id().postOrderId() < 0) { - // It's a symbol with a different path, e.g. a Function. - // Don't try to resolve it now in this - // traversal. Instead, resolve it e.g. when the function is called. + // We are resolving a symbol with a different path (e.g., a Function or + // a CompositeType declaration). In most cases we do not try to resolve + // in this traversal. However, if we are a nested function and the child + // is also a nested function, we need to check and potentially propagate + // their outer variable set into our own. + auto idChild = decl->id(); + if (idChild.isSymbolDefiningScope()) { + if (this->symbol != nullptr && + parsing::idIsNestedFunction(context, this->symbol->id()) && + parsing::idIsNestedFunction(context, idChild) && + outerVars.get()) { + if (auto ovs = computeOuterVariables(context, idChild)) { + for (int i = 0; i < ovs->numVariables(); i++) { + + // If the variable is reaching in the child function, it means it + // was defined in one of _our_ parent(s). So we need to track it. + if (ovs->isReachingVariable(i)) { + ID var = ovs->variable(i); + + // Mentions from child functions are not recorded in the parent + // function's info, so just use the first (as a convenience). + ID mention = ovs->firstMention(var); + outerVars->add(context, mention, var); + } + } + } + } return; } diff --git a/frontend/lib/resolution/Resolver.h b/frontend/lib/resolution/Resolver.h index 79d5ee9a72ff..bb0c3e87c27c 100644 --- a/frontend/lib/resolution/Resolver.h +++ b/frontend/lib/resolution/Resolver.h @@ -63,6 +63,7 @@ struct Resolver { ReceiverScopesVec savedReceiverScopes; Resolver* parentResolver = nullptr; owned initResolver = nullptr; + owned outerVars; // results of the resolution process @@ -91,7 +92,8 @@ struct Resolver { const PoiScope* poiScope) : context(context), symbol(symbol), poiScope(poiScope), - byPostorder(byPostorder), poiInfo(makePoiInfo(poiScope)) { + byPostorder(byPostorder), + poiInfo(makePoiInfo(poiScope)) { tagTracker.resize(uast::asttags::AstTag::NUM_AST_TAGS); enterScope(symbol); @@ -143,7 +145,8 @@ struct Resolver { // set up Resolver to scope resolve a Function static Resolver createForScopeResolvingFunction(Context* context, const uast::Function* fn, - ResolutionResultByPostorderID& byPostorder); + ResolutionResultByPostorderID& byPostorder, + owned outerVars); static Resolver createForScopeResolvingField(Context* context, const uast::AggregateDecl* ad, diff --git a/frontend/lib/resolution/resolution-queries.cpp b/frontend/lib/resolution/resolution-queries.cpp index 4d8c838a960b..880840b8cb22 100644 --- a/frontend/lib/resolution/resolution-queries.cpp +++ b/frontend/lib/resolution/resolution-queries.cpp @@ -1754,11 +1754,11 @@ ApplicabilityResult instantiateSignature(Context* context, const TypedFnSignature* parentFnTyped = nullptr; if (sig->parentFn()) { - CHPL_UNIMPL("generic child functions not yet supported"); - return ApplicabilityResult::failure(sig->id(), FAIL_CANDIDATE_OTHER); - // TODO: how to compute parentFn for the instantiation? - // Does the parent function need to be instantiated in some case? - // Set parentFnTyped somehow. + if (nullptr != computeOuterVariables(context, sig->id())) { + CHPL_UNIMPL("generic child functions that refer to outer variables " + "are not yet supported"); + return ApplicabilityResult::failure(sig->id(), FAIL_CANDIDATE_OTHER); + } } auto faMap = FormalActualMap(sig, call); @@ -2379,6 +2379,8 @@ const TypedFnSignature* inferRefMaybeConstFormals(Context* context, const ResolvedFunction* resolveFunction(Context* context, const TypedFnSignature* sig, const PoiScope* poiScope) { + // If there were outer variables, then do not bother trying to resolve. + if (computeOuterVariables(context, sig->id())) return nullptr; return helpResolveFunction(context, sig, poiScope, /* skipIfRunning */ false); } @@ -2386,6 +2388,9 @@ const ResolvedFunction* resolveConcreteFunction(Context* context, ID id) { if (id.isEmpty()) return nullptr; + // If there were outer variables, then do not bother trying to resolve. + if (computeOuterVariables(context, id)) return nullptr; + const UntypedFnSignature* uSig = UntypedFnSignature::get(context, id); const TypedFnSignature* sig = typedSignatureInitial(context, uSig); @@ -2403,20 +2408,29 @@ const ResolvedFunction* resolveConcreteFunction(Context* context, ID id) { return ret; } -static const owned& -scopeResolveFunctionQuery(Context* context, ID id) { - QUERY_BEGIN(scopeResolveFunctionQuery, context, id); +// This should be set when performing 'scopeResolveFunction', below. +static const owned& +computeOuterVariablesQuery(Context* context, ID id) { + QUERY_BEGIN(computeOuterVariablesQuery, context, id); + owned ret; + CHPL_ASSERT(false && "Should not be called directly!"); + return QUERY_END(ret); +} +static owned +scopeResolveFunctionQueryBody(Context* context, ID id) { const AstNode* ast = parsing::idToAst(context, id); const Function* fn = ast->toFunction(); ResolutionResultByPostorderID resolutionById; const TypedFnSignature* sig = nullptr; owned result; + owned outerVars = toOwned(new OuterVariables(context, id)); if (fn) { auto visitor = - Resolver::createForScopeResolvingFunction(context, fn, resolutionById); + Resolver::createForScopeResolvingFunction(context, fn, resolutionById, + std::move(outerVars)); // visit the children of fn to scope resolve // (visiting the children because visiting a function will not @@ -2435,14 +2449,53 @@ scopeResolveFunctionQuery(Context* context, ID id) { checkForParenlessMethodFieldRedefinition(context, fn, visitor); sig = visitor.typedSignature; + + if (!visitor.outerVars->isEmpty()) { + std::swap(outerVars, visitor.outerVars); + } } + QUERY_STORE_RESULT(computeOuterVariablesQuery, + context, + std::move(outerVars), + id); + result = toOwned(new ResolvedFunction(sig, fn->returnIntent(), std::move(resolutionById), PoiInfo(), QualifiedType())); + return result; +} - return QUERY_END(result); +static const owned& +scopeResolveFunctionQuery(Context* context, ID id) { + QUERY_BEGIN(scopeResolveFunctionQuery, context, id); + auto ret = scopeResolveFunctionQueryBody(context, id); + return QUERY_END(ret); +} + +const OuterVariables* computeOuterVariables(Context* context, ID id) { + + // For now, preemptively return 'nullptr' if 'id' is not a function. + if (!parsing::idIsNestedFunction(context, id)) return nullptr; + + if (!context->hasCurrentResultForQuery(computeOuterVariablesQuery, { id })) { + if (!context->isQueryRunning(scopeResolveFunctionQuery, { id })) { + + // The 'computeOuterVariablesQuery' is set as a side effect of + // performing scope resolution, since both require a traversal. + std::ignore = scopeResolveFunction(context, id); + } else { + + // Just return 'nullptr', we have no results to use, yet. The caller + // can only be the Resolver set up for scope-resolve if this branch + // is happening. + return nullptr; + } + } + + // We should have a result at this point. + return computeOuterVariablesQuery(context, id).get(); } const ResolvedFunction* scopeResolveFunction(Context* context, diff --git a/frontend/lib/resolution/resolution-types.cpp b/frontend/lib/resolution/resolution-types.cpp index b6f843f5b76a..2a2df4bcce5b 100644 --- a/frontend/lib/resolution/resolution-types.cpp +++ b/frontend/lib/resolution/resolution-types.cpp @@ -40,6 +40,33 @@ namespace resolution { using namespace uast; using namespace types; +void OuterVariables::add(Context* context, ID mention, ID var) { + ID mentionParent = mention.parentSymbolId(context); + ID symbolParent = symbol_.parentSymbolId(context); + ID varParent = var.parentSymbolId(context); + const bool isReachingUse = symbolParent != varParent; + const bool isChildUse = mentionParent != symbol_; + + CHPL_ASSERT(varParent != symbol_); + if (!isReachingUse) { + CHPL_ASSERT(mention && symbol_.contains(mention)); + } + + auto it = idToVarAndMentionIndices_.find(var); + if (it == idToVarAndMentionIndices_.end()) { + auto p = std::make_pair(variables_.size(), std::vector()); + it = idToVarAndMentionIndices_.emplace_hint(it, var, std::move(p)); + variables_.push_back(var); + if (isReachingUse) numReachingVariables_++; + } + + // Don't bother storing the mention for a child use. + if (!isChildUse) { + it->second.second.push_back(mentions_.size()); + mentions_.push_back(mention); + } +} + const owned& UntypedFnSignature::getUntypedFnSignature(Context* context, ID id, UniqueString name, diff --git a/frontend/test/resolution/CMakeLists.txt b/frontend/test/resolution/CMakeLists.txt index f3e37aa48b88..c4d96bc28a9f 100644 --- a/frontend/test/resolution/CMakeLists.txt +++ b/frontend/test/resolution/CMakeLists.txt @@ -52,6 +52,7 @@ comp_unit_test(testMethodCalls) comp_unit_test(testMethodFieldAccess) comp_unit_test(testModuleInitOrder) comp_unit_test(testMultiDecl) +comp_unit_test(testNestedFunctions) comp_unit_test(testNew) comp_unit_test(testOpaqueExternTypes) comp_unit_test(testOperatorOverloads) diff --git a/frontend/test/resolution/testNestedFunctions.cpp b/frontend/test/resolution/testNestedFunctions.cpp new file mode 100644 index 000000000000..46d6068b5d25 --- /dev/null +++ b/frontend/test/resolution/testNestedFunctions.cpp @@ -0,0 +1,203 @@ +/* + * Copyright 2021-2024 Hewlett Packard Enterprise Development LP + * Other additional copyright holders may be indicated within. + * + * The entirety of this work is licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "test-resolution.h" + +#include "chpl/parsing/parsing-queries.h" +#include "chpl/resolution/resolution-queries.h" +#include "chpl/resolution/scope-queries.h" +#include "chpl/types/all-types.h" +#include "chpl/uast/all-uast.h" +#include "./ErrorGuard.h" + +#define TEST_NAME(ctx__)\ + chpl::UniqueString::getConcat(ctx__, __FUNCTION__, ".chpl") + +static Context* +turnOnWarnUnstable(Context* ctx) { + CompilerFlags flags; + flags.set(CompilerFlags::WARN_UNSTABLE, true); + setCompilerFlags(ctx, std::move(flags)); + assert(isCompilerFlagSet(ctx, CompilerFlags::WARN_UNSTABLE)); + return ctx; +} + +static void test0(void) { + Context context; + Context* ctx = turnOnWarnUnstable(&context); + ErrorGuard guard(ctx); + + auto path = TEST_NAME(ctx); + std::cout << path.c_str() << std::endl; + + std::string contents = + R""""( + var global = 0; + proc foo() { + var x = 0; var y = 0; var z = 0; + proc bar() { + var a = 0; var b = 0; + x; y; z; + proc baz() { + a; b; x; y; z; global; + a; b; x; y; z; global; + return 0; + } + return baz(); + } + return bar(); + } + )""""; + + setFileText(ctx, path, contents); + + // Get the top module. + auto& br = parseAndReportErrors(ctx, path); + assert(br.numTopLevelExpressions() == 1); + auto mod = br.topLevelExpression(0)->toModule(); + assert(mod && mod->numStmts() == 2); + auto globalVar = mod->stmt(0)->toVariable(); + + // Get the 'foo' function and its declared variables. + auto fooFn = mod->stmt(1)->toFunction(); + assert(fooFn && fooFn->numStmts() == 5); + auto xVar = fooFn->stmt(0)->toVariable(); + auto yVar = fooFn->stmt(1)->toVariable(); + auto zVar = fooFn->stmt(2)->toVariable(); + assert(xVar && yVar && zVar); + + // Get the 'bar' function and its declared variables. + auto barFn = fooFn->stmt(3)->toFunction(); + assert(barFn && barFn->numStmts() == 7); + auto aVar = barFn->stmt(0)->toVariable(); + auto bVar = barFn->stmt(1)->toVariable(); + + // Get the 'baz' function. + auto bazFn = barFn->stmt(5)->toFunction(); + assert(bazFn && bazFn->numStmts() == 13); + + // The 'foo' function should have no outer variables. + assert(computeOuterVariables(ctx, fooFn->id()) == nullptr); + + auto ovsBarPtr = computeOuterVariables(ctx, barFn->id()); + assert(ovsBarPtr); + auto& ovsBar = *ovsBarPtr; + assert(ovsBar.numVariables() == 3); + assert(ovsBar.numMentions() == 3); + + assert(ovsBar.numMentions(xVar->id()) == 1); + assert(ovsBar.firstMention(xVar->id()) == barFn->stmt(2)->id()); + + assert(ovsBar.numMentions(yVar->id()) == 1); + assert(ovsBar.firstMention(yVar->id()) == barFn->stmt(3)->id()); + + assert(ovsBar.numMentions(zVar->id()) == 1); + assert(ovsBar.firstMention(zVar->id()) == barFn->stmt(4)->id()); + + // Not reaching as declared directly in parent. + assert(!ovsBar.isReachingVariable(xVar->id())); + assert(!ovsBar.isReachingVariable(yVar->id())); + assert(!ovsBar.isReachingVariable(zVar->id())); + + auto ovsBazPtr = computeOuterVariables(ctx, bazFn->id()); + assert(ovsBazPtr); + auto& ovsBaz = *ovsBazPtr; + assert(ovsBaz.numVariables() == 5); + assert(ovsBaz.numMentions() == 10); + + assert(ovsBaz.variable(0) == aVar->id()); + assert(ovsBaz.numMentions(aVar->id()) == 2); + assert(ovsBaz.mention(aVar->id(), 0) == bazFn->stmt(0)->id()); + assert(ovsBaz.mention(aVar->id(), 1) == bazFn->stmt(6)->id()); + + assert(ovsBaz.variable(1) == bVar->id()); + assert(ovsBaz.numMentions(bVar->id()) == 2); + assert(ovsBaz.mention(bVar->id(), 0) == bazFn->stmt(1)->id()); + assert(ovsBaz.mention(bVar->id(), 1) == bazFn->stmt(7)->id()); + + assert(ovsBaz.variable(2) == xVar->id()); + assert(ovsBaz.numMentions(xVar->id()) == 2); + assert(ovsBaz.mention(xVar->id(), 0) == bazFn->stmt(2)->id()); + assert(ovsBaz.mention(xVar->id(), 1) == bazFn->stmt(8)->id()); + + assert(ovsBaz.variable(3) == yVar->id()); + assert(ovsBaz.numMentions(yVar->id()) == 2); + assert(ovsBaz.mention(yVar->id(), 0) == bazFn->stmt(3)->id()); + assert(ovsBaz.mention(yVar->id(), 1) == bazFn->stmt(9)->id()); + + assert(ovsBaz.variable(4) == zVar->id()); + assert(ovsBaz.numMentions(zVar->id()) == 2); + assert(ovsBaz.mention(zVar->id(), 0) == bazFn->stmt(4)->id()); + assert(ovsBaz.mention(zVar->id(), 1) == bazFn->stmt(10)->id()); + + // Reaching because 'baz' has to reach across 'bar' into 'foo'. + assert(ovsBaz.isReachingVariable(xVar->id())); + assert(ovsBaz.isReachingVariable(yVar->id())); + assert(ovsBaz.isReachingVariable(zVar->id())); + + // Module variables are not recorded as outer variables at the moment. + assert(!ovsBaz.mentions(globalVar->id())); + assert(ovsBaz.numMentions(globalVar->id()) == 0); + assert(!guard.realizeErrors()); +} + +// This test demonstrates that it is safe to resolve a nested function like +// normal if the function does not refer to outer variables, regardless of +// whether or not the function is generic or concrete. +static void test1(void) { + Context context; + Context* ctx = turnOnWarnUnstable(&context); + ErrorGuard guard(ctx); + + auto path = TEST_NAME(ctx); + std::cout << path.c_str() << std::endl; + + // This snippet is a mockup of code from the internal modules. + std::string program = + R""""( + proc isEnumType(type t) param { + proc isEnumHelp(type t: enum) param do return true; + proc isEnumHelp(type t) param do return false; + return isEnumHelp(t); + } + + enum colors { red, green, blue } + + proc foo(arg) { + if isEnumType(arg.type) { + return 42; + } else { + return "hello"; + } + } + + var color = colors.red; + var x = foo(color); + )""""; + + auto qt = resolveQualifiedTypeOfX(ctx, program); + assert(qt.kind() == QualifiedType::VAR); + assert(qt.type() && qt.type()->isIntType()); +} + +int main() { + test0(); + test1(); + return 0; +}