From 0abc071adf4e9b8966039196033a5c5093bfe2a4 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Sat, 30 Mar 2024 22:05:24 +0800 Subject: [PATCH] libnixf: variable lookups (#372) --- .../include/nixf/Basic/DiagnosticKinds.inc | 37 +- libnixf/include/nixf/Basic/Nodes/Expr.h | 1 + libnixf/include/nixf/Basic/Nodes/Lambda.h | 2 +- libnixf/include/nixf/Sema/VariableLookup.h | 126 ++++++ libnixf/meson.build | 2 + libnixf/src/Sema/VariableLookup.cpp | 388 ++++++++++++++++++ libnixf/test/Sema/VariableLookup.cpp | 128 ++++++ 7 files changed, 672 insertions(+), 12 deletions(-) create mode 100644 libnixf/include/nixf/Sema/VariableLookup.h create mode 100644 libnixf/src/Sema/VariableLookup.cpp create mode 100644 libnixf/test/Sema/VariableLookup.cpp diff --git a/libnixf/include/nixf/Basic/DiagnosticKinds.inc b/libnixf/include/nixf/Basic/DiagnosticKinds.inc index 15a9f684f..e26a8d8c8 100644 --- a/libnixf/include/nixf/Basic/DiagnosticKinds.inc +++ b/libnixf/include/nixf/Basic/DiagnosticKinds.inc @@ -9,12 +9,17 @@ DIAG("lex-float-no-exp", FloatNoExp, Fatal, DIAG("lex-float-leading-zero", FloatLeadingZero, Warning, "float begins with extra zeros `{}` is nixf extension") DIAG("parse-expected", Expected, Error, "expected {}") -DIAG("parse-attrpath-extra-dot", AttrPathExtraDot, Error, "extra `.` at the end of attrpath") -DIAG("parse-select-extra-dot", SelectExtraDot, Error, "extra `.` after expression, but missing attrpath") -DIAG("parse-unexpected-between", UnexpectedBetween, Error, "unexpected {} between {} and {}") +DIAG("parse-attrpath-extra-dot", AttrPathExtraDot, Error, + "extra `.` at the end of attrpath") +DIAG("parse-select-extra-dot", SelectExtraDot, Error, + "extra `.` after expression, but missing attrpath") +DIAG("parse-unexpected-between", UnexpectedBetween, Error, + "unexpected {} between {} and {}") DIAG("parse-unexpected", UnexpectedText, Error, "unexpected text") -DIAG("parse-missing-sep-formals", MissingSepFormals, Error, "missing seperator `,` between two lambda formals") -DIAG("parse-lambda-arg-extra-at", LambdaArgExtraAt, Error, "extra `@` for lambda arg") +DIAG("parse-missing-sep-formals", MissingSepFormals, Error, + "missing seperator `,` between two lambda formals") +DIAG("parse-lambda-arg-extra-at", LambdaArgExtraAt, Error, + "extra `@` for lambda arg") DIAG("let-dynamic", LetDynamic, Error, "dynamic attributes are not allowed in let ... in ... expression") DIAG("empty-inherit", EmptyInherit, Warning, "empty inherit expression") @@ -33,15 +38,25 @@ DIAG("merge-diff-rec", MergeDiffRec, Warning, DIAG("bison", BisonParse, Fatal, "{}") DIAG("invalid-float", InvalidFloat, Fatal, "invalid float {}") DIAG("invalid-integer", InvalidInteger, Fatal, "invalid integer {}") -DIAG("sema-duplicated-attrname", DuplicatedAttrName, Error, "duplicated attrname `{}`") +DIAG("sema-duplicated-attrname", DuplicatedAttrName, Error, + "duplicated attrname `{}`") DIAG("sema-dynamic-inherit", DynamicInherit, Error, "dynamic attributes are not allowed in inherit") DIAG("sema-empty-formal", EmptyFormal, Error, "empty formal") -DIAG("sema-formal-missing-comma", FormalMissingComma, Error, "missing `,` for lambda formal") -DIAG("sema-formal-extra-ellipsis", FormalExtraEllipsis, Error, "extra `...` for lambda formal") -DIAG("sema-misplaced-ellipsis", FormalMisplacedEllipsis, Error, "misplaced `...` for lambda formal") -DIAG("sema-dup-formal", DuplicatedFormal, Error, - "duplicated function formal") +DIAG("sema-formal-missing-comma", FormalMissingComma, Error, + "missing `,` for lambda formal") +DIAG("sema-formal-extra-ellipsis", FormalExtraEllipsis, Error, + "extra `...` for lambda formal") +DIAG("sema-misplaced-ellipsis", FormalMisplacedEllipsis, Error, + "misplaced `...` for lambda formal") +DIAG("sema-dup-formal", DuplicatedFormal, Error, "duplicated function formal") DIAG("sema-dup-formal-arg", DuplicatedFormalToArg, Error, "function argument duplicated to a function formal") +DIAG("sema-undefined-variable", UndefinedVariable, Error, + "undefined variable `{}`") +DIAG("sema-def-not-used", DefinitionNotUsed, Warning, + "definition `{}` is not used") +DIAG("sema-extra-rec", ExtraRecursive, Warning, + "attrset is not necessary to be `rec`ursive ") +DIAG("sema-extra-with", ExtraWith, Warning, "unused `with` expression") #endif // DIAG diff --git a/libnixf/include/nixf/Basic/Nodes/Expr.h b/libnixf/include/nixf/Basic/Nodes/Expr.h index b22c60b10..b5c1437cc 100644 --- a/libnixf/include/nixf/Basic/Nodes/Expr.h +++ b/libnixf/include/nixf/Basic/Nodes/Expr.h @@ -148,6 +148,7 @@ class ExprLet : public Expr { [[nodiscard]] const Binds *binds() const { return Attrs ? Attrs->binds() : nullptr; } + [[nodiscard]] const ExprAttrs *attrs() const { return Attrs.get(); } [[nodiscard]] const Expr *expr() const { return E.get(); } [[nodiscard]] const Misc &let() const { return *KwLet; } [[nodiscard]] const Misc *in() const { return KwIn.get(); } diff --git a/libnixf/include/nixf/Basic/Nodes/Lambda.h b/libnixf/include/nixf/Basic/Nodes/Lambda.h index 213d704a7..4ca8dc91b 100644 --- a/libnixf/include/nixf/Basic/Nodes/Lambda.h +++ b/libnixf/include/nixf/Basic/Nodes/Lambda.h @@ -96,7 +96,7 @@ class LambdaArg : public Node { std::shared_ptr F) : Node(NK_LambdaArg, Range), ID(std::move(ID)), F(std::move(F)) {} - [[nodiscard]] Identifier *id() { return ID.get(); } + [[nodiscard]] Identifier *id() const { return ID.get(); } [[nodiscard]] Formals *formals() const { return F.get(); } diff --git a/libnixf/include/nixf/Sema/VariableLookup.h b/libnixf/include/nixf/Sema/VariableLookup.h new file mode 100644 index 000000000..029b8a02e --- /dev/null +++ b/libnixf/include/nixf/Sema/VariableLookup.h @@ -0,0 +1,126 @@ +/// \file +/// \brief Lookup variable names, from it's parent scope. +/// +/// This file declares a variable-lookup analysis on AST. +/// We do variable lookup for liveness checking, and emit diagnostics +/// like "unused with", or "undefined variable". +/// The implementation aims to be consistent with C++ nix (NixOS/nix). + +#pragma once + +#include "nixf/Basic/Diagnostic.h" +#include "nixf/Basic/Nodes/Attrs.h" +#include "nixf/Basic/Nodes/Basic.h" +#include "nixf/Basic/Nodes/Expr.h" +#include "nixf/Basic/Nodes/Lambda.h" +#include "nixf/Basic/Nodes/Simple.h" + +#include +#include +#include +#include + +namespace nixf { + +/// \brief Represents a definition +class Definition { + std::vector Uses; + const Node *Syntax; + +public: + explicit Definition(const Node *Syntax) : Syntax(Syntax) {} + Definition(std::vector Uses, const Node *Syntax) + : Uses(std::move(Uses)), Syntax(Syntax) {} + + [[nodiscard]] const Node *syntax() const { return Syntax; } + + [[nodiscard]] const std::vector &uses() const { + return Uses; + } + + void usedBy(const ExprVar &User) { Uses.emplace_back(&User); } + + [[nodiscard]] bool isBuiltin() const { return Syntax; } +}; + +/// \brief A set of variable definitions, which may inherit parent environment. +class EnvNode { +public: + using DefMap = std::map>; + +private: + const std::shared_ptr Parent; // Points to the parent node. + + DefMap Defs; // Definitions. + + const Node *Syntax; + +public: + EnvNode(std::shared_ptr Parent, DefMap Defs, const Node *Syntax) + : Parent(std::move(Parent)), Defs(std::move(Defs)), Syntax(Syntax) {} + + [[nodiscard]] EnvNode *parent() const { return Parent.get(); } + + /// \brief Where this node comes from. + [[nodiscard]] const Node *syntax() const { return Syntax; } + + [[nodiscard]] bool isWith() const { + return Syntax && Syntax->kind() == Node::NK_ExprWith; + } + + [[nodiscard]] const DefMap &defs() const { return Defs; } + + [[nodiscard]] bool isLive() const; +}; + +class VariableLookupAnalysis { +public: + enum class LookupResultKind { + Undefined, + FromWith, + Defined, + }; + + struct LookupResult { + LookupResultKind Kind; + std::shared_ptr Def; + }; + +private: + std::vector &Diags; + + std::map> + WithDefs; // record with ... ; users. + + void lookupVar(const ExprVar &Var, const std::shared_ptr &Env); + + std::shared_ptr dfsAttrs(const SemaAttrs &SA, + const std::shared_ptr &Env, + const Node *Syntax); + + void emitEnvLivenessWarning(const std::shared_ptr &NewEnv); + + void dfsDynamicAttrs(const std::vector &DynamicAttrs, + const std::shared_ptr &Env); + + // "dfs" is an abbreviation of "Deep-First-Search". + void dfs(const ExprLambda &Lambda, const std::shared_ptr &Env); + void dfs(const ExprAttrs &Attrs, const std::shared_ptr &Env); + void dfs(const ExprLet &Let, const std::shared_ptr &Env); + void dfs(const ExprWith &With, const std::shared_ptr &Env); + + void dfs(const Node &Root, const std::shared_ptr &Env); + + void trivialDispatch(const Node &Root, const std::shared_ptr &Env); + + std::map Results; + +public: + VariableLookupAnalysis(std::vector &Diags); + + void runOnAST(const Node &Root); + + LookupResult query(const ExprVar &Var) { return Results.at(&Var); } +}; + +} // namespace nixf diff --git a/libnixf/meson.build b/libnixf/meson.build index ee027c336..2618fc1e1 100644 --- a/libnixf/meson.build +++ b/libnixf/meson.build @@ -16,6 +16,7 @@ libnixf = library( 'src/Parse/ParseStrings.cpp', 'src/Parse/ParseSupport.cpp', 'src/Sema/SemaActions.cpp', + 'src/Sema/VariableLookup.cpp', include_directories: libnixf_inc, dependencies: libnixf_deps, install: true, @@ -59,6 +60,7 @@ test('unit/libnixf/Parse', test('unit/libnixf/Sema', executable('unit-libnixf-sema', 'test/Sema/SemaActions.cpp', + 'test/Sema/VariableLookup.cpp', dependencies: [ nixf, gtest_main ], include_directories: [ 'src/Sema' ] # Private headers ) diff --git a/libnixf/src/Sema/VariableLookup.cpp b/libnixf/src/Sema/VariableLookup.cpp new file mode 100644 index 000000000..6766a969f --- /dev/null +++ b/libnixf/src/Sema/VariableLookup.cpp @@ -0,0 +1,388 @@ +#include "nixf/Sema/VariableLookup.h" +#include "nixf/Basic/Diagnostic.h" +#include "nixf/Basic/Nodes/Attrs.h" +#include "nixf/Basic/Nodes/Lambda.h" + +using namespace nixf; + +namespace { + +/// Builder a map of definitions. If there are something overlapped, maybe issue +/// a diagnostic. +class DefBuilder { + EnvNode::DefMap Def; + +public: + void addBuiltin(std::string Name) { add(std::move(Name), nullptr); } + void add(std::string Name, const Node *Entry) { + assert(!Def.contains(Name)); + Def.insert({std::move(Name), std::make_shared(Entry)}); + } + + EnvNode::DefMap finish() { return std::move(Def); } +}; + +} // namespace + +bool EnvNode::isLive() const { + for (const auto &[_, D] : Defs) { + if (!D->uses().empty()) + return true; + } + return false; +} + +void VariableLookupAnalysis::emitEnvLivenessWarning( + const std::shared_ptr &NewEnv) { + for (const auto &[Name, Def] : NewEnv->defs()) { + if (Def->uses().empty()) { + Diagnostic &D = Diags.emplace_back(Diagnostic::DK_DefinitionNotUsed, + Def->syntax()->range()); + D << Name; + D.tag(DiagnosticTag::Faded); + } + } +} + +void VariableLookupAnalysis::lookupVar(const ExprVar &Var, + const std::shared_ptr &Env) { + const std::string &Name = Var.id().name(); + + bool EnclosedWith = false; // If there is a "With" enclosed this var name. + EnvNode *WithEnv = nullptr; + EnvNode *CurEnv = Env.get(); + std::shared_ptr Def; + for (; CurEnv; CurEnv = CurEnv->parent()) { + if (CurEnv->defs().contains(Name)) + Def = CurEnv->defs().at(Name); + // Find the most nested `with` expression, and set uses. + if (CurEnv->isWith() && !EnclosedWith) { + EnclosedWith = true; + WithEnv = CurEnv; + } + } + + if (Def) { + Def->usedBy(Var); + Results.insert({&Var, LookupResult{LookupResultKind::Defined, Def}}); + return; + } + if (EnclosedWith) { + Def = WithDefs.at(WithEnv->syntax()); + Def->usedBy(Var); + Results.insert({&Var, LookupResult{LookupResultKind::FromWith, Def}}); + return; + } + + // Otherwise, this variable is undefined. + Results.insert({&Var, LookupResult{LookupResultKind::Undefined, nullptr}}); + Diagnostic &Diag = + Diags.emplace_back(Diagnostic::DK_UndefinedVariable, Var.range()); + Diag << Var.id().name(); +} + +void VariableLookupAnalysis::dfs(const ExprLambda &Lambda, + const std::shared_ptr &Env) { + // Early exit for in-complete lambda. + if (!Lambda.body()) + return; + + // Create a new EnvNode, as lambdas may have formal & arg. + DefBuilder DBuilder; + assert(Lambda.arg()); + const LambdaArg &Arg = *Lambda.arg(); + + // foo: body + // ^~~<------- add function argument. + if (Arg.id()) + DBuilder.add(Arg.id()->name(), Arg.id()); + + // { foo, bar, ... } : body + /// ^~~~~~~~~<-------------- add function formals. + if (Arg.formals()) + for (const auto &[Name, Formal] : Arg.formals()->dedup()) + DBuilder.add(Name, Formal->id()); + + auto NewEnv = std::make_shared(Env, DBuilder.finish(), &Lambda); + + dfs(*Lambda.body(), NewEnv); + + emitEnvLivenessWarning(NewEnv); +} + +void VariableLookupAnalysis::dfsDynamicAttrs( + const std::vector &DynamicAttrs, + const std::shared_ptr &Env) { + for (const auto &Attr : DynamicAttrs) { + if (!Attr.value()) + continue; + dfs(Attr.key(), Env); + dfs(*Attr.value(), Env); + } +} + +std::shared_ptr +VariableLookupAnalysis::dfsAttrs(const SemaAttrs &SA, + const std::shared_ptr &Env, + const Node *Syntax) { + if (SA.isRecursive()) { + // rec { }, or let ... in ... + DefBuilder DB; + // For each static names, create a name binding. + for (const auto &[Name, Attr] : SA.staticAttrs()) + DB.add(Name, Attr.value()); + + auto NewEnv = std::make_shared(Env, DB.finish(), Syntax); + + // inherit (expr) attrs; + // ~~~~~~<------- this expression should have "parent scope". + for (const auto &[_, Attr] : SA.staticAttrs()) { + if (!Attr.value()) + continue; + dfs(*Attr.value(), Attr.fromInherit() ? Env : NewEnv); + } + + dfsDynamicAttrs(SA.dynamicAttrs(), NewEnv); + return NewEnv; + } + + // Non-recursive. Dispatch nested node with old Env + for (const auto &[_, Attr] : SA.staticAttrs()) { + if (!Attr.value()) + continue; + dfs(*Attr.value(), Env); + } + + dfsDynamicAttrs(SA.dynamicAttrs(), Env); + return Env; +}; + +void VariableLookupAnalysis::dfs(const ExprAttrs &Attrs, + const std::shared_ptr &Env) { + const SemaAttrs &SA = Attrs.sema(); + std::shared_ptr NewEnv = dfsAttrs(SA, Env, &Attrs); + if (NewEnv != Env) { + assert(Attrs.isRecursive() && + "NewEnv must be created for recursive attrset"); + if (!NewEnv->isLive()) { + Diagnostic &D = Diags.emplace_back(Diagnostic::DK_ExtraRecursive, + Attrs.rec()->range()); + D.fix("remove `rec` keyword") + .edit(TextEdit::mkRemoval(Attrs.rec()->range())); + D.tag(DiagnosticTag::Faded); + } + } +} + +void VariableLookupAnalysis::dfs(const ExprLet &Let, + const std::shared_ptr &Env) { + if (!Let.attrs()) + return; + const SemaAttrs &SA = Let.attrs()->sema(); + assert(SA.isRecursive() && "let ... in ... attrset must be recursive"); + std::shared_ptr NewEnv = dfsAttrs(SA, Env, &Let); + if (Let.expr()) + dfs(*Let.expr(), NewEnv); + + emitEnvLivenessWarning(NewEnv); +} + +void VariableLookupAnalysis::trivialDispatch( + const Node &Root, const std::shared_ptr &Env) { + for (const Node *Ch : Root.children()) { + if (!Ch) + continue; + dfs(*Ch, Env); + } +} + +void VariableLookupAnalysis::dfs(const ExprWith &With, + const std::shared_ptr &Env) { + auto NewEnv = std::make_shared(Env, EnvNode::DefMap{}, &With); + if (!WithDefs.contains(&With)) + WithDefs.insert({&With, std::make_shared(&With)}); + + if (With.with()) + dfs(*With.with(), Env); + + if (With.expr()) + dfs(*With.expr(), NewEnv); + + if (WithDefs.at(&With)->uses().empty()) { + Diagnostic &D = + Diags.emplace_back(Diagnostic::DK_ExtraWith, With.kwWith().range()); + Fix &F = D.fix("remove `with` expression") + .edit(TextEdit::mkRemoval(With.kwWith().range())); + if (With.tokSemi()) + F.edit(TextEdit::mkRemoval(With.tokSemi()->range())); + if (With.with()) + F.edit(TextEdit::mkRemoval(With.with()->range())); + } +} + +void VariableLookupAnalysis::dfs(const Node &Root, + const std::shared_ptr &Env) { + switch (Root.kind()) { + case Node::NK_ExprVar: { + const auto &Var = static_cast(Root); + lookupVar(Var, Env); + break; + } + case Node::NK_ExprLambda: { + const auto &Lambda = static_cast(Root); + dfs(Lambda, Env); + break; + } + case Node::NK_ExprAttrs: { + const auto &Attrs = static_cast(Root); + dfs(Attrs, Env); + break; + } + case Node::NK_ExprLet: { + const auto &Let = static_cast(Root); + dfs(Let, Env); + break; + } + case Node::NK_ExprWith: { + const auto &With = static_cast(Root); + dfs(With, Env); + break; + } + default: + trivialDispatch(Root, Env); + } +} + +void VariableLookupAnalysis::runOnAST(const Node &Root) { + // Create a basic env + DefBuilder DB; + std::vector Builtins{ + "__add", + "__fetchurl", + "__isFloat", + "__seq", + "break", + "__addDrvOutputDependencies", + "__filter", + "__isFunction", + "__sort", + "builtins", + "__addErrorContext", + "__filterSource", + "__isInt", + "__split", + "derivation", + "__all", + "__findFile", + "__isList", + "__splitVersion", + "derivationStrict", + "__any", + "__flakeRefToString", + "__isPath", + "__storeDir", + "dirOf", + "__appendContext", + "__floor", + "__isString", + "__storePath", + "false", + "__attrNames", + "__foldl'", + "__langVersion", + "__stringLength", + "fetchGit", + "__attrValues", + "__fromJSON", + "__length", + "__sub", + "fetchMercurial", + "__bitAnd", + "__functionArgs", + "__lessThan", + "__substring", + "fetchTarball", + "__bitOr", + "__genList", + "__listToAttrs", + "__tail", + "fetchTree", + "__bitXor", + "__genericClosure", + "__mapAttrs", + "__toFile", + "fromTOML", + "__catAttrs", + "__getAttr", + "__match", + "__toJSON", + "import", + "__ceil", + "__getContext", + "__mul", + "__toPath", + "isNull", + "__compareVersions", + "__getEnv", + "__nixPath", + "__toXML", + "map", + "__concatLists", + "__getFlake", + "__nixVersion", + "__trace", + "null", + "__concatMap", + "__groupBy", + "__parseDrvName", + "__traceVerbose", + "placeholder", + "__concatStringsSep", + "__hasAttr", + "__parseFlakeRef", + "__tryEval", + "removeAttrs", + "__convertHash", + "__hasContext", + "__partition", + "__typeOf", + "scopedImport", + "__currentSystem", + "__hashFile", + "__path", + "__unsafeDiscardOutputDependency", + "throw", + "__currentTime", + "__hashString", + "__pathExists", + "__unsafeDiscardStringContext", + "toString", + "__deepSeq", + "__head", + "__readDir", + "__unsafeGetAttrPos", + "true", + "__div", + "__intersectAttrs", + "__readFile", + "__zipAttrsWith", + "__elem", + "__isAttrs", + "__readFileType", + "abort", + "__elemAt", + "__isBool", + "__replaceStrings", + "baseNameOf", + }; + + for (const auto &Builtin : Builtins) + DB.addBuiltin(Builtin); + + auto Env = std::make_shared(nullptr, DB.finish(), nullptr); + + dfs(Root, Env); +} + +VariableLookupAnalysis::VariableLookupAnalysis(std::vector &Diags) + : Diags(Diags) {} diff --git a/libnixf/test/Sema/VariableLookup.cpp b/libnixf/test/Sema/VariableLookup.cpp new file mode 100644 index 000000000..0ba52a4cd --- /dev/null +++ b/libnixf/test/Sema/VariableLookup.cpp @@ -0,0 +1,128 @@ +#include + +#include "nixf/Basic/Diagnostic.h" +#include "nixf/Basic/Nodes/Expr.h" +#include "nixf/Parse/Parser.h" +#include "nixf/Sema/VariableLookup.h" + +using namespace nixf; + +namespace { + +using VLAResult = VariableLookupAnalysis::LookupResult; +using VLAResultKind = VariableLookupAnalysis::LookupResultKind; + +struct VLATest : public testing::Test { + std::vector Diags; +}; + +TEST_F(VLATest, UndefinedVariable) { + std::shared_ptr AST = parse("a", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 1); + ASSERT_EQ(Diags[0].kind(), Diagnostic::DK_UndefinedVariable); + + ASSERT_EQ(AST->kind(), Node::NK_ExprVar); + + const auto &Var = *static_cast(AST.get()); + + VLAResult Result = VLA.query(Var); + + ASSERT_EQ(Result.Kind, VLAResultKind::Undefined); +} + +TEST_F(VLATest, DefinedBuiltin) { + std::shared_ptr AST = parse("builtins", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 0); + ASSERT_EQ(AST->kind(), Node::NK_ExprVar); + + const auto &Var = *static_cast(AST.get()); + + VLAResult Result = VLA.query(Var); + + ASSERT_EQ(Result.Kind, VLAResultKind::Defined); +} + +TEST_F(VLATest, LookupLambda) { + std::shared_ptr AST = parse("foo: foo", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 0); + + const auto &Var = *static_cast( + static_cast(AST.get())->body()); + + VLAResult Result = VLA.query(Var); + + ASSERT_EQ(Result.Kind, VLAResultKind::Defined); +} + +TEST_F(VLATest, LookupAttrs) { + std::shared_ptr AST = parse("rec { a = 1; y = a; }", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 0); +} + +TEST_F(VLATest, LookupNonRecursiveAttrs) { + std::shared_ptr AST = parse("{ a = 1; y = a; }", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 1); + ASSERT_EQ(Diags[0].kind(), Diagnostic::DK_UndefinedVariable); +} + +TEST_F(VLATest, LookupLet) { + std::shared_ptr AST = parse("let a = 1; b = a; in a + b", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 0); +} + +TEST_F(VLATest, LookupWith) { + std::shared_ptr AST = parse("with 1; foo", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + const auto &With = *static_cast(AST.get()); + const auto &Var = *static_cast(With.expr()); + + ASSERT_EQ(Diags.size(), 0); + + VLAResult Result = VLA.query(Var); + + ASSERT_EQ(Result.Kind, VLAResultKind::FromWith); +} + +TEST_F(VLATest, LivenessRec) { + std::shared_ptr AST = parse("rec { x = 1; y = 2; z = 3; }", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 1); + + ASSERT_EQ(Diags[0].kind(), Diagnostic::DK_ExtraRecursive); +} + +TEST_F(VLATest, LivenessArg) { + std::shared_ptr AST = parse("{ foo }: 1", Diags); + VariableLookupAnalysis VLA(Diags); + VLA.runOnAST(*AST); + + ASSERT_EQ(Diags.size(), 1); + + ASSERT_EQ(Diags[0].kind(), Diagnostic::DK_DefinitionNotUsed); + ASSERT_EQ(Diags[0].tags().size(), 1); + ASSERT_EQ(Diags[0].tags()[0], DiagnosticTag::Faded); +} + +} // namespace