diff --git a/bin/testml-c++ b/bin/testml-c++ new file mode 120000 index 00000000..fea10bc0 --- /dev/null +++ b/bin/testml-c++ @@ -0,0 +1 @@ +../src/cpp/bin/testml-c++ \ No newline at end of file diff --git a/bin/testml-cpp b/bin/testml-cpp new file mode 120000 index 00000000..ce5f2f39 --- /dev/null +++ b/bin/testml-cpp @@ -0,0 +1 @@ +../src/cpp/bin/testml-cpp \ No newline at end of file diff --git a/bin/testml-cpp-tap b/bin/testml-cpp-tap new file mode 120000 index 00000000..9824bd3c --- /dev/null +++ b/bin/testml-cpp-tap @@ -0,0 +1 @@ +../src/cpp/bin/testml-cpp-tap \ No newline at end of file diff --git a/src/cpp/.gitignore b/src/cpp/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/src/cpp/bin/testml-cpp-tap b/src/cpp/bin/testml-cpp-tap index 2c56e78b..fe8fbe60 100755 --- a/src/cpp/bin/testml-cpp-tap +++ b/src/cpp/bin/testml-cpp-tap @@ -15,11 +15,15 @@ testml-run-file() { set -x /usr/bin/env c++ \ -o $testml_runner \ + -std=c++14 -Wall -Wextra \ + -I ext \ $src_bin \ $lib/run/tap.cpp \ - $lib/run.cpp \ + $lib/runtime.cpp \ $lib/stdlib.cpp \ $lib/bridge.cpp \ + $lib/wrapper.cpp \ + $lib/utils.cpp \ $bridge_file || return chmod +x $testml_runner diff --git a/src/cpp/ext b/src/cpp/ext new file mode 120000 index 00000000..b9a1f900 --- /dev/null +++ b/src/cpp/ext @@ -0,0 +1 @@ +../../ext/cpp \ No newline at end of file diff --git a/src/cpp/lib/testml/bridge.cpp b/src/cpp/lib/testml/bridge.cpp index e69de29b..9074a8b5 100644 --- a/src/cpp/lib/testml/bridge.cpp +++ b/src/cpp/lib/testml/bridge.cpp @@ -0,0 +1,15 @@ +#include "bridge.hpp" + +namespace testml { + + using json = nlohmann::json; + + json Bridge::call(std::string const& name, std::vector const& args) { + auto it = _fns.find(name); + if (it == _fns.end()) { + throw std::runtime_error("Bridge method not found: " + name + "."); + } + return it->second->call(args); + } + +} diff --git a/src/cpp/lib/testml/bridge.hpp b/src/cpp/lib/testml/bridge.hpp new file mode 100644 index 00000000..79988b28 --- /dev/null +++ b/src/cpp/lib/testml/bridge.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "boost/callable_traits/args.hpp" +#include "boost/callable_traits/class_of.hpp" +#include "boost/callable_traits/return_type.hpp" +#include "nlohmann/json.hpp" + +#include "utils.hpp" +#include "wrapper.hpp" + +namespace testml { + + namespace details { + + using json = nlohmann::json; + using wrapper::cook; + using wrapper::uncook; + + namespace ct = boost::callable_traits; + + // we need this details class so that we can have a non-templated value + // stored in the Bridge _fns map. + struct FnHolder { + virtual json call(std::vector const&) = 0; + }; + + // the implementation of a FnHolder, which keeps the types around + template + class FnHolderImpl : public FnHolder { + Fn _fn; + BridgeT* _bridge; + static constexpr bool _is_pmf = std::is_member_function_pointer::value; + using RawArg = ct::args_t; + // in case of a PMF, remove the class type from the argument list + using Arg = std::conditional_t<_is_pmf, typename utils::remove_first_type::type, RawArg>; + using Ret = ct::return_type_t; + static constexpr std::size_t _num_args = std::tuple_size::value; + + // type of the N-th argument that the stored function takes + template + using ArgType = typename std::tuple_element::type; + + // uncook each argument to its expected type, and call the function + // we do SFINAE in the return type, using comma+sizeof() to get a dependance on I. + + // PMF case + template + auto call_impl(std::vector const& args, std::index_sequence) + -> typename std::enable_if<(sizeof...(I), _is_pmf), Ret>::type { + return (_bridge->*_fn)(uncook>(args[I])...); + } + + // non-PMF case (BridgeT = nullptr_t) + template + auto call_impl(std::vector const& args, std::index_sequence) + -> typename std::enable_if<(sizeof...(I), !_is_pmf), Ret>::type { + return _fn(uncook>(args[I])...); + } + + public: + FnHolderImpl(BridgeT* bridge, Fn fn) + : _fn{std::move(fn)}, + _bridge{bridge} { + } + + // check arity and call the function using our little helper, before wrapping it back to json + json call(std::vector const& args) override { + if (args.size() != _num_args) { + throw std::runtime_error("Bridge method call with wrong arity, expected " + std::to_string(_num_args) + ", got " + std::to_string(args.size()) + "."); + } + + return cook(call_impl(args, std::make_index_sequence<_num_args>{})); + } + + }; + + } + + class Bridge { + // store a wrapper FnHolder in the map, with FnHolderImpl to keep the correct types around and do FFI correctly + std::unordered_map> _fns; + + public: + // PMF version, takes the object to call the function on + template + auto bind(std::string const& name, BridgeT* obj, Fn fn) + -> typename std::enable_if::value, void>::type { + static_assert(std::is_same, BridgeT>::value, "Bridge subclass must pass itself"); + + using HolderType = details::FnHolderImpl; + _fns[name] = std::make_unique(obj, std::move(fn)); + } + + // any other candidate + template + auto bind(std::string const& name, Fn fn) + -> typename std::enable_if::value, void>::type { + using HolderType = details::FnHolderImpl; + _fns[name] = std::make_unique(nullptr, std::move(fn)); + } + + nlohmann::json call(std::string const& name, std::vector const& args); + }; + +} diff --git a/src/cpp/lib/testml/run.cpp b/src/cpp/lib/testml/run.cpp deleted file mode 100644 index 11ef8dfb..00000000 --- a/src/cpp/lib/testml/run.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include -#include -#include -#include "run.h" - -TestML_Run& TestML_Run::from_file(std::string file) { - std::ifstream ifs(file); - std::string content( (std::istreambuf_iterator(ifs) ), - (std::istreambuf_iterator() ) ); - - // this.version = ... - // this.code = ... - // this.data = ... - return *this; -} diff --git a/src/cpp/lib/testml/run.h b/src/cpp/lib/testml/run.h deleted file mode 100644 index 5812c4b2..00000000 --- a/src/cpp/lib/testml/run.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -class TestML_Run { - public: - TestML_Run () {} - TestML_Run& from_file (std::string file); - private: - std::string version; - // typename code; - // typename data; -}; diff --git a/src/cpp/lib/testml/run/tap.cpp b/src/cpp/lib/testml/run/tap.cpp index 683c3fea..c215fe50 100644 --- a/src/cpp/lib/testml/run/tap.cpp +++ b/src/cpp/lib/testml/run/tap.cpp @@ -1,12 +1,37 @@ -#include #include -#include "tap.h" +#include +#include "tap.hpp" + +namespace testml { +namespace run { -void TestML_Run_TAP::run(std::string file) { - TestML_Run_TAP tap; + void TAP::testml_eq(json want, json got, std::string const& label) { + if (want == got) { + tap_pass(label); + } else { + tap_fail(label); + } + } - tap.from_file(file); + void TAP::tap_pass(std::string const& label) { + std::cout << "ok " << ++count; + if (!label.empty()) { + std::cout << " - " << label; + } + std::cout << "\n"; + } - std::cout << "1..1" << std::endl; - std::cout << "ok 1 - It worked" << std::endl; + void TAP::tap_fail(std::string const& label) { + std::cout << "not ok " << ++count; + if (!label.empty()) { + std::cout << " - " << label; + } + std::cout << "\n"; + } + + void TAP::testml_end() { + std::cout << "1.." << count; + } + +} } diff --git a/src/cpp/lib/testml/run/tap.h b/src/cpp/lib/testml/run/tap.h deleted file mode 100644 index 9a16fe20..00000000 --- a/src/cpp/lib/testml/run/tap.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "tap.h" -#include "../run.h" - -#include - -class TestML_Run_TAP :public TestML_Run { - public: - TestML_Run_TAP(): TestML_Run() {} - static void run(std::string file); -}; diff --git a/src/cpp/lib/testml/run/tap.hpp b/src/cpp/lib/testml/run/tap.hpp new file mode 100644 index 00000000..0127c498 --- /dev/null +++ b/src/cpp/lib/testml/run/tap.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "../runtime.hpp" + +#include + +namespace testml { +namespace run { + + class TAP : public Runtime { + using json = nlohmann::json; + + using Runtime::Runtime; + + protected: + void testml_eq(json want, json got, std::string const& label) override; + void testml_end() override; + + private: + void tap_pass(std::string const& label); + void tap_fail(std::string const& label); + + private: + int count = 0; + }; + +} +} diff --git a/src/cpp/lib/testml/runtime.cpp b/src/cpp/lib/testml/runtime.cpp new file mode 100644 index 00000000..16203f5d --- /dev/null +++ b/src/cpp/lib/testml/runtime.cpp @@ -0,0 +1,144 @@ +#include +#include +#include // TODO remove +#include +#include +#include +#include + +#include "runtime.hpp" +#include "bridge.hpp" +#include "utils.hpp" + +[[noreturn]] static void NYI(std::string const& add = "") { + throw std::runtime_error("Not Yet Implemented, sorry! " + add); +} + +using json = nlohmann::json; + +namespace testml { + + Runtime::Runtime(std::string const& filename, Bridge& bridge) + : _bridge{bridge} { + + std::ifstream stream(filename); + stream >> _ast; + + _data = _ast["data"]; + } + + json Runtime::exec(json expr) { + json executed = exec_expr(expr); + return executed.size() > 0 ? executed[0] : static_cast(nullptr); + } + + json Runtime::exec_expr(json expr) { + if (!expr.is_array() || expr.size() == 0 || !expr[0].is_string()) + return json::array_t{expr}; + + std::string opcode = expr[0]; + expr.erase(expr.begin()); // pop first arg + json val; + // TODO vtable + if (opcode == "%<>") { + each_pick(expr[0], expr[1]); + return {}; // no return value + } else if (opcode == "==") { + assert_eq(expr[0], expr[1], expr.size() == 3 ? expr[2] : ""); + return {}; // no return value + } else if (opcode == ".") { + val = exec_dot(expr); + } else if (opcode == "*") { + val = get_point(expr[0]); + } else if (utils::is_all_lowercase(opcode)) { + val = call_bridge(opcode, expr); + } else if (utils::is_all_uppercase(opcode)) { + NYI("std lib"); + } else if (true) { + // TODO func + NYI(opcode); + } else { + throw std::runtime_error("Can't resolve TestML function"); + } + return val.is_null() ? json::array_t{} : json::array_t{val}; + } + + json Runtime::call_bridge(std::string const& name, json::array_t args) { + std::vector transformed; + std::transform(args.begin(), args.end(), std::back_inserter(transformed), + [this](json& j) { return exec(j); }); + return _bridge.call(name, transformed); + } + + void Runtime::assert_eq(json::array_t left, json::array_t right, std::string const& label) { + testml_eq(exec_expr(left), exec_expr(right), label); + } + + json Runtime::get_point(std::string const& name) { + return _currentBlock["point"][name]; + } + + json Runtime::exec_dot(json::array_t calls) { + json context = {}; // result of last call + + for (auto call : calls) { + // add context right after the opcode + call.insert(call.begin() + 1 /* after opcode */, context.begin(), context.end()); + // we now have the full argument list + context = exec_expr(call); + } + return context[0]; + } + + void Runtime::each_pick(json::array_t list, json::array_t expr) { + for (auto& datum : _data) { + _currentBlock = datum; + // TODO block.point.ONLY + + pick_exec(list, expr); + } + _currentBlock = {}; + } + + void Runtime::pick_exec(json::array_t list, json::array_t expr) { + // check whether we should run or not + auto& points = _currentBlock["point"]; + for (json::string_t str : list) { + if (!str.compare(0, 1, "*") && points.find(str.substr(1)) == points.end()) { + return; + } + if (!str.compare(0, 2, "!*") && points.find(str.substr(2)) != points.end()) { + return; + } + } + + // if we didn't return beforehand, we're safe to run the expression + if (is_function(expr)) { + // exec_func + NYI(); + } else { + exec_expr(expr); + } + } + + bool Runtime::is_function(json value) { + if (!value.is_object()) { + return false; + } + if (value.size() < 1) { + return false; + } + return value[0].is_string() && value[0] == "=>"; + } + + void Runtime::test() { + for (auto& statement : _ast["code"]) { + exec_expr(statement); + } + testml_end(); + } + + Runtime::~Runtime() { + } + +} diff --git a/src/cpp/lib/testml/runtime.hpp b/src/cpp/lib/testml/runtime.hpp new file mode 100644 index 00000000..d423444d --- /dev/null +++ b/src/cpp/lib/testml/runtime.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include "../../ext/nlohmann/json.hpp" + +#include "bridge.hpp" + +namespace testml { + + class Runtime { + using json = nlohmann::json; + + public: + Runtime(std::string const& filename, Bridge&); + virtual ~Runtime() = 0; + + void test(); + + private: + // toplevel methods + void each_pick(json::array_t list, json::array_t expr); + void pick_exec(json::array_t list, json::array_t expr); + + private: + // other methods + json exec(json expr); + json exec_expr(json expr); + json call_bridge(std::string const& name, json::array_t args); + json get_point(std::string const& name); + + private: + void assert_eq(json::array_t got, json::array_t want, std::string const& label); + + private: + json exec_dot(json::array_t calls); + + private: + bool is_function(json value); + + protected: + // those methods are to be overriden by the runtime class implementing + virtual void testml_eq(json want, json got, std::string const& label) = 0; + virtual void testml_end() = 0; + + private: + Bridge& _bridge; + + json _ast; + json _data; + json _currentBlock; /* TODO ptr or smth */ + }; + +} diff --git a/src/cpp/lib/testml/utils.cpp b/src/cpp/lib/testml/utils.cpp new file mode 100644 index 00000000..0a7e546f --- /dev/null +++ b/src/cpp/lib/testml/utils.cpp @@ -0,0 +1,19 @@ +#include "utils.hpp" + +namespace testml { +namespace utils { + + bool is_all_lowercase(std::string const& s) { + return std::all_of(s.begin(), s.end(), + [](char c) { return std::islower(c); } + ); + } + + bool is_all_uppercase(std::string const& s) { + return std::all_of(s.begin(), s.end(), + [](char c) { return std::isupper(c); } + ); + } + +} +} diff --git a/src/cpp/lib/testml/utils.hpp b/src/cpp/lib/testml/utils.hpp new file mode 100644 index 00000000..d3be3a9e --- /dev/null +++ b/src/cpp/lib/testml/utils.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace testml { +namespace utils { + + bool is_all_lowercase(std::string const& s); + bool is_all_uppercase(std::string const& s); + + template + struct remove_first_type + { + }; + + template + struct remove_first_type> + { + typedef std::tuple type; + }; + + template<> + struct remove_first_type> + { + typedef std::tuple<> type; + }; + +} +} diff --git a/src/cpp/lib/testml/wrapper.cpp b/src/cpp/lib/testml/wrapper.cpp new file mode 100644 index 00000000..b8728a57 --- /dev/null +++ b/src/cpp/lib/testml/wrapper.cpp @@ -0,0 +1,29 @@ +#include "wrapper.hpp" + +namespace testml { +namespace wrapper { + + using json = nlohmann::json; + + template<> + nlohmann::json cook(std::string s) { + return nlohmann::json::string_t{s}; + } + + template<> + nlohmann::json cook(int i) { + return nlohmann::json::number_integer_t{i}; + } + + template<> + std::string uncook(nlohmann::json s) { + return s; + } + + template<> + int uncook(nlohmann::json i) { + return i; + } + +} +} diff --git a/src/cpp/lib/testml/wrapper.hpp b/src/cpp/lib/testml/wrapper.hpp new file mode 100644 index 00000000..c288ebdb --- /dev/null +++ b/src/cpp/lib/testml/wrapper.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "../../ext/nlohmann/json.hpp" + +namespace testml { +namespace wrapper { + + template + nlohmann::json cook(T); + + template + T uncook(nlohmann::json); + + +} +} diff --git a/src/cpp/src/testml-run.cpp b/src/cpp/src/testml-run.cpp index dac4c191..55a22aa1 100644 --- a/src/cpp/src/testml-run.cpp +++ b/src/cpp/src/testml-run.cpp @@ -1,15 +1,18 @@ #include -#include "../lib/testml/run/tap.h" +#include +#include "../lib/testml/run/tap.hpp" +#include "../test/testml-bridge.hpp" -int main(int argc, char* argv[]) { +int main(int, char* argv[]) { - std::string file(argv[1]); -// TestML_Run_TAP tap; -// std::string f("asdf"); - - TestML_Run_TAP::run(file); -// tap.run(); + TestMLBridge bridge; + try { + testml::run::TAP tap{argv[1], bridge}; + tap.test(); + } catch (std::exception& e) { + std::cout << "exception thrown: " << e.what() << std::endl; + } return 0; } diff --git a/src/cpp/test/testml-bridge.cpp b/src/cpp/test/testml-bridge.cpp index e69de29b..8a6522f7 100644 --- a/src/cpp/test/testml-bridge.cpp +++ b/src/cpp/test/testml-bridge.cpp @@ -0,0 +1,13 @@ +#include "testml-bridge.hpp" + +TestMLBridge::TestMLBridge() { + bind("add", this, &TestMLBridge::add); + bind("sub", [](int a, int b) { + return a - b; + }); +} + +int TestMLBridge::add(int a, int b) { + return a + b; +} + diff --git a/src/cpp/test/testml-bridge.h b/src/cpp/test/testml-bridge.h deleted file mode 100644 index e69de29b..00000000 diff --git a/src/cpp/test/testml-bridge.hpp b/src/cpp/test/testml-bridge.hpp new file mode 100644 index 00000000..0bf44e8a --- /dev/null +++ b/src/cpp/test/testml-bridge.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "../lib/testml/bridge.hpp" + +class TestMLBridge : public testml::Bridge { +public: + TestMLBridge(); + +public: + int add(int a, int b); +};