Scoped variables, single namespace variables and functions

This commit is contained in:
Robert Bendun 2022-05-17 02:35:51 +02:00
parent 0f9c46dffb
commit 5d51d1672f
11 changed files with 359 additions and 23 deletions

View File

@ -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 \

2
examples/variables.mq Normal file
View File

@ -0,0 +1,2 @@
var x = 10;
say (x + 1)

49
src/environment.cc Normal file
View File

@ -0,0 +1,49 @@
#include <musique.hh>
std::vector<Env> *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];
}

View File

@ -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> 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<Token> tokens)
{
error_heading(std::cerr, std::nullopt, Error_Level::Bug);

View File

@ -4,7 +4,7 @@
static auto numeric_binary_operator(auto binop)
{
return [binop = std::move(binop)](std::vector<Value> args) {
return [binop = std::move(binop)](std::vector<Value> args) -> Result<Value> {
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<Value> 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<Value> args) -> Value {
auto &global = env_pool.emplace_back();
global.parent_enviroment_id = 0;
global.force_define("typeof", [](std::vector<Value> 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<Value> 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<Value> Interpreter::eval(Ast &&ast)
{
switch (ast.type) {
@ -84,15 +100,18 @@ Result<Value> 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<Value> 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);
}

View File

@ -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<template<typename ...> typename Template, typename>
struct is_template : std::false_type {};
template<template<typename ...> typename Template, typename ...T>
struct is_template<Template, Template<T...>> : std::true_type {};
template<template<typename ...> typename Template, typename T>
constexpr auto is_template_v = is_template<Template, T>::value;
template<typename T>
struct [[nodiscard("This value may contain critical error, so it should NOT be ignored")]] Result : tl::expected<T, Error>
{
@ -149,6 +159,23 @@ struct [[nodiscard("This value may contain critical error, so it should NOT be i
return Storage::value();
}
}
inline tl::expected<T, Error> to_expected() &&
{
return *static_cast<Storage*>(this);
}
template<typename Map>
requires is_template_v<Result, std::invoke_result_t<Map, T&&>>
auto and_then(Map &&map) &&
{
return std::move(*static_cast<Storage*>(this)).and_then(
[map = std::forward<Map>(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<Result<Value>(std::vector<Value>)>;
template<typename T, typename ...XS>
constexpr auto is_one_of = (std::is_same_v<T, XS> || ...);
// TODO Add location
struct Value
{
static Result<Value> 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<typename Callable>
requires (!std::is_same_v<std::remove_cvref_t<Callable>, Value>)
&& std::invocable<Callable, std::vector<Value>>
&& is_one_of<std::invoke_result_t<Callable, std::vector<Value>>, Value, Result<Value>>
inline Value(Callable &&callable)
: type{Type::Lambda}
{
if constexpr (std::is_same_v<Result<Value>, std::invoke_result_t<Callable, std::vector<Value>>>) {
f = std::move(callable);
} else {
f = [fun = std::move(callable)](std::vector<Value> args) -> Result<Value> {
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<Value> operator()(std::vector<Value> 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<Value(std::vector<Value>)>;
struct Env
{
static std::vector<Env> *pool;
std::unordered_map<std::string, Value> 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<std::string, Function> functions;
std::unordered_map<std::string, Function> operators;
std::vector<Env> 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<Value> 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> location, Value::Type value_type);
[[noreturn]]
void all_tokens_were_not_parsed(std::span<Token>);
}

View File

@ -110,23 +110,23 @@ Result<Ast> Parser::parse_atomic_expression()
token_id = start;
}
return parse_sequence().and_then([&](Ast ast) -> tl::expected<Ast, Error> {
return parse_sequence().and_then([&](Ast &&ast) -> Result<Ast> {
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<Ast, Error> {
return parse_expression().and_then([&](Ast ast) -> Result<Ast> {
Try(ensure(Token::Type::Close_Paren));
consume();
return ast;
});
default:
return peek().and_then([](auto const& token) -> tl::expected<Ast, Error> {
return peek().and_then([](auto const& token) -> Result<Ast> {
return tl::unexpected(errors::unexpected_token(token));
});
}

80
src/tests/environment.cc Normal file
View File

@ -0,0 +1,80 @@
#include <boost/ut.hpp>
#include <musique.hh>
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));
};
};
};

View File

@ -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));
}
};
};
};

35
src/tests/value.cc Normal file
View File

@ -0,0 +1,35 @@
#include <boost/ut.hpp>
#include <musique.hh>
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("<lambda>"sv, str(Value::lambda(nullptr))));
};
};

View File

@ -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> Value::operator()(std::vector<Value> 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 << "<lambda>";
}
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();
}