Scoped variables, single namespace variables and functions
This commit is contained in:
parent
0f9c46dffb
commit
5d51d1672f
1
Makefile
1
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 \
|
||||
|
2
examples/variables.mq
Normal file
2
examples/variables.mq
Normal file
@ -0,0 +1,2 @@
|
||||
var x = 10;
|
||||
say (x + 1)
|
49
src/environment.cc
Normal file
49
src/environment.cc
Normal 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];
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>);
|
||||
}
|
||||
|
@ -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
80
src/tests/environment.cc
Normal 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));
|
||||
};
|
||||
};
|
||||
};
|
@ -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
35
src/tests/value.cc
Normal 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))));
|
||||
};
|
||||
};
|
36
src/value.cc
36
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> 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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user