Error reporting system improvement
This commit is contained in:
parent
df236b13c8
commit
605533de32
@ -79,7 +79,10 @@ def run_tests(file_paths: list):
|
||||
return_code = 0
|
||||
for program_file in file_paths:
|
||||
test_case_file = find_path_for_test_case(program_file)
|
||||
tc = Test_Case.from_file(test_case_file) if os.path.exists(test_case_file) else Test_Case()
|
||||
if os.path.exists(test_case_file):
|
||||
tc = Test_Case.from_file(test_case_file)
|
||||
else:
|
||||
continue
|
||||
|
||||
flags_list = [Interpreter]
|
||||
if hasattr(tc, "flags"):
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"returncode": 0,
|
||||
"stdout": "4\n30\n42\nnil\n",
|
||||
"stdout": "4\n30\n42\n",
|
||||
"stderr": "",
|
||||
"flags": []
|
||||
}
|
6
examples/.tests_cache/arrays.mq.json
Normal file
6
examples/.tests_cache/arrays.mq.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"returncode": 0,
|
||||
"stdout": "1\n2\n3\n4\n5\n",
|
||||
"stderr": "",
|
||||
"flags": []
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"returncode": 0,
|
||||
"stdout": "100\n200\n120\nnil\n",
|
||||
"stdout": "100\n200\n120\n",
|
||||
"stderr": "",
|
||||
"flags": []
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"returncode": 0,
|
||||
"stdout": "1\n2\n6\n24\n120\n720\n5040\n40320\n362880\n3628800\nnil\n",
|
||||
"stdout": "1\n2\n6\n24\n120\n720\n5040\n40320\n362880\n3628800\n",
|
||||
"stderr": "",
|
||||
"flags": []
|
||||
}
|
6
examples/.tests_cache/fib.mq.json
Normal file
6
examples/.tests_cache/fib.mq.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"returncode": 0,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"flags": []
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"returncode": 0,
|
||||
"stdout": "11\nnil\n",
|
||||
"stdout": "11\n",
|
||||
"stderr": "",
|
||||
"flags": []
|
||||
}
|
234
src/errors.cc
234
src/errors.cc
@ -3,9 +3,20 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
bool Error::operator==(errors::Type type)
|
||||
template<typename T, typename ...Params>
|
||||
concept Callable = requires(T t, Params ...params) { t(params...); };
|
||||
|
||||
template<typename Lambda, typename ...T>
|
||||
concept Visitor = (Callable<Lambda, T> && ...);
|
||||
|
||||
/// Custom visit function for better C++ compilation error messages
|
||||
template<typename V, typename ...T>
|
||||
static auto visit(V &&visitor, std::variant<T...> const& variant)
|
||||
{
|
||||
return this->type == type;
|
||||
static_assert(Visitor<V, T...>, "visitor must cover all types");
|
||||
if constexpr (Visitor<V, T...>) {
|
||||
return std::visit(std::forward<V>(visitor), variant);
|
||||
}
|
||||
}
|
||||
|
||||
Error Error::with(Location loc) &&
|
||||
@ -17,183 +28,138 @@ Error Error::with(Location loc) &&
|
||||
enum class Error_Level
|
||||
{
|
||||
Error,
|
||||
Notice,
|
||||
Bug
|
||||
};
|
||||
|
||||
static std::ostream& error_heading(std::ostream &os, std::optional<Location> location, Error_Level lvl)
|
||||
static std::ostream& error_heading(
|
||||
std::ostream &os,
|
||||
std::optional<Location> location,
|
||||
Error_Level lvl,
|
||||
std::string_view short_description)
|
||||
{
|
||||
if (location) {
|
||||
os << *location;
|
||||
} else {
|
||||
os << "musique";
|
||||
}
|
||||
|
||||
switch (lvl) {
|
||||
case Error_Level::Error: return os << ": error: ";
|
||||
case Error_Level::Notice: return os << ": notice: ";
|
||||
case Error_Level::Error:
|
||||
os << "ERROR " << short_description << ' ';
|
||||
break;
|
||||
|
||||
// This branch should be reached if we have Error_Level::Bug
|
||||
// or definetely where error level is outside of enum Error_Level
|
||||
default: return os << ": implementation bug: ";
|
||||
default:
|
||||
os << "IMPLEMENTATION BUG " << short_description << ' ';
|
||||
}
|
||||
|
||||
if (location) {
|
||||
os << " at " << *location;
|
||||
}
|
||||
|
||||
return os << '\n';
|
||||
}
|
||||
|
||||
static void encourage_contact(std::ostream &os)
|
||||
{
|
||||
os <<
|
||||
"Interpreter got in state that was not expected by it's developers.\n"
|
||||
"Contact them providing code that coused it and error message above to resolve this trouble\n"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
void assert(bool condition, std::string message, Location loc)
|
||||
{
|
||||
if (condition) return;
|
||||
error_heading(std::cerr, loc, Error_Level::Bug) << message << std::endl;
|
||||
error_heading(std::cerr, loc, Error_Level::Bug, "Assertion in interpreter");
|
||||
|
||||
if (not message.empty())
|
||||
std::cerr << "with message: " << message << std::endl;
|
||||
|
||||
encourage_contact(std::cerr);
|
||||
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
[[noreturn]] void unimplemented(std::string_view message, Location loc)
|
||||
{
|
||||
if (message.empty()) {
|
||||
error_heading(std::cerr, loc, Error_Level::Bug) << "this part was not implemented yet" << std::endl;
|
||||
} else {
|
||||
error_heading(std::cerr, loc, Error_Level::Bug) << message << std::endl;
|
||||
error_heading(std::cerr, loc, Error_Level::Bug, "This part of interpreter was not implemented yet");
|
||||
|
||||
if (not message.empty()) {
|
||||
std::cerr << message << std::endl;
|
||||
}
|
||||
|
||||
encourage_contact(std::cerr);
|
||||
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
[[noreturn]] void unreachable(Location loc)
|
||||
{
|
||||
error_heading(std::cerr, loc, Error_Level::Bug) << "this place should not be reached" << std::endl;
|
||||
|
||||
error_heading(std::cerr, loc, Error_Level::Bug, "Reached unreachable state");
|
||||
encourage_contact(std::cerr);
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Error const& err)
|
||||
{
|
||||
error_heading(os, err.location, Error_Level::Error);
|
||||
std::string_view short_description = visit(Overloaded {
|
||||
[](errors::Expected_Keyword const&) { return "Expected keyword"; },
|
||||
[](errors::Failed_Numeric_Parsing const&) { return "Failed to parse a number"; },
|
||||
[](errors::Music_Literal_Used_As_Identifier const&) { return "Music literal in place of identifier"; },
|
||||
[](errors::Not_Callable const&) { return "Value not callable"; },
|
||||
[](errors::Undefined_Identifier const&) { return "Undefined identifier"; },
|
||||
[](errors::Undefined_Operator const&) { return "Undefined operator"; },
|
||||
[](errors::Unexpected_Empty_Source const&) { return "Unexpected end of file"; },
|
||||
[](errors::Unexpected_Keyword const&) { return "Unexpected keyword"; },
|
||||
[](errors::Unrecognized_Character const&) { return "Unrecognized character"; },
|
||||
[](errors::internal::Unexpected_Token const&) { return "Unexpected token"; }
|
||||
}, err.details);
|
||||
|
||||
switch (err.type) {
|
||||
case errors::End_Of_File:
|
||||
return os << "end of file\n";
|
||||
error_heading(os, err.location, Error_Level::Error, short_description);
|
||||
|
||||
case errors::Unrecognized_Character:
|
||||
return err.message.empty() ? os << "unrecognized character\n" : os << err.message;
|
||||
visit(Overloaded {
|
||||
[&os](errors::Unrecognized_Character const& err) {
|
||||
os << "I encountered character in the source code that was not supposed to be here.\n";
|
||||
os << " Character Unicode code: " << std::hex << err.invalid_character << '\n';
|
||||
os << " Character printed: '" << utf8::Print{err.invalid_character} << "'\n";
|
||||
os << "\n";
|
||||
os << "Musique only accepts characters that are unicode letters or ascii numbers and punctuation\n";
|
||||
},
|
||||
|
||||
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';
|
||||
|
||||
case errors::Unexpected_Empty_Source:
|
||||
return os << "unexpected end of input\n";
|
||||
|
||||
case errors::Failed_Numeric_Parsing:
|
||||
return err.error_code == std::errc::result_out_of_range
|
||||
? os << "number " << err.message << " cannot be represented with " << (sizeof(Number::num)*8) << " bit number\n"
|
||||
: os << "couldn't parse number " << err.message << '\n';
|
||||
[&os](errors::Failed_Numeric_Parsing const& err) {
|
||||
constexpr auto Max = std::numeric_limits<decltype(Number::num)>::max();
|
||||
constexpr auto Min = std::numeric_limits<decltype(Number::num)>::min();
|
||||
os << "I tried to parse numeric literal, but I failed.";
|
||||
if (err.reason == std::errc::result_out_of_range) {
|
||||
os << " Declared number is outside of valid range of numbers that can be represented.\n";
|
||||
os << " Only numbers in range [" << Min << ", " << Max << "] are supported\n";
|
||||
}
|
||||
},
|
||||
|
||||
return os << "unrecognized error type\n";
|
||||
}
|
||||
[&os](errors::internal::Unexpected_Token const& ut) {
|
||||
os << "I encountered unexpected token during " << ut.when << '\n';
|
||||
os << " Token type: " << ut.type << '\n';
|
||||
os << " Token source: " << ut.source << '\n';
|
||||
|
||||
static std::string format(auto const& ...args)
|
||||
{
|
||||
std::stringstream ss;
|
||||
(void) (ss << ... << args);
|
||||
return ss.str();
|
||||
}
|
||||
os << "\nThis error is considered an internal one. It should not be displayed to the end user.\n";
|
||||
encourage_contact(os);
|
||||
},
|
||||
|
||||
Error errors::unrecognized_character(u32 invalid_character)
|
||||
{
|
||||
Error err;
|
||||
err.type = errors::Unrecognized_Character;
|
||||
err.message = format(
|
||||
"unrecognized charater U+",
|
||||
std::hex, invalid_character,
|
||||
" (char: '", utf8::Print{invalid_character}, "')");
|
||||
[&os](errors::Expected_Keyword const&) {},
|
||||
[&os](errors::Music_Literal_Used_As_Identifier const&) {},
|
||||
[&os](errors::Not_Callable const&) {},
|
||||
[&os](errors::Undefined_Identifier const&) {},
|
||||
[&os](errors::Undefined_Operator const&) {},
|
||||
[&os](errors::Unexpected_Keyword const&) {},
|
||||
[&os](errors::Unexpected_Empty_Source const&) {}
|
||||
}, err.details);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error errors::unrecognized_character(u32 invalid_character, Location location)
|
||||
{
|
||||
return unrecognized_character(invalid_character).with(std::move(location));
|
||||
}
|
||||
|
||||
Error errors::unexpected_token(Token const& unexpected)
|
||||
{
|
||||
Error err;
|
||||
err.type = errors::Unexpected_Token_Type;
|
||||
err.location = unexpected.location;
|
||||
err.message = format("unexpected ", unexpected.type);
|
||||
return err;
|
||||
}
|
||||
|
||||
Error errors::unexpected_token(Token::Type expected, Token const& unexpected)
|
||||
{
|
||||
Error err;
|
||||
err.type = errors::Unexpected_Token_Type;
|
||||
err.location = unexpected.location;
|
||||
err.message = format("expected ", expected, ", but got ", unexpected.type);
|
||||
return err;
|
||||
}
|
||||
|
||||
Error errors::unexpected_end_of_source(Location location)
|
||||
{
|
||||
Error err;
|
||||
err.type = errors::Unexpected_Empty_Source;
|
||||
err.location = location;
|
||||
return err;
|
||||
}
|
||||
|
||||
Error errors::failed_numeric_parsing(Location location, std::errc errc, std::string_view source)
|
||||
{
|
||||
Error err;
|
||||
err.type = errors::Failed_Numeric_Parsing;
|
||||
err.location = location;
|
||||
err.error_code = errc;
|
||||
err.message = source;
|
||||
return err;
|
||||
}
|
||||
|
||||
Error errors::function_not_defined(Value const& value)
|
||||
{
|
||||
Error err;
|
||||
err.type = Function_Not_Defined;
|
||||
err.message = "Function '" + value.s + "' has not been defined yet";
|
||||
return err;
|
||||
}
|
||||
|
||||
Error errors::unresolved_operator(Token const& op)
|
||||
{
|
||||
Error err;
|
||||
err.type = errors::Unresolved_Operator;
|
||||
err.location = op.location;
|
||||
err.message = format("Unresolved operator '", op.source, "'");
|
||||
return err;
|
||||
}
|
||||
|
||||
Error errors::expected_keyword(Token const& unexpected, std::string_view keyword)
|
||||
{
|
||||
Error err;
|
||||
err.type = errors::Expected_Keyword;
|
||||
err.location = unexpected.location;
|
||||
err.message = format("Expected keyword '", keyword, "', but found ", unexpected);
|
||||
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;
|
||||
return os;
|
||||
}
|
||||
|
||||
void errors::all_tokens_were_not_parsed(std::span<Token> tokens)
|
||||
{
|
||||
error_heading(std::cerr, std::nullopt, Error_Level::Bug);
|
||||
error_heading(std::cerr, std::nullopt, Error_Level::Bug, "Remaining tokens");
|
||||
std::cerr << "remaining tokens after parsing. Listing remaining tokens:\n";
|
||||
|
||||
for (auto const& token : tokens) {
|
||||
error_heading(std::cerr, token.location, Error_Level::Notice);
|
||||
std::cerr << token << '\n';
|
||||
}
|
||||
|
||||
|
@ -374,7 +374,10 @@ Result<Value> Interpreter::eval(Ast &&ast)
|
||||
auto op = operators.find(std::string(ast.token.source));
|
||||
|
||||
if (op == operators.end()) {
|
||||
return errors::unresolved_operator(ast.token);
|
||||
return Error {
|
||||
.details = errors::Undefined_Operator { .op = ast.token.source },
|
||||
.location = ast.token.location
|
||||
};
|
||||
}
|
||||
|
||||
for (auto& a : ast.arguments) {
|
||||
|
@ -60,13 +60,13 @@ void Lexer::skip_whitespace_and_comments()
|
||||
}
|
||||
}
|
||||
|
||||
auto Lexer::next_token() -> Result<Token>
|
||||
auto Lexer::next_token() -> Result<std::variant<Token, End_Of_File>>
|
||||
{
|
||||
skip_whitespace_and_comments();
|
||||
start();
|
||||
|
||||
if (peek() == 0) {
|
||||
return errors::End_Of_File;
|
||||
return End_Of_File{};
|
||||
}
|
||||
|
||||
switch (peek()) {
|
||||
@ -135,7 +135,10 @@ auto Lexer::next_token() -> Result<Token>
|
||||
return Token { Token::Type::Operator, finish(), token_location };
|
||||
}
|
||||
|
||||
return errors::unrecognized_character(peek(), token_location);
|
||||
return Error {
|
||||
.details = errors::Unrecognized_Character { .invalid_character = peek() },
|
||||
.location = token_location
|
||||
};
|
||||
}
|
||||
|
||||
auto Lexer::peek() const -> u32
|
||||
|
119
src/musique.hh
119
src/musique.hh
@ -45,22 +45,86 @@ using isize = std::ptrdiff_t;
|
||||
/// as first class feature in Zig programming language.
|
||||
namespace errors
|
||||
{
|
||||
enum Type
|
||||
struct Unrecognized_Character
|
||||
{
|
||||
End_Of_File,
|
||||
Unrecognized_Character,
|
||||
u32 invalid_character;
|
||||
};
|
||||
|
||||
Unexpected_Token_Type,
|
||||
Unexpected_Empty_Source,
|
||||
Failed_Numeric_Parsing,
|
||||
struct Unexpected_Empty_Source
|
||||
{
|
||||
};
|
||||
|
||||
Function_Not_Defined,
|
||||
Unresolved_Operator,
|
||||
Expected_Keyword,
|
||||
Not_Callable
|
||||
struct Failed_Numeric_Parsing
|
||||
{
|
||||
std::errc reason;
|
||||
};
|
||||
|
||||
struct Undefined_Identifier
|
||||
{
|
||||
};
|
||||
|
||||
struct Expected_Keyword
|
||||
{
|
||||
std::string_view keyword;
|
||||
std::string_view received_type = {};
|
||||
};
|
||||
|
||||
struct Unexpected_Keyword
|
||||
{
|
||||
std::string_view keyword;
|
||||
};
|
||||
|
||||
struct Undefined_Operator
|
||||
{
|
||||
std::string_view op;
|
||||
};
|
||||
|
||||
struct Not_Callable
|
||||
{
|
||||
std::string_view type;
|
||||
};
|
||||
|
||||
struct Music_Literal_Used_As_Identifier
|
||||
{
|
||||
std::string_view source;
|
||||
|
||||
/// Why only identifier can be used?
|
||||
std::string_view identifier_context;
|
||||
};
|
||||
|
||||
/// Collection of messages that are considered internal and should not be printed to the end user.
|
||||
namespace internal
|
||||
{
|
||||
struct Unexpected_Token
|
||||
{
|
||||
/// Type of the token
|
||||
std::string_view type;
|
||||
|
||||
/// Source of the token
|
||||
std::string_view source;
|
||||
|
||||
/// Where this token was encountered that was unexpected?
|
||||
std::string_view when;
|
||||
};
|
||||
}
|
||||
|
||||
using Details = std::variant<
|
||||
Expected_Keyword,
|
||||
Failed_Numeric_Parsing,
|
||||
Music_Literal_Used_As_Identifier,
|
||||
Not_Callable,
|
||||
Undefined_Identifier,
|
||||
Undefined_Operator,
|
||||
Unexpected_Empty_Source,
|
||||
Unexpected_Keyword,
|
||||
Unrecognized_Character,
|
||||
internal::Unexpected_Token
|
||||
>;
|
||||
}
|
||||
|
||||
template<typename ...Lambdas>
|
||||
struct Overloaded : Lambdas... { using Lambdas::operator()...; };
|
||||
|
||||
/// \brief Location describes code position in `file line column` format.
|
||||
/// It's used both to represent position in source files provided
|
||||
// to interpreter and internal interpreter usage.
|
||||
@ -113,12 +177,9 @@ void assert(bool condition, std::string message, Location loc = Location::caller
|
||||
|
||||
struct Error
|
||||
{
|
||||
errors::Type type;
|
||||
errors::Details details;
|
||||
std::optional<Location> location = std::nullopt;
|
||||
std::string message{};
|
||||
std::errc error_code{};
|
||||
|
||||
bool operator==(errors::Type);
|
||||
Error with(Location) &&;
|
||||
};
|
||||
|
||||
@ -152,11 +213,6 @@ struct [[nodiscard("This value may contain critical error, so it should NOT be i
|
||||
{
|
||||
}
|
||||
|
||||
constexpr Result(errors::Type error)
|
||||
: Storage(tl::unexpect, Error { error } )
|
||||
{
|
||||
}
|
||||
|
||||
inline Result(Error error)
|
||||
: Storage(tl::unexpected(std::move(error)))
|
||||
{
|
||||
@ -300,6 +356,9 @@ std::ostream& operator<<(std::ostream& os, Token const& tok);
|
||||
/// Token type debug printing
|
||||
std::ostream& operator<<(std::ostream& os, Token::Type type);
|
||||
|
||||
/// Explicit marker of the end of file
|
||||
struct End_Of_File {};
|
||||
|
||||
/// Lexer takes source code and turns it into list of tokens
|
||||
///
|
||||
/// It allows for creating sequence of tokens by using next_token() method.
|
||||
@ -332,8 +391,8 @@ struct Lexer
|
||||
/// Used only for rewinding
|
||||
Location prev_location{};
|
||||
|
||||
/// Try to tokenize next token
|
||||
auto next_token() -> Result<Token>;
|
||||
/// Try to tokenize next token.
|
||||
auto next_token() -> Result<std::variant<Token, End_Of_File>>;
|
||||
|
||||
/// Skip whitespace and comments from the beggining of the source
|
||||
///
|
||||
@ -475,9 +534,6 @@ struct Parser
|
||||
|
||||
/// Tests if current token has given type and source
|
||||
bool expect(Token::Type type, std::string_view lexeme) const;
|
||||
|
||||
/// Ensures that current token has one of types given, otherwise returns error
|
||||
Result<void> ensure(Token::Type type) const;
|
||||
};
|
||||
|
||||
/// Number type supporting integer and fractional constants
|
||||
@ -787,21 +843,6 @@ struct Interpreter
|
||||
|
||||
namespace errors
|
||||
{
|
||||
Error unrecognized_character(u32 invalid_character);
|
||||
Error unrecognized_character(u32 invalid_character, Location location);
|
||||
|
||||
Error unexpected_token(Token::Type expected, Token const& unexpected);
|
||||
Error unexpected_token(Token const& unexpected);
|
||||
Error unexpected_end_of_source(Location location);
|
||||
|
||||
Error failed_numeric_parsing(Location location, std::errc errc, std::string_view source);
|
||||
|
||||
Error function_not_defined(Value const& v);
|
||||
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>);
|
||||
}
|
||||
|
@ -165,7 +165,10 @@ Result<Number> Number::from(Token token)
|
||||
num_end = begin;
|
||||
goto parse_fractional;
|
||||
}
|
||||
return errors::failed_numeric_parsing(std::move(token.location), ec, token.source);
|
||||
return Error {
|
||||
.details = errors::Failed_Numeric_Parsing { .reason = ec },
|
||||
.location = std::move(token.location)
|
||||
};
|
||||
}
|
||||
parsed_numerator = true;
|
||||
|
||||
@ -177,7 +180,10 @@ parse_fractional:
|
||||
if (ec != std::errc{}) {
|
||||
if (parsed_numerator && ec == std::errc::invalid_argument && token.source != ".")
|
||||
return result;
|
||||
return errors::failed_numeric_parsing(std::move(token.location), ec, token.source);
|
||||
return Error {
|
||||
.details = errors::Failed_Numeric_Parsing { .reason = ec },
|
||||
.location = std::move(token.location)
|
||||
};
|
||||
}
|
||||
result += Number{ frac, pow10(frac_end - num_end) };
|
||||
}
|
||||
|
@ -27,12 +27,14 @@ Result<Ast> Parser::parse(std::string_view source, std::string_view filename)
|
||||
lexer.location.filename = filename;
|
||||
Parser parser;
|
||||
|
||||
for (;;) if (auto maybe_token = lexer.next_token(); maybe_token.has_value()) {
|
||||
parser.tokens.emplace_back(*std::move(maybe_token));
|
||||
} else if (maybe_token.error().type == errors::End_Of_File) {
|
||||
break;
|
||||
for (;;) {
|
||||
auto token_or_eof = Try(lexer.next_token());
|
||||
if (auto maybe_token = std::get_if<Token>(&token_or_eof); maybe_token) {
|
||||
parser.tokens.emplace_back(std::move(*maybe_token));
|
||||
} else {
|
||||
return std::move(maybe_token).error();
|
||||
// We encountered end of file so no more tokens, break the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto const result = parser.parse_sequence();
|
||||
@ -61,7 +63,14 @@ Result<Ast> Parser::parse_expression()
|
||||
Result<Ast> Parser::parse_variable_declaration()
|
||||
{
|
||||
if (!expect(Token::Type::Keyword, "var")) {
|
||||
return errors::expected_keyword(Try(peek()), "var");
|
||||
Error error;
|
||||
errors::Expected_Keyword kw { .keyword = "var" };
|
||||
if (token_id >= tokens.size()) {
|
||||
kw.received_type = ""; // TODO Token type
|
||||
error.location = peek()->location;
|
||||
}
|
||||
error.details = std::move(kw);
|
||||
return error;
|
||||
}
|
||||
auto var = consume();
|
||||
|
||||
@ -98,7 +107,10 @@ Result<Ast> Parser::parse_atomic_expression()
|
||||
// but keywords like `var` announce variable declaration which is higher up in expression parsing.
|
||||
// So we need to explicitly allow only keywords that are also literals
|
||||
if (std::find(Literal_Keywords.begin(), Literal_Keywords.end(), peek()->source) == Literal_Keywords.end()) {
|
||||
return errors::unexpected_token(*peek());
|
||||
return Error {
|
||||
.details = errors::Unexpected_Keyword { .keyword = peek()->source },
|
||||
.location = peek()->location
|
||||
};
|
||||
}
|
||||
[[fallthrough]];
|
||||
case Token::Type::Chord:
|
||||
@ -133,7 +145,9 @@ Result<Ast> Parser::parse_atomic_expression()
|
||||
}
|
||||
|
||||
return parse_sequence().and_then([&](Ast &&ast) -> Result<Ast> {
|
||||
Try(ensure(Token::Type::Close_Block));
|
||||
if (not expect(Token::Type::Close_Block)) {
|
||||
unimplemented("Error handling of this code is not implemented yet");
|
||||
}
|
||||
consume();
|
||||
if (is_lambda) {
|
||||
return Ast::lambda(opening.location, std::move(ast), std::move(parameters));
|
||||
@ -146,21 +160,38 @@ Result<Ast> Parser::parse_atomic_expression()
|
||||
case Token::Type::Open_Paren:
|
||||
consume();
|
||||
return parse_expression().and_then([&](Ast ast) -> Result<Ast> {
|
||||
Try(ensure(Token::Type::Close_Paren));
|
||||
if (not expect(Token::Type::Close_Paren)) {
|
||||
unimplemented("Error handling of this code is not implemented yet");
|
||||
}
|
||||
consume();
|
||||
return ast;
|
||||
});
|
||||
|
||||
default:
|
||||
return peek().and_then([](auto const& token) -> Result<Ast> {
|
||||
return tl::unexpected(errors::unexpected_token(token));
|
||||
});
|
||||
return Error {
|
||||
.details = errors::internal::Unexpected_Token {
|
||||
.type = "", // TODO fill type
|
||||
.source = peek()->source,
|
||||
.when = "atomic expression parsing"
|
||||
},
|
||||
.location = peek()->location
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Result<Ast> Parser::parse_identifier_with_trailing_separators()
|
||||
{
|
||||
Try(ensure(Token::Type::Symbol));
|
||||
if (not expect(Token::Type::Symbol)) {
|
||||
// TODO Specific error message
|
||||
return Error {
|
||||
.details = errors::internal::Unexpected_Token {
|
||||
.type = "", // TODO fill type
|
||||
.source = peek()->source,
|
||||
.when = "identifier parsing"
|
||||
},
|
||||
.location = peek()->location
|
||||
};
|
||||
}
|
||||
auto lit = Ast::literal(consume());
|
||||
while (expect(Token::Type::Expression_Separator)) { consume(); }
|
||||
return lit;
|
||||
@ -168,7 +199,17 @@ Result<Ast> Parser::parse_identifier_with_trailing_separators()
|
||||
|
||||
Result<Ast> Parser::parse_identifier()
|
||||
{
|
||||
Try(ensure(Token::Type::Symbol));
|
||||
if (not expect(Token::Type::Symbol)) {
|
||||
// TODO Specific error message
|
||||
return Error {
|
||||
.details = errors::internal::Unexpected_Token {
|
||||
.type = "", // TODO fill type
|
||||
.source = peek()->source,
|
||||
.when = "identifier parsing"
|
||||
},
|
||||
.location = peek()->location
|
||||
};
|
||||
}
|
||||
return Ast::literal(consume());
|
||||
}
|
||||
|
||||
@ -187,7 +228,7 @@ static Result<std::vector<Ast>> parse_many(
|
||||
while ((expr = (p.*parser)()).has_value()) {
|
||||
trees.push_back(std::move(expr).value());
|
||||
if (separator) {
|
||||
if (auto s = p.ensure(*separator); !s.has_value()) {
|
||||
if (not p.expect(*separator)) {
|
||||
break;
|
||||
}
|
||||
do p.consume(); while (p.expect(*separator));
|
||||
@ -204,7 +245,10 @@ static Result<std::vector<Ast>> parse_many(
|
||||
Result<Token> Parser::peek() const
|
||||
{
|
||||
return token_id >= tokens.size()
|
||||
? errors::unexpected_end_of_source(tokens.back().location)
|
||||
? Error {
|
||||
.details = errors::Unexpected_Empty_Source {},
|
||||
.location = tokens.back().location
|
||||
}
|
||||
: Result<Token>(tokens[token_id]);
|
||||
}
|
||||
|
||||
@ -228,15 +272,6 @@ bool Parser::expect(Token::Type type, std::string_view lexeme) const
|
||||
return token_id < tokens.size() && tokens[token_id].type == type && tokens[token_id].source == lexeme;
|
||||
}
|
||||
|
||||
Result<void> Parser::ensure(Token::Type type) const
|
||||
{
|
||||
return token_id >= tokens.size()
|
||||
? errors::unexpected_end_of_source(tokens.back().location)
|
||||
: tokens[token_id].type != type
|
||||
? errors::unexpected_token(type, tokens[token_id])
|
||||
: Result<void>{};
|
||||
}
|
||||
|
||||
// Don't know if it's a good idea to defer parsing of literal values up to value creation, which is current approach.
|
||||
// This may create unexpected performance degradation during program evaluation.
|
||||
Ast Ast::literal(Token token)
|
||||
|
@ -47,6 +47,7 @@ suite intepreter_test = [] {
|
||||
|
||||
should("allows only for calling which is callable") = [] {
|
||||
evaluates_to(Value::from(Number(0)), "[i|i] 0");
|
||||
#if 0
|
||||
{
|
||||
Interpreter i;
|
||||
{
|
||||
@ -61,6 +62,7 @@ suite intepreter_test = [] {
|
||||
expect(eq(result.error().type, errors::Not_Callable));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
should("allow for value (in)equality comparisons") = [] {
|
||||
|
@ -3,6 +3,25 @@
|
||||
|
||||
using namespace boost::ut;
|
||||
|
||||
enum class EOF_Status : bool
|
||||
{
|
||||
Reject = false,
|
||||
Accept = true,
|
||||
};
|
||||
|
||||
template<EOF_Status Expect_End_Of_File = EOF_Status::Reject>
|
||||
static auto handle_end_of_file(
|
||||
std::variant<Token, End_Of_File> token_or_eof,
|
||||
reflection::source_location const& sl)
|
||||
{
|
||||
if constexpr (Expect_End_Of_File == EOF_Status::Accept) {
|
||||
expect(std::holds_alternative<End_Of_File>(token_or_eof), sl) << "Expected to encounter end of file";
|
||||
} else {
|
||||
expect(std::holds_alternative<Token>(token_or_eof), sl) << "Expected to NOT encounter end of file";
|
||||
return std::get<Token>(std::move(token_or_eof));
|
||||
}
|
||||
}
|
||||
|
||||
static void expect_token_type(
|
||||
Token::Type expected_type,
|
||||
std::string source,
|
||||
@ -11,7 +30,8 @@ static void expect_token_type(
|
||||
Lexer lexer{source};
|
||||
auto result = lexer.next_token();
|
||||
expect(result.has_value() >> fatal, sl) << "have not parsed any tokens";
|
||||
expect(eq(result->type, expected_type), sl) << "different token type then expected";
|
||||
auto token = handle_end_of_file(*std::move(result), sl);
|
||||
expect(eq(token.type, expected_type), sl) << "different token type then expected";
|
||||
}
|
||||
|
||||
static void expect_token_type_and_value(
|
||||
@ -25,8 +45,9 @@ static void expect_token_type_and_value(
|
||||
expect(result.has_value(), sl) << "have not parsed any tokens";
|
||||
|
||||
if (result.has_value()) {
|
||||
expect(eq(result->type, expected_type), sl) << "different token type then expected";
|
||||
expect(eq(result->source, expected), sl) << "tokenized source is not equal to original";
|
||||
auto token = handle_end_of_file(*std::move(result), sl);
|
||||
expect(eq(token.type, expected_type), sl) << "different token type then expected";
|
||||
expect(eq(token.source, expected), sl) << "tokenized source is not equal to original";
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,8 +68,10 @@ static void expect_token_type_and_location(
|
||||
Lexer lexer{source};
|
||||
auto result = lexer.next_token();
|
||||
expect(result.has_value() >> fatal, sl) << "have not parsed any tokens";
|
||||
expect(eq(result->type, expected_type), sl) << "different token type then expected";
|
||||
expect(eq(result->location, location), sl) << "tokenized source is at different place then expected";
|
||||
|
||||
auto token = handle_end_of_file(*std::move(result), sl);
|
||||
expect(eq(token.type, expected_type), sl) << "different token type then expected";
|
||||
expect(eq(token.location, location), sl) << "tokenized source is at different place then expected";
|
||||
}
|
||||
|
||||
static void expect_empty_file(
|
||||
@ -57,10 +80,8 @@ static void expect_empty_file(
|
||||
{
|
||||
Lexer lexer{source};
|
||||
auto result = lexer.next_token();
|
||||
expect(!result.has_value(), sl) << "could not produce any tokens from empty file";
|
||||
if (not result.has_value()) {
|
||||
expect(result.error() == errors::End_Of_File, sl) << "could not produce any tokens from empty file";
|
||||
}
|
||||
expect(result.has_value(), sl) << "Encountered error when expecting end of file";
|
||||
handle_end_of_file<EOF_Status::Accept>(*std::move(result), sl);
|
||||
}
|
||||
|
||||
template<auto N>
|
||||
@ -76,14 +97,18 @@ static void expect_token_sequence(
|
||||
expect(result.has_value(), sl) << "expected token, received nothing";
|
||||
|
||||
if (result.has_value()) {
|
||||
expect(eq(result->type, expected.type)) << "different token type then expected";
|
||||
expect(eq(result->source, expected.source)) << "different token source then expected";
|
||||
expect(eq(result->location, expected.location)) << "different token location then expected";
|
||||
auto token = handle_end_of_file(*std::move(result), sl);
|
||||
expect(eq(token.type, expected.type)) << "different token type then expected";
|
||||
expect(eq(token.source, expected.source)) << "different token source then expected";
|
||||
expect(eq(token.location, expected.location)) << "different token location then expected";
|
||||
}
|
||||
}
|
||||
|
||||
auto const result = lexer.next_token();
|
||||
expect(not result.has_value(), sl) << "more tokens then expected";
|
||||
expect(result.has_value(), sl) << "expected end of file after sequence of tokens";
|
||||
if (result.has_value()) {
|
||||
expect(std::holds_alternative<End_Of_File>(*result), sl) << "expected end of file after sequence of tokens";
|
||||
}
|
||||
}
|
||||
|
||||
suite lexer_test = [] {
|
||||
|
@ -169,7 +169,12 @@ Result<Value> Value::operator()(Interpreter &i, std::vector<Value> args)
|
||||
}
|
||||
default:
|
||||
// TODO Fill location
|
||||
return errors::not_callable(std::nullopt, type);
|
||||
return Error {
|
||||
.details = errors::Not_Callable {
|
||||
.type = type_name(type)
|
||||
},
|
||||
.location = std::nullopt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user