From 5d51d1672f8a7c10f845408860a7d6b4dfe93fc3 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Tue, 17 May 2022 02:35:51 +0200 Subject: [PATCH] Scoped variables, single namespace variables and functions --- Makefile | 1 + examples/variables.mq | 2 + src/environment.cc | 49 ++++++++++++++++++++ src/errors.cc | 10 +++++ src/interpreter.cc | 49 +++++++++++++------- src/musique.hh | 97 +++++++++++++++++++++++++++++++++++++++- src/parser.cc | 8 ++-- src/tests/environment.cc | 80 +++++++++++++++++++++++++++++++++ src/tests/interpreter.cc | 15 +++++++ src/tests/value.cc | 35 +++++++++++++++ src/value.cc | 36 ++++++++++++++- 11 files changed, 359 insertions(+), 23 deletions(-) create mode 100644 examples/variables.mq create mode 100644 src/environment.cc create mode 100644 src/tests/environment.cc create mode 100644 src/tests/value.cc diff --git a/Makefile b/Makefile index d448178..d7793f2 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch -Werror=return-typ CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -Ilib/ut/ -Isrc/ Obj= \ + bin/environment.o \ bin/errors.o \ bin/interpreter.o \ bin/lexer.o \ diff --git a/examples/variables.mq b/examples/variables.mq new file mode 100644 index 0000000..cbcd8ee --- /dev/null +++ b/examples/variables.mq @@ -0,0 +1,2 @@ +var x = 10; +say (x + 1) diff --git a/src/environment.cc b/src/environment.cc new file mode 100644 index 0000000..6077cff --- /dev/null +++ b/src/environment.cc @@ -0,0 +1,49 @@ +#include + +std::vector *Env::pool = nullptr; + +Env& Env::force_define(std::string name, Value new_value) +{ + variables.insert_or_assign(std::move(name), std::move(new_value)); + return *this; +} + +Env& Env::parent() +{ + return (*pool)[parent_enviroment_id]; +} + +Value* Env::find(std::string const& name) +{ + for (Env *prev = nullptr, *env = this; env != prev; prev = std::exchange(env, &env->parent())) { + if (auto it = env->variables.find(name); it != env->variables.end()) { + return &it->second; + } + } + return nullptr; +} + +usize Env::operator++() const +{ + auto const parent_id = this - pool->data(); + auto const free = std::find_if(pool->begin(), pool->end(), [](Env const& env) { return env.parent_enviroment_id == Env::Unused; }); + Env* next = free == pool->end() + ? &pool->emplace_back() + : &*free; + + next->parent_enviroment_id = parent_id; + return next - pool->data(); +} + +usize Env::operator--() +{ + if (this == pool->data()) + return 0; + variables.clear(); + return std::exchange(parent_enviroment_id, Unused); +} + +Env& Env::global() +{ + return (*pool)[0]; +} diff --git a/src/errors.cc b/src/errors.cc index e7bf707..cd8f562 100644 --- a/src/errors.cc +++ b/src/errors.cc @@ -71,6 +71,7 @@ std::ostream& operator<<(std::ostream& os, Error const& err) case errors::Expected_Keyword: case errors::Function_Not_Defined: + case errors::Not_Callable: case errors::Unexpected_Token_Type: case errors::Unresolved_Operator: return os << err.message << '\n'; @@ -173,6 +174,15 @@ Error errors::expected_keyword(Token const& unexpected, std::string_view keyword return err; } +Error errors::not_callable(std::optional location, Value::Type value_type) +{ + Error err; + err.type = errors::Not_Callable; + err.location = std::move(location); + err.message = format("Couldn't call value of type ", type_name(value_type)); + return err; +} + void errors::all_tokens_were_not_parsed(std::span tokens) { error_heading(std::cerr, std::nullopt, Error_Level::Bug); diff --git a/src/interpreter.cc b/src/interpreter.cc index 93e6252..96fe4c1 100644 --- a/src/interpreter.cc +++ b/src/interpreter.cc @@ -4,7 +4,7 @@ static auto numeric_binary_operator(auto binop) { - return [binop = std::move(binop)](std::vector args) { + return [binop = std::move(binop)](std::vector args) -> Result { auto result = std::move(args.front()); for (auto &v : std::span(args).subspan(1)) { assert(result.type == Value::Type::Number, "LHS should be a number"); @@ -20,20 +20,26 @@ Interpreter::Interpreter() { } +Interpreter::~Interpreter() +{ + Env::pool = nullptr; +} + Interpreter::Interpreter(std::ostream& out) : out(out) { - functions["typeof"] = [](std::vector args) -> Value { - assert(args.size() == 1, "typeof expects only one argument"); - switch (args.front().type) { - case Value::Type::Nil: return Value::symbol("nil"); - case Value::Type::Number: return Value::symbol("number"); - case Value::Type::Symbol: return Value::symbol("symbol"); - } - unreachable(); - }; + assert(Env::pool == nullptr, "Only one instance of interpreter can be at one time"); + Env::pool = &env_pool; - functions["say"] = [&out](std::vector args) -> Value { + auto &global = env_pool.emplace_back(); + global.parent_enviroment_id = 0; + + global.force_define("typeof", [](std::vector args) -> Value { + assert(args.size() == 1, "typeof expects only one argument"); + return Value::symbol(std::string(type_name(args.front().type))); + }); + + global.force_define("say", [&out](std::vector args) -> Value { for (auto it = args.begin(); it != args.end(); ++it) { out << *it; if (std::next(it) != args.end()) @@ -41,7 +47,7 @@ Interpreter::Interpreter(std::ostream& out) } out << '\n'; return {}; - }; + }); operators["+"] = numeric_binary_operator(std::plus<>{}); operators["-"] = numeric_binary_operator(std::minus<>{}); @@ -49,6 +55,16 @@ Interpreter::Interpreter(std::ostream& out) operators["/"] = numeric_binary_operator(std::divides<>{}); } +Env& Interpreter::env() +{ + return env_pool[current_env]; +} + +Env const& Interpreter::env() const +{ + return env_pool[current_env]; +} + Result Interpreter::eval(Ast &&ast) { switch (ast.type) { @@ -84,15 +100,18 @@ Result Interpreter::eval(Ast &&ast) case Ast::Type::Call: { + auto location = ast.arguments.front().location; Value func = Try(eval(std::move(ast.arguments.front()))); - assert(func.type == Value::Type::Symbol, "Currently only symbols can be called"); - if (auto it = functions.find(func.s); it != functions.end()) { + if (func.type != Value::Type::Symbol) + return errors::not_callable(std::move(location), func.type); + + if (auto body = env().find(func.s); body) { std::vector values; values.reserve(ast.arguments.size()); for (auto& a : std::span(ast.arguments).subspan(1)) { values.push_back(Try(eval(std::move(a)))); } - return it->second(std::move(values)); + return (*body)(std::move(values)); } else { return errors::function_not_defined(func); } diff --git a/src/musique.hh b/src/musique.hh index 8691f82..25d0bee 100644 --- a/src/musique.hh +++ b/src/musique.hh @@ -48,6 +48,7 @@ namespace errors Function_Not_Defined, Unresolved_Operator, Expected_Keyword, + Not_Callable }; } @@ -113,6 +114,15 @@ struct Error std::ostream& operator<<(std::ostream& os, Error const& err); +template typename Template, typename> +struct is_template : std::false_type {}; + +template typename Template, typename ...T> +struct is_template> : std::true_type {}; + +template typename Template, typename T> +constexpr auto is_template_v = is_template::value; + template struct [[nodiscard("This value may contain critical error, so it should NOT be ignored")]] Result : tl::expected { @@ -149,6 +159,23 @@ struct [[nodiscard("This value may contain critical error, so it should NOT be i return Storage::value(); } } + + inline tl::expected to_expected() && + { + return *static_cast(this); + } + + template + requires is_template_v> + auto and_then(Map &&map) && + { + return std::move(*static_cast(this)).and_then( + [map = std::forward(map)](T &&value) { + return std::move(map)(std::move(value)).to_expected(); + }); + } + + using Storage::and_then; }; // NOTE This implementation requires C++ language extension: statement expressions @@ -390,22 +417,54 @@ struct Number std::ostream& operator<<(std::ostream& os, Number const& num); +struct Value; + +using Function = std::function(std::vector)>; + +template +constexpr auto is_one_of = (std::is_same_v || ...); + // TODO Add location struct Value { static Result from(Token t); static Value number(Number n); static Value symbol(std::string s); + static Value lambda(Function f); enum class Type { Nil, Number, Symbol, + Lambda, }; + Value() = default; + Value(Value const&) = default; + Value(Value &&) = default; + Value& operator=(Value const&) = default; + Value& operator=(Value &&) = default; + + template + requires (!std::is_same_v, Value>) + && std::invocable> + && is_one_of>, Value, Result> + inline Value(Callable &&callable) + : type{Type::Lambda} + { + if constexpr (std::is_same_v, std::invoke_result_t>>) { + f = std::move(callable); + } else { + f = [fun = std::move(callable)](std::vector args) -> Result { + return fun(std::move(args)); + }; + } + } + Type type = Type::Nil; Number n{}; + Function f{}; // TODO Most strings should not be allocated by Value, but reference to string allocated previously // Wrapper for std::string is needed that will allocate only when needed, middle ground between: @@ -413,24 +472,56 @@ struct Value // std::string_view - not-owning string type std::string s{}; + Result operator()(std::vector args); + bool operator==(Value const& other) const; }; +std::string_view type_name(Value::Type t); + std::ostream& operator<<(std::ostream& os, Value const& v); -using Function = std::function)>; +struct Env +{ + static std::vector *pool; + std::unordered_map variables; + usize parent_enviroment_id; + + Env() = default; + Env(Env const&) = delete; + Env(Env &&) = default; + Env& operator=(Env const&) = delete; + Env& operator=(Env &&) = default; + + static Env& global(); + + /// Defines new variable regardless of it's current existance + Env& force_define(std::string name, Value new_value); + Env& parent(); + Value* find(std::string const& name); + + usize operator++() const; + usize operator--(); + + static constexpr decltype(Env::parent_enviroment_id) Unused = -1; +}; struct Interpreter { std::ostream &out; - std::unordered_map functions; std::unordered_map operators; + std::vector env_pool; + usize current_env = 0; Interpreter(); + ~Interpreter(); Interpreter(std::ostream& out); Interpreter(Interpreter const&) = delete; Interpreter(Interpreter &&) = default; + Env& env(); + Env const& env() const; + Result eval(Ast &&ast); }; @@ -449,6 +540,8 @@ namespace errors Error unresolved_operator(Token const& op); Error expected_keyword(Token const& unexpected, std::string_view keyword); + Error not_callable(std::optional location, Value::Type value_type); + [[noreturn]] void all_tokens_were_not_parsed(std::span); } diff --git a/src/parser.cc b/src/parser.cc index e238848..0b431ee 100644 --- a/src/parser.cc +++ b/src/parser.cc @@ -110,23 +110,23 @@ Result Parser::parse_atomic_expression() token_id = start; } - return parse_sequence().and_then([&](Ast ast) -> tl::expected { + return parse_sequence().and_then([&](Ast &&ast) -> Result { Try(ensure(Token::Type::Close_Block)); consume(); - return Ast::block(opening.location, ast, std::move(parameters)); + return Ast::block(opening.location, std::move(ast), std::move(parameters)); }); } case Token::Type::Open_Paren: consume(); - return parse_expression().and_then([&](Ast ast) -> tl::expected { + return parse_expression().and_then([&](Ast ast) -> Result { Try(ensure(Token::Type::Close_Paren)); consume(); return ast; }); default: - return peek().and_then([](auto const& token) -> tl::expected { + return peek().and_then([](auto const& token) -> Result { return tl::unexpected(errors::unexpected_token(token)); }); } diff --git a/src/tests/environment.cc b/src/tests/environment.cc new file mode 100644 index 0000000..2ff31bf --- /dev/null +++ b/src/tests/environment.cc @@ -0,0 +1,80 @@ +#include +#include + +using namespace boost::ut; +using namespace std::string_view_literals; + +static void equals( + Value *received, + Value expected, + reflection::source_location sl = reflection::source_location::current()) +{ + expect(received != nullptr, sl) << "Value was not found"; + expect(eq(*received, expected), sl); +} + +suite environment_test = [] { + "Global enviroment exists"_test = [] { + Interpreter i; + expect(eq(&i.env_pool.front(), &Env::global())); + }; + + "Environment scoping"_test = [] { + should("nested scoping preserve outer scope") = [] { + Interpreter i; + + i.env().force_define("x", Value::number(Number(10))); + i.env().force_define("y", Value::number(Number(20))); + + equals(i.env().find("x"), Value::number(Number(10))); + equals(i.env().find("y"), Value::number(Number(20))); + + i.current_env = ++i.env(); + i.env().force_define("x", Value::number(Number(30))); + equals(i.env().find("x"), Value::number(Number(30))); + equals(i.env().find("y"), Value::number(Number(20))); + + i.current_env = --i.env(); + equals(i.env().find("x"), Value::number(Number(10))); + equals(i.env().find("y"), Value::number(Number(20))); + }; + + should("nested variables missing from outer scope") = [] { + Interpreter i; + + i.current_env = ++i.env(); + i.env().force_define("x", Value::number(Number(30))); + equals(i.env().find("x"), Value::number(Number(30))); + + i.current_env = --i.env(); + expect(eq(i.env().find("x"), nullptr)); + }; + + should("stay at global scope when too much end of scope is applied") = [] { + Interpreter i; + i.current_env = --i.env(); + expect(eq(&i.env(), &Env::global())); + }; + + should("reuse already allocated enviroments") = [] { + Interpreter i; + + i.current_env = ++i.env(); + auto const first_env = &i.env(); + i.env().force_define("x", Value::number(Number(30))); + equals(i.env().find("x"), Value::number(Number(30))); + + i.current_env = --i.env(); + expect(eq(i.env().find("x"), nullptr)); + + i.current_env = ++i.env(); + expect(eq(first_env, &i.env())); + expect(eq(i.env().find("x"), nullptr)); + i.env().force_define("x", Value::number(Number(30))); + equals(i.env().find("x"), Value::number(Number(30))); + + i.current_env = --i.env(); + expect(eq(i.env().find("x"), nullptr)); + }; + }; +}; diff --git a/src/tests/interpreter.cc b/src/tests/interpreter.cc index f4bbe25..8f93e59 100644 --- a/src/tests/interpreter.cc +++ b/src/tests/interpreter.cc @@ -59,5 +59,20 @@ suite intepreter_test = [] { produces_output("say 1; say 2; say 3", "1\n2\n3\n"); produces_output("say 1 2 3", "1 2 3\n"); }; + + should("allows only for calling which is callable") = [] { + Interpreter i; + { + auto result = Parser::parse("10 20", "test").and_then([&](Ast &&ast) { return i.eval(std::move(ast)); }); + expect(!result.has_value()) << "Expected code to have failed"; + expect(eq(result.error().type, errors::Not_Callable)); + } + { + i.env().force_define("call_me", Value::number(Number(10))); + auto result = Parser::parse("call_me 20", "test").and_then([&](Ast &&ast) { return i.eval(std::move(ast)); }); + expect(!result.has_value()) << "Expected code to have failed"; + expect(eq(result.error().type, errors::Not_Callable)); + } + }; }; }; diff --git a/src/tests/value.cc b/src/tests/value.cc new file mode 100644 index 0000000..dd440c0 --- /dev/null +++ b/src/tests/value.cc @@ -0,0 +1,35 @@ +#include +#include + +using namespace boost::ut; +using namespace std::string_view_literals; + +static std::string str(auto const& printable) +{ + std::stringstream ss; + ss << printable; + return std::move(ss).str(); +} + +suite value_test = [] { + "Comparisons"_test = [] { + should("be always not equal for lambdas") = [] { + expect(neq(Value::lambda(nullptr), Value::lambda(nullptr))); + }; + + should("are always not equal when types differ") = [] { + expect(neq(Value::symbol("0"), Value::number(Number(0)))); + }; + }; + + "Value printing"_test = [] { + expect(eq("nil"sv, str(Value{}))); + + expect(eq("10"sv, str(Value::number(Number(10))))); + expect(eq("1/2"sv, str(Value::number(Number(2, 4))))); + + expect(eq("foo"sv, str(Value::symbol("foo")))); + + expect(eq(""sv, str(Value::lambda(nullptr)))); + }; +}; diff --git a/src/value.cc b/src/value.cc index 6e137c6..bd492cb 100644 --- a/src/value.cc +++ b/src/value.cc @@ -20,7 +20,7 @@ Value Value::number(Number n) { Value v; v.type = Type::Number; - v.n = n; + v.n = std::move(n).simplify(); return v; } @@ -28,10 +28,27 @@ Value Value::symbol(std::string s) { Value v; v.type = Type::Symbol; - v.s = s; + v.s = std::move(s); return v; } +Value Value::lambda(Function f) +{ + Value v; + v.type = Type::Lambda; + v.f = std::move(f); + return v; +} + +Result Value::operator()(std::vector args) +{ + if (type == Type::Lambda) { + return f(std::move(args)); + } + // TODO Fill location + return errors::not_callable(std::nullopt, type); +} + bool Value::operator==(Value const& other) const { if (type != other.type) @@ -41,6 +58,7 @@ bool Value::operator==(Value const& other) const case Type::Nil: return true; case Type::Number: return n == other.n; case Type::Symbol: return s == other.s; + case Type::Lambda: return false; // TODO Reconsider if functions are comparable } unreachable(); @@ -57,6 +75,20 @@ std::ostream& operator<<(std::ostream& os, Value const& v) case Value::Type::Symbol: return os << v.s; + + case Value::Type::Lambda: + return os << ""; + } + unreachable(); +} + +std::string_view type_name(Value::Type t) +{ + switch (t) { + case Value::Type::Lambda: return "lambda"; + case Value::Type::Nil: return "nil"; + case Value::Type::Number: return "number"; + case Value::Type::Symbol: return "symbol"; } unreachable(); }