/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 2016, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ // A lot of ugly helper code to // a) check whether two ASTs are the same // b) format an AST into human-readable format in case they aren't #ifndef INC_AulSyntaxTestDetail #define INC_AulSyntaxTestDetail #include #include #include "script/C4AulAST.h" #include "script/C4AulParse.h" class AstFormattingVisitor : public ::aul::AstVisitor { std::ostream ⌖ public: AstFormattingVisitor(std::ostream &target) : target(target) {} virtual void visit(const ::aul::ast::Noop *) override { target << "no-op"; } virtual void visit(const ::aul::ast::StringLit *n) override { target << "\"" << n->value << "\""; } virtual void visit(const ::aul::ast::IntLit *n) override { target << n->value; } virtual void visit(const ::aul::ast::BoolLit *n) override { target << (n->value ? "true" : "false"); } virtual void visit(const ::aul::ast::ArrayLit *n) override { target << "(array"; for (auto &v : n->values) { target << " "; v->accept(this); } target << ")"; } virtual void visit(const ::aul::ast::ProplistLit *n) override { target << "(proplist"; for (auto &v : n->values) { target << " (\"" << v.first << "\" "; v.second->accept(this); target << ")"; } target << ")"; } virtual void visit(const ::aul::ast::NilLit *) override { target << "nil"; } virtual void visit(const ::aul::ast::ThisLit *) override { target << "this"; } virtual void visit(const ::aul::ast::VarExpr *n) override { target << "(var-expr " << n->identifier << ")"; } virtual void visit(const ::aul::ast::UnOpExpr *n) override { target << "(" << C4ScriptOpMap[n->op].Identifier << " "; if (C4ScriptOpMap[n->op].Postfix) target << "postfix "; n->operand->accept(this); target << ")"; } virtual void visit(const ::aul::ast::BinOpExpr *n) override { target << "(" << C4ScriptOpMap[n->op].Identifier << " "; n->lhs->accept(this); target << " "; n->rhs->accept(this); target << ")"; } virtual void visit(const ::aul::ast::AssignmentExpr *n) override { target << "(= "; n->lhs->accept(this); target << " "; n->rhs->accept(this); target << ")"; } virtual void visit(const ::aul::ast::SubscriptExpr *n) override { target << "(subscript "; n->object->accept(this); target << " "; n->index->accept(this); target << ")"; } virtual void visit(const ::aul::ast::SliceExpr *n) override { target << "(slice "; n->object->accept(this); target << " "; n->start->accept(this); target << " "; n->end->accept(this); target << ")"; } virtual void visit(const ::aul::ast::CallExpr *n) override { target << "("; if (n->safe_call) target << "safe-"; target << "call"; if (n->context) { target << "-with-context"; n->context->accept(this); } target << " (args"; for (auto &v : n->args) { target << " "; v->accept(this); } target << ")"; if (n->append_unnamed_pars) target << " append-unnamed"; target << ")"; } virtual void visit(const ::aul::ast::ParExpr *n) override { target << "(par "; n->arg->accept(this); target << ")"; } virtual void visit(const ::aul::ast::Block *n) override { target << "(block"; for (auto &v : n->children) { target << " "; v->accept(this); } target << ")"; } virtual void visit(const ::aul::ast::Return *n) override { if (n->value) { target << "(return "; n->value->accept(this); target << ")"; } else { target << "(return)"; } } virtual void visit(const ::aul::ast::ForLoop *n) override { target << "(for"; if (n->init) { target << " (init "; n->init->accept(this); target << ")"; } if (n->cond) { target << " (cond "; n->cond->accept(this); target << ")"; } if (n->incr) { target << " (incr "; n->incr->accept(this); target << ")"; } target << " "; n->body->accept(this); target << ")"; } virtual void visit(const ::aul::ast::RangeLoop *n) override { target << "(for-in"; if (n->scoped_var) target << "-with-scope"; target << " \"" << n->var << "\" "; n->cond->accept(this); target << " "; n->body->accept(this); target << ")"; } virtual void visit(const ::aul::ast::DoLoop *n) override { target << "(do "; n->cond->accept(this); target << " "; n->body->accept(this); target << ")"; } virtual void visit(const ::aul::ast::WhileLoop *n) override { target << "(while "; n->cond->accept(this); target << " "; n->body->accept(this); target << ")"; } virtual void visit(const ::aul::ast::Break *n) override { target << "break"; } virtual void visit(const ::aul::ast::Continue *n) override { target << "continue"; } virtual void visit(const ::aul::ast::If *n) override { target << "(if "; n->cond->accept(this); target << " "; n->iftrue->accept(this); if (n->iffalse) { target << " "; n->iffalse->accept(this); } target << ")"; } virtual void visit(const ::aul::ast::VarDecl *n) override { target << "(var-decl "; if (n->constant) target << "const "; switch (n->scope) { case ::aul::ast::VarDecl::Scope::Func: target << "func-scope"; break; case ::aul::ast::VarDecl::Scope::Object: target << "obj-scope"; break; case ::aul::ast::VarDecl::Scope::Global: target << "global-scope"; break; } for (auto &d : n->decls) { target << " (" << d.name; if (d.init) { target << " "; d.init->accept(this); } target << ")"; } target << ")"; } virtual void visit(const ::aul::ast::FunctionDecl *n) override { target << "(func-decl " << n->name << " ("; for (auto &p : n->params) { target << "(" << GetC4VName(p.type) << " " << p.name << ")"; } if (n->has_unnamed_params) target << " variable-args"; n->body->accept(this); target << ")"; } virtual void visit(const ::aul::ast::FunctionExpr *n) override { target << "(func-expr " << " ("; for (auto &p : n->params) { target << "(" << GetC4VName(p.type) << " " << p.name << ")"; } if (n->has_unnamed_params) target << " variable-args"; n->body->accept(this); target << ")"; } virtual void visit(const ::aul::ast::IncludePragma *n) override { target << "(include-pragma \"" << n->what << "\")"; } virtual void visit(const ::aul::ast::AppendtoPragma *n) override { target << "(appendto-pragma \"" << n->what << "\")"; } virtual void visit(const ::aul::ast::Script *n) override { target << "(script"; for (auto &d : n->declarations) { target << " "; d->accept(this); } target << ")"; } }; // These templates use the above formatter to write an AST as a human-readable // expression to the output if a test fails, instead of the default which is a // hex memory dump template std::enable_if_t::value, std::ostream &> operator<<(::std::ostream &os, const T &node) { AstFormattingVisitor v(os); node.accept(&v); return os; } template std::enable_if_t::value, std::ostream &> operator<<(::std::ostream &os, const std::unique_ptr &node) { return os << *node; } template std::enable_if_t::value, std::ostream &> operator<<(::std::ostream &os, const std::reference_wrapper &node) { return os << node.get(); } static bool MatchesAstImpl(const ::aul::ast::Node *a_, const ::aul::ast::Node *b_); template static bool MatchesAstImpl(const std::unique_ptr &a_, const std::unique_ptr &b_) { return MatchesAstImpl(a_.get(), b_.get()); } template static bool MatchesAstImpl(const std::unique_ptr &a_, const ::aul::ast::Node *b_) { return MatchesAstImpl(a_.get(), b_); } template static bool MatchesAstImpl(const ::aul::ast::Node *a_, const std::unique_ptr &b_) { return MatchesAstImpl(a_, b_.get()); } static bool MatchesAstImpl(const ::aul::ast::Node *a_, const ::aul::ast::Node *b_) { // It would be real nice if C++ had proper multimethods, but alas it // does not. // Since this method is only used in testing, the overhead of all // the dynamic_cast'ing we're doing here should be fine. // If a and b are both nullptr, they match. if (a_ == nullptr && b_ == nullptr) return true; // If one of a and b is a nullptr, but not the other, they don't match. if ((a_ == nullptr) != (b_ == nullptr)) return false; // If a and b are not of the same (dynamic) type, they don't match. if (typeid(*a_) != typeid(*b_)) return false; // Ok this is ugly as sin but I don't think we can do it any cleaner // without adding specialized acceptors to the AST nodes. // We're dynamic_cast'ing both nodes to the expected type, test the // result to make sure the cast succeeded, then run the body, then // set a and b to nullptr to break out of the loop. // The body gets a and b cast to the expected type instead of the base // Node*, so we can check all members without additional, explicit // casting. #define WHEN(type) for (const type *a = dynamic_cast(a_), *b = dynamic_cast(b_); a && b; a = b = nullptr) // The base (non-composite) literals all just compare values, but since // they're different types we can't just use one common case. WHEN(::aul::ast::StringLit) { return a->value == b->value; } WHEN(::aul::ast::IntLit) { return a->value == b->value; } WHEN(::aul::ast::BoolLit) { return a->value == b->value; } // nil and this don't have any values to compare, so just checking type // is sufficient WHEN(::aul::ast::NilLit) { return true; } WHEN(::aul::ast::ThisLit) { return true; } // No-ops don't have anything to compare either WHEN(::aul::ast::Noop) { return true; } // Array literals need to compare all entries recursively WHEN(::aul::ast::ArrayLit) { return std::equal(a->values.begin(), a->values.end(), b->values.begin(), b->values.end(), [](const auto &a0, const auto &b0) { return MatchesAstImpl(a0, b0); }); } // Proplist literals need to compare all entries by key and value WHEN(::aul::ast::ProplistLit) { return std::equal(a->values.begin(), a->values.end(), b->values.begin(), b->values.end(), [](const auto &a0, const auto &b0) { if (a0.first != b0.first) return false; return MatchesAstImpl(a0.second, b0.second); }); } // Operators need to have matching opcodes and LHS/RHS WHEN(::aul::ast::UnOpExpr) { return a->op == b->op && MatchesAstImpl(a->operand, b->operand); } WHEN(::aul::ast::BinOpExpr) { return a->op == b->op && MatchesAstImpl(a->lhs, b->lhs) && MatchesAstImpl(a->rhs, b->rhs); } WHEN(::aul::ast::AssignmentExpr) { return MatchesAstImpl(a->lhs, b->lhs) && MatchesAstImpl(a->rhs, b->rhs); } // Variable expressions just need to reference the same identifier WHEN(::aul::ast::VarExpr) { return a->identifier == b->identifier; } // Subscript expressions need to have the same object and index WHEN(::aul::ast::SubscriptExpr) { return MatchesAstImpl(a->index, b->index) && MatchesAstImpl(a->object, b->object); } // Slice expressions need to have the same base object and start/end indices WHEN(::aul::ast::SliceExpr) { return MatchesAstImpl(a->object, b->object) && MatchesAstImpl(a->start, b->start) && MatchesAstImpl(a->end, b->end); } // Call expressions need to match safety, context, identifier and args // (including unnamed arg passthrough) WHEN(::aul::ast::CallExpr) { if (!(a->safe_call == b->safe_call && a->append_unnamed_pars == b->append_unnamed_pars && MatchesAstImpl(a->context, b->context) && a->callee == b->callee)) return false; return std::equal(a->args.begin(), a->args.end(), b->args.begin(), b->args.end(), [](const auto &a0, const auto &b0) { return MatchesAstImpl(a0, b0); }); } // Par() expressions need the same index WHEN(::aul::ast::ParExpr) { return MatchesAstImpl(a->arg, b->arg); } // Blocks need to have the same children WHEN(::aul::ast::Block) { return std::equal(a->children.begin(), a->children.end(), b->children.begin(), b->children.end(), [](const auto &a0, const auto &b0) { return MatchesAstImpl(a0, b0); }); } // Return statements need to have the same parameters WHEN(::aul::ast::Return) { return MatchesAstImpl(a->value, b->value); } // for loops need to have the same initializer, condition, incrementor, // and body WHEN(::aul::ast::ForLoop) { return MatchesAstImpl(a->init, b->init) && MatchesAstImpl(a->cond, b->cond) && MatchesAstImpl(a->incr, b->incr) && MatchesAstImpl(a->body, b->body); } // range loops need to have the same scoping, loop variable, target object, // and body WHEN(::aul::ast::RangeLoop) { return a->scoped_var == b->scoped_var && a->var == b->var && MatchesAstImpl(a->cond, b->cond) && MatchesAstImpl(a->body, b->body); } // do and while loops need to have the same condition and body WHEN(::aul::ast::Loop) { assert(typeid(*a) == typeid(::aul::ast::DoLoop) || typeid(*a) == typeid(::aul::ast::WhileLoop)); return MatchesAstImpl(a->cond, b->cond) && MatchesAstImpl(a->body, b->body); } // break and continue just need to have the same type WHEN(::aul::ast::LoopControl) { return true; } // if-else needs to have the same condition, then-branch and else-branch WHEN(::aul::ast::If) { return MatchesAstImpl(a->cond, b->cond) && MatchesAstImpl(a->iftrue, b->iftrue) && MatchesAstImpl(a->iffalse, b->iffalse); } // variable declarations need to have the same scope, constancy, and // for each declaration have the same identifier and initializer WHEN(::aul::ast::VarDecl) { if (a->scope != b->scope || a->constant != b->constant) return false; return std::equal(a->decls.begin(), a->decls.end(), b->decls.begin(), b->decls.end(), [](const auto &a0, const auto &b0) { return a0.name == b0.name && MatchesAstImpl(a0.init, b0.init); }); } // function declarations need to have the same name, and scope, // plus the common function parts WHEN(::aul::ast::FunctionDecl) { if (a->name != b->name) return false; if (a->is_global != b->is_global) return false; // but keep checking } // all functions (declarations and expressions) need to have the same // parameter list and body WHEN(::aul::ast::Function) { if (a->has_unnamed_params != b->has_unnamed_params) return false; return std::equal(a->params.begin(), a->params.end(), b->params.begin(), b->params.end(), [](const auto &a0, const auto &b0) { return a0.name == b0.name && a0.type == b0.type; }) && MatchesAstImpl(a->body, b->body); } // include and appendto pragmas need to include/appendto the same identifier WHEN(::aul::ast::IncludePragma) { return a->what == b->what; } WHEN(::aul::ast::AppendtoPragma) { return a->what == b->what; } // scripts need to have the same list of declarations WHEN(::aul::ast::Script) { return std::equal(a->declarations.begin(), a->declarations.end(), b->declarations.begin(), b->declarations.end(), [](const auto &a0, const auto &b0) { return MatchesAstImpl(a0, b0); }); } assert(!"AST matching fell through to the default case"); return false; #undef WHEN } // helper templates to turn a pointer, unique_ptr or reference to T // into an unconditional reference to T. We're using this so we only // have to handle references in MatchesAst instead of requiring // several overloads. template static const T &deref(const std::unique_ptr &p) { return *p; } template static const T &deref(const std::reference_wrapper &p) { return p; } template static const T &deref(const T *p) { return *p; } template static const T &deref(const T &p) { return p; } // The actual matcher. Just delegates to the recursive function above. MATCHER_P(MatchesAstP, ast, "") { return MatchesAstImpl(&deref(arg), &deref(ast)); } // And a convenience wrapper that stores the AST we're matching against // in a std::reference_wrapper because we can't copy ASTs, but GTest // requires that. Don't keep the matcher around longer than the AST or // things will go sour. template auto MatchesAst(const T &t) { return MatchesAstP(std::cref(t)); } #endif