From e862da900064ce80d9219b4bcc190ba3b237dc9b Mon Sep 17 00:00:00 2001 From: bog Date: Sat, 14 Oct 2023 03:56:31 +0200 Subject: [PATCH] :sparkles: can now build rules. --- README.md | 7 +- doc/grammar.bnf | 7 ++ meson.build | 9 ++ src/DAG.cpp | 92 +++++++++++++++ src/DAG.hpp | 34 ++++++ src/Interpreter.cpp | 279 ++++++++++++++++++++++++++++++++++++++++++++ src/Interpreter.hpp | 52 +++++++++ src/Lexer.cpp | 154 ++++++++++++++++++++++++ src/Lexer.hpp | 62 ++++++++++ src/Node.cpp | 56 +++++++++ src/Node.hpp | 41 +++++++ src/Parser.cpp | 171 +++++++++++++++++++++++++++ src/Parser.hpp | 43 +++++++ src/State.cpp | 220 ++++++++++++++++++++++++++++++++++ src/State.hpp | 45 +++++++ src/commons.hpp | 34 ++++++ src/main.cpp | 7 +- tests/Lexer.cpp | 43 +++++++ tests/Parser.cpp | 48 ++++++++ 19 files changed, 1400 insertions(+), 4 deletions(-) create mode 100644 doc/grammar.bnf create mode 100644 src/DAG.cpp create mode 100644 src/DAG.hpp create mode 100644 src/Interpreter.cpp create mode 100644 src/Interpreter.hpp create mode 100644 src/Lexer.cpp create mode 100644 src/Lexer.hpp create mode 100644 src/Node.cpp create mode 100644 src/Node.hpp create mode 100644 src/Parser.cpp create mode 100644 src/Parser.hpp create mode 100644 src/State.cpp create mode 100644 src/State.hpp create mode 100644 src/commons.hpp create mode 100644 tests/Lexer.cpp create mode 100644 tests/Parser.cpp diff --git a/README.md b/README.md index db55bd0..099a346 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ None for now, but maybe one day... ## How to install Snake relies on some libraries you must install in order to compile it. -- catch2 (for testing) +- catch2 (only for testing) +- sqlite3 First, you need to compile Snake using the given Makefile: @@ -38,6 +39,6 @@ sudo make install Don't. ## License -Snake is released under the GPLv3. +Snake is released under the GPLv3. -See the LICENSE file at the project's root for more information. \ No newline at end of file +See the LICENSE file in the project directory for more information. diff --git a/doc/grammar.bnf b/doc/grammar.bnf new file mode 100644 index 0000000..6b915a6 --- /dev/null +++ b/doc/grammar.bnf @@ -0,0 +1,7 @@ +DOC ::= RULE* +RULE ::= TARGET rarrow DEPS BLOCK +TARGET ::= ident+ +DEPS ::= ident* +BLOCK ::= obrace CMD_LST cbrace +CMD_LST ::= (CMD (comma CMD)* comma?)? +CMD ::= ident* diff --git a/meson.build b/meson.build index fab025b..7695a44 100644 --- a/meson.build +++ b/meson.build @@ -16,8 +16,15 @@ configure_file(input: 'src/snake.in.hpp', snake_lib = static_library('snake', sources: [ + 'src/Node.cpp', + 'src/Lexer.cpp', + 'src/Parser.cpp', + 'src/Interpreter.cpp', + 'src/State.cpp', + 'src/DAG.cpp', ], dependencies: [ + dependency('sqlite3') ]) snake_dep = declare_dependency(link_with: [ @@ -36,6 +43,8 @@ executable('snake', executable('snake-tests', sources: [ 'tests/main.cpp', + 'tests/Lexer.cpp', + 'tests/Parser.cpp', ], dependencies: [ snake_dep, diff --git a/src/DAG.cpp b/src/DAG.cpp new file mode 100644 index 0000000..8481a40 --- /dev/null +++ b/src/DAG.cpp @@ -0,0 +1,92 @@ +#include "DAG.hpp" + +namespace sn +{ + /*explicit*/ DAG::DAG() + { + } + + /*virtual*/ DAG::~DAG() + { + } + + void DAG::add_node(std::filesystem::path const& path) + { + m_nodes[path] = {}; + } + + void DAG::link(std::filesystem::path const& lhs, + std::filesystem::path const& rhs) + { + m_nodes[lhs].push_back(rhs); + } + + bool DAG::exists(std::filesystem::path const& path) const + { + return m_nodes.find(path) != std::end(m_nodes); + } + + std::string DAG::string() const + { + std::stringstream ss; + + for (auto entry: m_nodes) + { + ss << entry.first << std::endl; + + for (auto d: entry.second) + { + ss << "\t" << d << std::endl; + } + } + + return ss.str(); + } + + std::vector DAG::sort() const + { + if (m_nodes.empty()) + { + return {}; + } + + std::vector result; + std::unordered_map visited; + + for (auto const& entry: m_nodes) + { + visited[entry.first] = false; + } + + std::function + visit = [&](auto node){ + if (visited[node]) { return; } + + for (auto const& entry: m_nodes.at(node)) + { + visit(entry); + } + + visited[node] = true; + result.insert(std::begin(result), node); + }; + + bool running = true; + + while (running) + { + running = false; + + for (auto const& entry: visited) + { + if (!entry.second) + { + running = true; + visit(entry.first); + } + } + } + + return result; + } +} diff --git a/src/DAG.hpp b/src/DAG.hpp new file mode 100644 index 0000000..031458c --- /dev/null +++ b/src/DAG.hpp @@ -0,0 +1,34 @@ +#ifndef sn_DAG_HPP +#define sn_DAG_HPP + +#include "commons.hpp" + +namespace sn +{ + /** + * Mathematical Directed Acyclic Graph (DAG). + **/ + class DAG + { + public: + explicit DAG(); + virtual ~DAG(); + + void add_node(std::filesystem::path const& path); + + void link(std::filesystem::path const& lhs, + std::filesystem::path const& rhs); + + bool exists(std::filesystem::path const& path) const; + + std::string string() const; + + std::vector sort() const; + + private: + std::unordered_map> m_nodes; + }; +} + +#endif diff --git a/src/Interpreter.cpp b/src/Interpreter.cpp new file mode 100644 index 0000000..be8e068 --- /dev/null +++ b/src/Interpreter.cpp @@ -0,0 +1,279 @@ +#include "Interpreter.hpp" +#include "Lexer.hpp" +#include "Parser.hpp" +#include "State.hpp" + +namespace sn +{ + /*explicit*/ Interpreter::Interpreter() + { + } + + /*virtual*/ Interpreter::~Interpreter() + { + } + + void Interpreter::run() + { + // Process the document ast + // ------------------------ + auto node = parse_snakefile(); + process(node); + + // Get all files + // ------------- + std::vector files; + + for (auto entry: m_dependencies) + { + if (auto itr = std::find(std::begin(files), std::end(files), + entry.first); + itr == std::end(files)) + { + auto path = format_path(entry.first); + + files.push_back(path); + } + + for (auto d: entry.second) + { + if (auto itr = std::find(std::begin(files), std::end(files), d); + itr == std::end(files)) + { + files.push_back(d); + } + } + } + + // Get modified files + // ------------------ + auto state = std::make_shared(); + + state->init(files); + + auto modified_files = state->get_modified_files(files); + + // Build DAG + // --------- + auto dag = std::make_shared(); + + std::function + add = [&](std::filesystem::path file) { + file = format_path(file); + + if (!dag->exists(file)) + { + dag->add_node(file); + } + + auto nexts = targets(file); + + for (auto const& next: nexts) + { + if (!dag->exists(next)) + { + add(next); + } + + dag->link(file, next); + } + }; + + for (auto file: modified_files) + { + add(file); + } + + auto sorted = dag->sort(); + + int max_w = 0; + for (auto file: sorted) + { + max_w = std::fmax(max_w, file.filename().string().size()); + } + + for (auto file: sorted) + { + if (auto all_scripts=m_scripts.find(file); + all_scripts != std::end(m_scripts)) + { + for (auto script: all_scripts->second) + { + int const SQUARES_W = 2; + int const SPACE = 2; + int const WIDTH = max_w + SQUARES_W + SPACE; + + std::cout << std::setw(WIDTH); + std::cout << std::left; + + std::cout << "[" + file.filename().string() + "]"; + + std::cout << std::setw(WIDTH); + std::cout << script << std::endl; + + auto ret = execute(script); + + if (ret.empty() == false) + { + std::cout << ret << std::endl; + } + } + } + + state->update(file); + } + + if (sorted.empty()) + { + std::cout << "everything is up to date" << std::endl; + } + } + + std::filesystem::path Interpreter::find_snakefile() + { + auto path = std::filesystem::current_path() / "Snakefile"; + + if (!std::filesystem::exists(path)) + { + throw run_error {"Snakefile not found"}; + } + + return path; + } + + std::string Interpreter::load_snakefile() + { + std::ifstream file {find_snakefile()}; + assert(file); + std::stringstream ss; + ss << file.rdbuf(); + return ss.str(); + } + + std::shared_ptr Interpreter::parse_snakefile() + { + Lexer lexer; + lexer.scan(load_snakefile()); + + Parser parser; + auto node = parser.parse(lexer.all()); + + return node; + } + + std::vector + Interpreter::targets(std::filesystem::path path) const + { + std::vector result; + + for (auto entry: m_dependencies) + { + if (auto itr = std::find(std::begin(entry.second), + std::end(entry.second), + path); + itr != std::end(entry.second)) + { + result.push_back(format_path(entry.first)); + } + } + + return result; + } + + void Interpreter::process(std::shared_ptr node) + { + switch (node->type()) + { + case NODE_DOC: { + for (size_t i=0; isize(); i++) + { + process(*node->get(i)); + } + } break; + + case NODE_RULE: { + auto all_targets = *node->get(0); + auto deps = *node->get(1); + auto block = *node->get(2); + + for (size_t i=0; isize(); i++) + { + auto target = *all_targets->get(i); + + for (size_t j=0; jsize(); j++) + { + auto dep = *deps->get(j); + auto path = format_path(dep->repr()); + + m_dependencies[format_path(target->repr())].push_back(path); + + auto target_path = format_path(target->repr()); + + m_scripts[target_path] = scripts(block); + } + } + } break; + + default: + std::cerr << "Unknown node of type '" + << SN_GET(NodeTypeStr, node->type(), "NODE_") + << "'" + << std::endl; + abort(); + } + } + + std::vector + Interpreter::scripts(std::shared_ptr block) const + { + std::vector result; + + for (size_t i=0; isize(); i++) + { + auto cmd = *block->get(i); + + std::string script; + std::string sep; + + for (size_t j=0; jsize(); j++) + { + script += sep + (*cmd->get(j))->repr(); + sep = " "; + } + + result.push_back(script); + } + + return result; + } + + std::string Interpreter::execute(std::string const& script) const + { + std::stringstream ss; + + FILE* out = popen(script.c_str(), "r"); + assert(out); + + char buf; + + while ( fread(&buf, sizeof(char), 1, out) ) + { + ss << buf; + } + + pclose(out); + return ss.str(); + } + + std::filesystem::path + Interpreter::format_path(std::filesystem::path path) const + { + if (std::filesystem::exists(path)) + { + return std::filesystem::canonical(path); + } + + return std::filesystem::absolute(path); + } + +} diff --git a/src/Interpreter.hpp b/src/Interpreter.hpp new file mode 100644 index 0000000..f512893 --- /dev/null +++ b/src/Interpreter.hpp @@ -0,0 +1,52 @@ +#ifndef sn_INTERPRETER_HPP +#define sn_INTERPRETER_HPP + +#include "commons.hpp" +#include "Node.hpp" +#include "State.hpp" +#include "DAG.hpp" + +namespace sn +{ + SN_ERROR(run_error); + + /** + * Process a snakefile AST and run build commands. + * @see Node + * @see State + **/ + class Interpreter + { + public: + explicit Interpreter(); + virtual ~Interpreter(); + + void run(); + + std::filesystem::path find_snakefile(); + std::string load_snakefile(); + std::shared_ptr parse_snakefile(); + + private: + std::unordered_map> m_dependencies; + + std::unordered_map> m_scripts; + + std::vector + targets(std::filesystem::path path) const; + + void process(std::shared_ptr node); + + std::vector scripts(std::shared_ptr block) const; + + std::string execute(std::string const& script) const; + + std::filesystem::path + format_path(std::filesystem::path path) const; + + }; +} + +#endif diff --git a/src/Lexer.cpp b/src/Lexer.cpp new file mode 100644 index 0000000..883b085 --- /dev/null +++ b/src/Lexer.cpp @@ -0,0 +1,154 @@ +#include "Lexer.hpp" + +namespace sn +{ + /*explicit*/ Lexer::Lexer() + { + m_separators.push_back('\n'); + m_separators.push_back('\t'); + m_separators.push_back('\n'); + m_separators.push_back(' '); + + add_text("->", NODE_RARROW); + add_text("{", NODE_OBRACE); + add_text("}", NODE_CBRACE); + add_text(",", NODE_COMMA); + + m_scanners.push_back(std::bind(&Lexer::scan_ident, this)); + } + + /*virtual*/ Lexer::~Lexer() + { + } + + void Lexer::scan(std::string const& source) + { + m_source = source; + m_cursor = 0; + } + + std::optional> Lexer::next() + { + std::optional next_info; + + skip_spaces(); + + for (auto const& scanner: m_scanners) + { + auto info = scanner(); + + if (// first info + (info && !next_info) + // better info + || (info && next_info && info->cursor > next_info->cursor)) + { + next_info = info; + } + } + + if (next_info) + { + auto node = std::make_shared(next_info->type, + next_info->repr); + m_cursor = next_info->cursor; + return node; + } + + return std::nullopt; + } + + std::vector> Lexer::all() + { + std::vector> result; + + while (m_cursor < m_source.size()) + { + if (auto n=next(); + n) + { + result.push_back(*n); + } + else + { + return result; + } + } + + return result; + } + + bool Lexer::is_separator(char c) const + { + return std::find(std::begin(m_separators), std::end(m_separators), c) + != std::end(m_separators); + } + + void Lexer::skip_spaces() + { + while (m_cursor < m_source.size() + && std::isspace(m_source[m_cursor])) + { + m_cursor++; + } + } + + std::optional Lexer::scan_text(std::string const& text, + NodeType type, + bool has_repr) + { + if (text.size() + m_cursor > m_source.size()) + { + return std::nullopt; + } + + for (size_t i=0; i Lexer::scan_ident() + { + size_t cursor = m_cursor; + std::string repr; + + while (cursor < m_source.size() + && !is_separator(m_source[cursor])) + { + repr += m_source[cursor]; + cursor++; + } + + if (repr.empty() == false) + { + return ScanInfo { + cursor, + NODE_IDENT, + repr + }; + } + + return std::nullopt; + } +} diff --git a/src/Lexer.hpp b/src/Lexer.hpp new file mode 100644 index 0000000..5d7789d --- /dev/null +++ b/src/Lexer.hpp @@ -0,0 +1,62 @@ +#ifndef sn_LEXER_HPP +#define sn_LEXER_HPP + +#include "commons.hpp" +#include "Node.hpp" + +namespace sn +{ + /** + * Utility to collect and sort scan informations. + * @see Lexer + **/ + struct ScanInfo { + size_t cursor; + NodeType type; + std::string repr; + }; + + + /** + * A scanner is a function returning (or not) a scan information. + * @see ScanInfo + **/ + using scanner_t = std::function()>; + + /** + * Scan an input source and gives its corresponding tokens. + * @see Node + **/ + class Lexer + { + public: + explicit Lexer(); + virtual ~Lexer(); + + void scan(std::string const& source); + std::optional> next(); + std::vector> all(); + + private: + std::string m_source; + size_t m_cursor; + std::vector m_scanners; + std::vector m_separators; + + bool is_separator(char c) const; + + void skip_spaces(); + + std::optional scan_text(std::string const& text, + NodeType type, + bool has_repr); + + void add_text(std::string const& text, + NodeType type, + bool has_repr=false); + + std::optional scan_ident(); + }; +} + +#endif diff --git a/src/Node.cpp b/src/Node.cpp new file mode 100644 index 0000000..62ea647 --- /dev/null +++ b/src/Node.cpp @@ -0,0 +1,56 @@ +#include "Node.hpp" + +namespace sn +{ + /*explicit*/ Node::Node(NodeType type, std::string const& repr) + : m_type { type } + , m_repr { repr } + { + } + + /*virtual*/ Node::~Node() + { + } + + void Node::add_child(std::shared_ptr child) + { + m_children.push_back(child); + } + + std::optional> Node::get(size_t index) + { + if (index < size()) + { + return m_children[index]; + } + + return std::nullopt; + } + + std::string Node::string() const + { + std::stringstream ss; + ss << SN_GET(NodeTypeStr, m_type, "NODE_"); + + if (m_repr.empty() == false) + { + ss << "[" << m_repr << "]"; + } + + if (size() > 0) + { + ss << "("; + std::string sep; + + for (auto const& child: m_children) + { + ss << sep << child->string(); + sep = ","; + } + + ss << ")"; + } + + return ss.str(); + } +} diff --git a/src/Node.hpp b/src/Node.hpp new file mode 100644 index 0000000..41978bd --- /dev/null +++ b/src/Node.hpp @@ -0,0 +1,41 @@ +#ifndef sn_NODE_HPP +#define sn_NODE_HPP + +#include "commons.hpp" + +#define NODE_TYPES(G) \ + G(NODE_DOC), G(NODE_IDENT), G(NODE_RARROW), G(NODE_OBRACE), \ + G(NODE_CBRACE), G(NODE_CMD), G(NODE_CMD_LST), G(NODE_COMMA), \ + G(NODE_BLOCK), G(NODE_TARGET), G(NODE_DEPS), G(NODE_RULE) + +namespace sn +{ + SN_ENUM(NodeType, NODE_TYPES); + + /** + * A node is an element of the snake AST. + **/ + class Node + { + public: + explicit Node(NodeType type, std::string const& repr=""); + virtual ~Node(); + + NodeType type() const { return m_type; } + std::string repr() const { return m_repr; } + + size_t size() const { return m_children.size(); } + + void add_child(std::shared_ptr child); + std::optional> get(size_t index); + + std::string string() const; + + private: + NodeType m_type; + std::string m_repr; + std::vector> m_children; + }; +} + +#endif diff --git a/src/Parser.cpp b/src/Parser.cpp new file mode 100644 index 0000000..c210def --- /dev/null +++ b/src/Parser.cpp @@ -0,0 +1,171 @@ +#include "Parser.hpp" +#include "Node.hpp" + +namespace sn +{ + /*explicit*/ Parser::Parser() + { + } + + /*virtual*/ Parser::~Parser() + { + } + + std::shared_ptr Parser::parse(std::vector> + const& tokens) + { + m_cursor = 0; + m_tokens = tokens; + + return parse_doc(); + } + + std::optional> + Parser::consume(NodeType type) + { + auto node = consume(); + + if ((*node)->type() != type) + { + std::stringstream ss; + ss << "expected '" << SN_GET(NodeTypeStr, type, "NODE_") << "'" + << " got '" << SN_GET(NodeTypeStr, (*node)->type(), "NODE_") << "'"; + + throw syntax_error {ss.str()}; + } + + return node; + } + + std::optional> + Parser::consume() + { + if (m_cursor >= m_tokens.size()) + { + return std::nullopt; + } + + auto node = m_tokens[m_cursor]; + m_cursor++; + return node; + } + + bool Parser::type_is(NodeType type, int lookahead/*=0*/) const + { + if (m_cursor + lookahead >= m_tokens.size()) + { + return false; + } + + return m_tokens[m_cursor + lookahead]->type() == type; + } + + std::shared_ptr Parser::parse_doc() + { + auto node = std::make_shared(NODE_DOC); + + while (m_cursor < m_tokens.size()) + { + node->add_child(parse_rule()); + } + + return node; + } + + std::shared_ptr Parser::parse_rule() + { + auto node = std::make_shared(NODE_RULE); + + auto target = parse_target(); + auto deps = parse_deps(); + auto block = parse_block(); + + node->add_child(target); + node->add_child(deps); + node->add_child(block); + + return node; + } + + std::shared_ptr Parser::parse_target() + { + auto node = std::make_shared(NODE_TARGET); + + node->add_child(*consume(NODE_IDENT)); + + while (!type_is(NODE_RARROW)) + { + node->add_child(*consume(NODE_IDENT)); + } + + consume(); + + return node; + } + + std::shared_ptr Parser::parse_deps() + { + auto node = std::make_shared(NODE_DEPS); + + while (!type_is(NODE_OBRACE)) + { + node->add_child(*consume(NODE_IDENT)); + } + + return node; + } + + std::shared_ptr Parser::parse_block() + { + consume(NODE_OBRACE); + auto node = std::make_shared(NODE_BLOCK); + + auto lst = parse_cmd_lst(); + + for (size_t i=0; isize(); i++) + { + node->add_child(*lst->get(i)); + } + + consume(NODE_CBRACE); + + return node; + } + + std::shared_ptr Parser::parse_cmd_lst() + { + auto node = std::make_shared(NODE_CMD_LST); + + if (type_is(NODE_CBRACE)) + { + return node; + } + + node->add_child(parse_cmd()); + + while (type_is(NODE_COMMA)) + { + consume(); + node->add_child(parse_cmd()); + } + + if (type_is(NODE_COMMA)) + { + consume(); + } + + return node; + } + + std::shared_ptr Parser::parse_cmd() + { + auto node = std::make_shared(NODE_CMD); + + while (!type_is(NODE_CBRACE) && !type_is(NODE_COMMA)) + { + node->add_child(*consume(NODE_IDENT)); + } + + return node; + } +} diff --git a/src/Parser.hpp b/src/Parser.hpp new file mode 100644 index 0000000..ee66de2 --- /dev/null +++ b/src/Parser.hpp @@ -0,0 +1,43 @@ +#ifndef sn_PARSER_HPP +#define sn_PARSER_HPP + +#include "commons.hpp" +#include "Node.hpp" + +namespace sn +{ + SN_ERROR(syntax_error); + + /** + * Builds the AST given a list of tokens. + * @see Node + * @see Lexer + **/ + class Parser + { + public: + explicit Parser(); + virtual ~Parser(); + + std::shared_ptr parse(std::vector> + const& tokens); + private: + std::vector> m_tokens; + size_t m_cursor = 0; + + std::optional> consume(NodeType type); + std::optional> consume(); + + bool type_is(NodeType type, int lookahead=0) const; + + std::shared_ptr parse_doc(); + std::shared_ptr parse_rule(); + std::shared_ptr parse_target(); + std::shared_ptr parse_deps(); + std::shared_ptr parse_block(); + std::shared_ptr parse_cmd_lst(); + std::shared_ptr parse_cmd(); + }; +} + +#endif diff --git a/src/State.cpp b/src/State.cpp new file mode 100644 index 0000000..4c8e45f --- /dev/null +++ b/src/State.cpp @@ -0,0 +1,220 @@ +#include "State.hpp" +#include + +namespace sn +{ + /*explicit*/ State::State() + { + sqlite3_open(m_db_path.c_str(), &m_db); + std::stringstream ss; + ss << "CREATE TABLE IF NOT EXISTS Files (" + << "path VARCHAR(1024) PRIMARY KEY, " + << "timestamp VARCHAR(4096))"; + + size_t const SZ = 1024; + char* status[SZ]; + + if (sqlite3_exec(m_db, + ss.str().c_str(), + nullptr, + nullptr, + reinterpret_cast(&status)) != 0) + { + std::cerr << *status << std::endl; + } + } + + /*virtual*/ State::~State() + { + sqlite3_close(m_db); + m_db = nullptr; + } + + void State::init(std::vector const& files) + { + for (auto file: files) + { + file = format_path(file); + + if (!exists(file)) + { + create(file); + } + } + } + + void State::update(std::filesystem::path path) + { + path = format_path(path); + + long long unsigned timestamp = 0; + + if (std::filesystem::exists(path)) + { + timestamp = std::filesystem::last_write_time(path) + .time_since_epoch() + .count(); + } + + std::stringstream ss; + ss << "UPDATE Files SET timestamp = '" + << timestamp + << "' WHERE path = '" + << format_path(path).string() + << "'"; + + size_t const SZ = 256; + char* msg[SZ]; + int status = sqlite3_exec(m_db, ss.str().c_str(), nullptr, nullptr, msg); + + if (status != 0) + { + std::cerr << msg << std::endl; + } + } + + std::vector + State::get_modified_files(std::vector const& files) + { + std::vector result; + + // Get DB timestamp + // ---------------- + auto files_state = load(); + + for (auto file: files) + { + file = format_path(file); + + // Get file timestamp + // ------------------ + unsigned long long timestamp = 0; + + if (std::filesystem::exists(file)) + { + timestamp = + std::filesystem::last_write_time(file) + .time_since_epoch().count(); + } + + + // Compare with DB timestamp + // ------------------------- + if (timestamp != files_state[file] || timestamp == 0) + { + result.push_back(file); + } + } + + return result; + } + + std::filesystem::path + State::format_path(std::filesystem::path path) const + { + if (std::filesystem::exists(path)) + { + return std::filesystem::canonical(path); + } + + return std::filesystem::absolute(path); + } + + bool State::exists(std::filesystem::path file) + { + file = format_path(file); + + std::stringstream ss; + ss << "SELECT * FROM Files WHERE path = '" + << file.string() << "'"; + + int counter = 0; + + sqlite3_exec(m_db, ss.str().c_str(), [](void* data, + int, + char**, + char**){ + int* counter = static_cast(data); + *counter = *counter + 1; + + return 0; + }, &counter, nullptr); + + return counter > 0; + } + + void State::create(std::filesystem::path file) + { + file = format_path(file); + + std::stringstream ss; + + ss << "INSERT INTO Files (path, timestamp) VALUES (" + << file << ", '" << 0 + << "');"; + + size_t const SZ = 1024; + char* msg[SZ]; + + int status = sqlite3_exec(m_db, ss.str().c_str(), + nullptr, nullptr, msg); + if (status != 0) + { + std::cerr << *msg << std::endl; + } + } + + std::unordered_map + State::load() + { + std::stringstream ss; + ss << "SELECT * FROM Files;"; + + size_t const SZ = 1024; + char* msg [SZ]; + + std::unordered_map files_state; + + int err = sqlite3_exec(m_db, ss.str().c_str(), [](void* data, + int argc, + char** argv, + char** col) -> int { + std::filesystem::path path; + long long unsigned timestamp = 0; + auto states = (std::unordered_map*) data; + + for (int i=0; i(msg)); + + if (err != 0) + { + std::cerr << *msg << std::endl; + } + + decltype(files_state) results; + + for (auto& fs: files_state) + { + results.insert({format_path(fs.first), fs.second}); + } + + return results; + } +} diff --git a/src/State.hpp b/src/State.hpp new file mode 100644 index 0000000..264b000 --- /dev/null +++ b/src/State.hpp @@ -0,0 +1,45 @@ +#ifndef sn_STATE_HPP +#define sn_STATE_HPP + +#include + +#include + +#include "commons.hpp" + + +namespace sn +{ + using file_state_t = std::pair; + + /** + * Watches a given set of files. + **/ + class State + { + public: + explicit State(); + virtual ~State(); + + void init(std::vector const& files); + void update(std::filesystem::path path); + + std::vector + get_modified_files(std::vector const& files); + + std::filesystem::path format_path(std::filesystem::path path) const; + + private: + sqlite3* m_db = nullptr; + std::filesystem::path m_db_path = + std::filesystem::temp_directory_path() / "snake.db"; + + bool exists(std::filesystem::path file); + void create(std::filesystem::path file); + + std::unordered_map load(); + }; +} + +#endif diff --git a/src/commons.hpp b/src/commons.hpp new file mode 100644 index 0000000..3e543a9 --- /dev/null +++ b/src/commons.hpp @@ -0,0 +1,34 @@ +#ifndef sn_COMMONS_HPP +#define sn_COMMONS_HPP + +#define SN_GEN_ENUM(X) X +#define SN_GEN_STRING(X) #X + +#define SN_ENUM(PREFIX, KINDS) \ + enum PREFIX { KINDS(SN_GEN_ENUM) }; \ + constexpr char const* PREFIX ## Str [] = { KINDS(SN_GEN_STRING) } + +#define SN_GET(ARRAY, INDEX, TOREM) \ + std::string( ARRAY [INDEX] + strlen(TOREM)) + +#define SN_ERROR(NAME) \ + struct NAME : public std::runtime_error { \ + explicit NAME (std::string const& what): std::runtime_error(what) {} \ + } + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/src/main.cpp b/src/main.cpp index e0871d8..1500ad8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,13 @@ +#include #include #include "snake.hpp" +#include "Interpreter.hpp" +#include "State.hpp" int main(int, char**) { - std::cout << "snake v" << SNAKE_VERSION << std::endl; + sn::Interpreter interpreter; + interpreter.run(); + return 0; } diff --git a/tests/Lexer.cpp b/tests/Lexer.cpp new file mode 100644 index 0000000..c7c7a25 --- /dev/null +++ b/tests/Lexer.cpp @@ -0,0 +1,43 @@ +#include +#include "../src/Lexer.hpp" + +using namespace sn; + +class LexerTest +{ +public: + explicit LexerTest() {} + virtual ~LexerTest() {} + + void test_next(std::string const& oracle) + { + auto tok = m_lexer.next(); + INFO("tok for '" << oracle << "' is nullopt"); + REQUIRE(tok); + + INFO("expected '" << oracle << "', got '" << (*tok)->string() << "'"); + REQUIRE(oracle == (*tok)->string()); + } + + void test_end() + { + auto tok = m_lexer.next(); + INFO("end not found"); + REQUIRE(std::nullopt == tok); + } + +protected: + Lexer m_lexer; +}; + +TEST_CASE_METHOD(LexerTest, "Lexer_rule") +{ + m_lexer.scan("hello.world, -> { }"); + + test_next("IDENT[hello.world]"); + test_next("COMMA"); + test_next("RARROW"); + test_next("OBRACE"); + test_next("CBRACE"); + test_end(); +} diff --git a/tests/Parser.cpp b/tests/Parser.cpp new file mode 100644 index 0000000..41641fd --- /dev/null +++ b/tests/Parser.cpp @@ -0,0 +1,48 @@ +#include +#include "../src/Lexer.hpp" +#include "../src/Parser.hpp" + +using namespace sn; + +class ParserTest +{ +public: + explicit ParserTest() {} + virtual ~ParserTest() {} + + void test_parse(std::string const& oracle, + std::string const& source) + { + Lexer lexer; + lexer.scan(source); + + Parser parser; + auto node = parser.parse(lexer.all()); + + REQUIRE(oracle == node->string()); + } + +protected: +}; + +TEST_CASE_METHOD(ParserTest, "Parser_rule") +{ + std::stringstream ss; + ss << " hello.elf -> hello.cpp {" << std::endl; + ss << " g++ hello.cpp -o hello.elf, " << std::endl; + ss << " ls " << std::endl; + ss << " }" << std::endl; + + test_parse("DOC(RULE(TARGET(" + "IDENT[hello.elf]" + "),DEPS(" + "IDENT[hello.cpp]" + "),BLOCK(" + "CMD(" + "IDENT[g++],IDENT[hello.cpp],IDENT[-o],IDENT[hello.elf]" + "),CMD(" + "IDENT[ls]" + ")" + ")))", + ss.str()); +}