Better main, refactoring of error system and Result type
This commit is contained in:
parent
31262e74a0
commit
35a8345bc5
9
Makefile
9
Makefile
@ -2,10 +2,11 @@ MAKEFLAGS="-j $(grep -c ^processor /proc/cpuinfo)"
|
||||
CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch
|
||||
CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -Ilib/ut/ -Isrc/
|
||||
|
||||
Obj=bin/errors.o \
|
||||
bin/lexer.o \
|
||||
bin/parser.o \
|
||||
bin/unicode.o \
|
||||
Obj=bin/errors.o \
|
||||
bin/lexer.o \
|
||||
bin/location.o \
|
||||
bin/parser.o \
|
||||
bin/unicode.o \
|
||||
bin/unicode_tables.o
|
||||
|
||||
all: bin/musique bin/unit-tests
|
||||
|
@ -21,7 +21,7 @@ enum class Error_Level
|
||||
Bug
|
||||
};
|
||||
|
||||
static void 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)
|
||||
{
|
||||
if (location) {
|
||||
os << *location;
|
||||
@ -30,12 +30,34 @@ static void error_heading(std::ostream &os, std::optional<Location> location, Er
|
||||
}
|
||||
|
||||
switch (lvl) {
|
||||
case Error_Level::Bug: os << ": implementation bug: "; return;
|
||||
case Error_Level::Error: os << ": error: "; return;
|
||||
case Error_Level::Notice: os << ": notice: "; return;
|
||||
case Error_Level::Error: return os << ": error: ";
|
||||
case Error_Level::Notice: return os << ": notice: ";
|
||||
|
||||
// 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: ";
|
||||
}
|
||||
}
|
||||
|
||||
void assert(bool condition, std::string message, Location loc)
|
||||
{
|
||||
if (condition) return;
|
||||
error_heading(std::cerr, loc, Error_Level::Bug) << message << std::endl;
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
[[noreturn]] void unimplemented(Location loc)
|
||||
{
|
||||
error_heading(std::cerr, loc, Error_Level::Bug) << "this part was not implemented yet" << std::endl;
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
[[noreturn]] void unreachable(Location loc)
|
||||
{
|
||||
error_heading(std::cerr, loc, Error_Level::Bug) << "this place should not be reached" << std::endl;
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Error const& err)
|
||||
{
|
||||
error_heading(os, err.location, Error_Level::Error);
|
||||
|
24
src/lexer.cc
24
src/lexer.cc
@ -221,7 +221,7 @@ auto Lexer::consume_if(auto first, auto second) -> bool
|
||||
|
||||
void Lexer::rewind()
|
||||
{
|
||||
assert(last_rune_length != 0);
|
||||
assert(last_rune_length != 0, "cannot rewind to not existing rune");
|
||||
source = { source.data() - last_rune_length, source.size() + last_rune_length };
|
||||
token_length -= last_rune_length;
|
||||
location = prev_location;
|
||||
@ -262,25 +262,5 @@ std::ostream& operator<<(std::ostream& os, Token::Type type)
|
||||
case Token::Type::Operator: return os << "OPERATOR";
|
||||
case Token::Type::Expression_Separator: return os << "EXPRESSION SEPARATOR";
|
||||
}
|
||||
|
||||
assert(false && "exhaustive handling of Token::Type enumeration");
|
||||
}
|
||||
|
||||
Location Location::advance(u32 rune)
|
||||
{
|
||||
switch (rune) {
|
||||
case '\n':
|
||||
line += 1;
|
||||
[[fallthrough]];
|
||||
case '\r':
|
||||
column = 1;
|
||||
return *this;
|
||||
}
|
||||
column += 1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Location const& location)
|
||||
{
|
||||
return os << location.filename << ':' << location.line << ':' << location.column;
|
||||
unreachable();
|
||||
}
|
||||
|
32
src/location.cc
Normal file
32
src/location.cc
Normal file
@ -0,0 +1,32 @@
|
||||
#include <musique.hh>
|
||||
|
||||
Location Location::advance(u32 rune)
|
||||
{
|
||||
switch (rune) {
|
||||
case '\n':
|
||||
line += 1;
|
||||
[[fallthrough]];
|
||||
case '\r':
|
||||
column = 1;
|
||||
return *this;
|
||||
}
|
||||
column += 1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Location const& location)
|
||||
{
|
||||
return os << location.filename << ':' << location.line << ':' << location.column;
|
||||
}
|
||||
|
||||
#if defined(__cpp_lib_source_location)
|
||||
Location Location::caller(std::source_location loc = std::source_location::current())
|
||||
{
|
||||
return Location { loc.file_name(), loc.line(), loc.column() };
|
||||
}
|
||||
#elif (__has_builtin(__builtin_FILE) and __has_builtin(__builtin_LINE))
|
||||
Location Location::caller(char const* file, usize line)
|
||||
{
|
||||
return Location { file, line };
|
||||
}
|
||||
#endif
|
60
src/main.cc
60
src/main.cc
@ -1,7 +1,11 @@
|
||||
#include <iostream>
|
||||
#include <musique.hh>
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <fstream>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string_view pop(std::span<char const*> &span)
|
||||
{
|
||||
@ -10,11 +14,45 @@ static std::string_view pop(std::span<char const*> &span)
|
||||
return element;
|
||||
}
|
||||
|
||||
Result<Unit> Main(std::span<char const*> args)
|
||||
[[noreturn]]
|
||||
void usage()
|
||||
{
|
||||
std::cerr <<
|
||||
"usage: musique <options> [filename]\n"
|
||||
" where filename is path to file with Musique code that will be executed\n"
|
||||
" where options are:\n"
|
||||
" -c CODE\n"
|
||||
" --run CODE\n"
|
||||
" executes given code\n";
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
static Result<void> run(std::string_view source, std::string_view filename)
|
||||
{
|
||||
Try(Parser::parse(source, filename));
|
||||
std::cout << "successfully parsed: " << source << " \n";
|
||||
return {};
|
||||
}
|
||||
|
||||
// We make sure that through life of interpreter source code is allways allocated
|
||||
std::vector<std::string> eternal_sources;
|
||||
|
||||
static Result<void> Main(std::span<char const*> args)
|
||||
{
|
||||
if (args.empty()) {
|
||||
usage();
|
||||
}
|
||||
|
||||
std::vector<std::string_view> files;
|
||||
|
||||
while (not args.empty()) {
|
||||
std::string_view arg = pop(args);
|
||||
|
||||
if (!arg.starts_with('-')) {
|
||||
files.push_back(std::move(arg));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "-c" || arg == "--run") {
|
||||
if (args.empty()) {
|
||||
std::cerr << "musique: error: option " << arg << " requires an argument" << std::endl;
|
||||
@ -22,8 +60,7 @@ Result<Unit> Main(std::span<char const*> args)
|
||||
}
|
||||
|
||||
auto const source = pop(args);
|
||||
Try(Parser::parse(source, "arguments"));
|
||||
std::cout << "successfully parsed: " << source << " \n";
|
||||
Try(run(source, "arguments"));
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -31,12 +68,27 @@ Result<Unit> Main(std::span<char const*> args)
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
for (auto const& path : files) {
|
||||
if (not fs::exists(path)) {
|
||||
std::cerr << "musique: error: couldn't open file: " << path << std::endl;
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
{
|
||||
std::ifstream source_file{fs::path(path)};
|
||||
eternal_sources.emplace_back(std::istreambuf_iterator<char>(source_file), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
Try(run(eternal_sources.back(), path));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int main(int argc, char const** argv)
|
||||
{
|
||||
auto result = Main(std::span{ argv+1, usize(argc-1) });
|
||||
auto const args = std::span(argv, argc).subspan(1);
|
||||
auto const result = Main(args);
|
||||
if (not result.has_value()) {
|
||||
std::cerr << result.error() << std::flush;
|
||||
return 1;
|
||||
|
@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@ -11,6 +10,15 @@
|
||||
#include <tl/expected.hpp>
|
||||
#include <variant>
|
||||
|
||||
#if defined(__cpp_lib_source_location)
|
||||
#include <source_location>
|
||||
#endif
|
||||
|
||||
// To make sure, that we don't collide with <cassert> macro
|
||||
#ifdef assert
|
||||
#undef assert
|
||||
#endif
|
||||
|
||||
using u8 = std::uint8_t;
|
||||
using u16 = std::uint16_t;
|
||||
using u32 = std::uint32_t;
|
||||
@ -24,10 +32,6 @@ using i64 = std::int64_t;
|
||||
using usize = std::size_t;
|
||||
using isize = std::ptrdiff_t;
|
||||
|
||||
struct Unit {};
|
||||
|
||||
#define Fun(Function) ([]<typename ...T>(T&& ...args) { return (Function)(std::forward<T>(args)...); })
|
||||
|
||||
// Error handling mechanism inspired by Andrew Kelly approach, that was implemented
|
||||
// as first class feature in Zig programming language.
|
||||
namespace errors
|
||||
@ -45,7 +49,7 @@ namespace errors
|
||||
struct Location
|
||||
{
|
||||
std::string_view filename = "<unnamed>";
|
||||
usize column = 1, line = 1;
|
||||
usize line = 1, column = 1;
|
||||
|
||||
Location advance(u32 rune);
|
||||
|
||||
@ -58,10 +62,27 @@ struct Location
|
||||
loc.column = column;
|
||||
return loc;
|
||||
}
|
||||
|
||||
// Used to describe location of function call in interpreter (internal use only)
|
||||
#if defined(__cpp_lib_source_location)
|
||||
static Location caller(std::source_location loc = std::source_location::current());
|
||||
#elif (__has_builtin(__builtin_FILE) and __has_builtin(__builtin_LINE))
|
||||
static Location caller(char const* file = __builtin_FILE(), usize line = __builtin_LINE());
|
||||
#else
|
||||
#error Cannot implement Location::caller function
|
||||
#endif
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Location const& location);
|
||||
|
||||
void assert(bool condition, std::string message, Location loc = Location::caller());
|
||||
|
||||
// Marks part of code that was not implemented yet
|
||||
[[noreturn]] void unimplemented(Location loc = Location::caller());
|
||||
|
||||
// Marks location that should not be reached
|
||||
[[noreturn]] void unreachable(Location loc = Location::caller());
|
||||
|
||||
struct Error
|
||||
{
|
||||
errors::Type type;
|
||||
@ -72,19 +93,29 @@ struct Error
|
||||
Error with(Location) &&;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Error const& err);
|
||||
|
||||
template<typename T>
|
||||
struct Result : tl::expected<T, Error>
|
||||
{
|
||||
using Storage = tl::expected<T, Error>;
|
||||
|
||||
constexpr Result() = default;
|
||||
constexpr Result(Result const&) = default;
|
||||
constexpr Result(Result&&) = default;
|
||||
constexpr Result& operator=(Result const&) = default;
|
||||
constexpr Result& operator=(Result&&) = default;
|
||||
constexpr Result() = default;
|
||||
|
||||
template<typename ...Args> requires (not std::is_void_v<T>) && std::is_constructible_v<T, Args...>
|
||||
constexpr Result(Args&& ...args)
|
||||
: Storage( T{ std::forward<Args>(args)... } )
|
||||
{
|
||||
}
|
||||
|
||||
template<typename Arg> requires std::is_constructible_v<Storage, Arg>
|
||||
constexpr Result(Arg &&arg)
|
||||
: Storage(std::forward<Arg>(arg))
|
||||
{
|
||||
}
|
||||
|
||||
constexpr Result(errors::Type error)
|
||||
: Storage(tl::unexpected(Error { error }))
|
||||
: Storage(tl::unexpect, Error { error } )
|
||||
{
|
||||
}
|
||||
|
||||
@ -93,36 +124,34 @@ struct Result : tl::expected<T, Error>
|
||||
{
|
||||
}
|
||||
|
||||
inline Result(tl::unexpected<Error> error)
|
||||
: Storage(std::move(error))
|
||||
{
|
||||
}
|
||||
|
||||
template<typename ...Args>
|
||||
requires std::is_constructible_v<T, Args...>
|
||||
constexpr Result(Args&& ...args)
|
||||
: Storage( T{ std::forward<Args>(args)... } )
|
||||
// Internal function used for definition of Try macro
|
||||
inline auto value() &&
|
||||
{
|
||||
if constexpr (not std::is_void_v<T>) {
|
||||
return Storage::value();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Error const& err);
|
||||
|
||||
// NOTE This implementation requires C++ language extension: statement expressions
|
||||
// It's supported by GCC, other compilers i don't know
|
||||
#define Try(Value) ({ \
|
||||
auto try_value = (Value); \
|
||||
if (not try_value.has_value()) return tl::unexpected(try_value.error()); \
|
||||
*std::move(try_value); \
|
||||
// It's supported by GCC and Clang, other compilers i don't know
|
||||
//
|
||||
// Inspired by SerenityOS TRY macro
|
||||
#define Try(Value) \
|
||||
({ \
|
||||
auto try_value = (Value); \
|
||||
if (not try_value.has_value()) [[unlikely]] \
|
||||
return tl::unexpected(try_value.error()); \
|
||||
std::move(try_value).value(); \
|
||||
})
|
||||
|
||||
namespace unicode
|
||||
{
|
||||
inline namespace special_runes
|
||||
{
|
||||
constexpr u32 Rune_Error = 0xfffd;
|
||||
constexpr u32 Rune_Self = 0x80;
|
||||
constexpr u32 Max_Bytes = 4;
|
||||
[[maybe_unused]] constexpr u32 Rune_Error = 0xfffd;
|
||||
[[maybe_unused]] constexpr u32 Rune_Self = 0x80;
|
||||
[[maybe_unused]] constexpr u32 Max_Bytes = 4;
|
||||
}
|
||||
|
||||
// is_digit returns true if `digit` is ASCII digit
|
||||
@ -257,9 +286,6 @@ struct Ast
|
||||
Token token;
|
||||
};
|
||||
|
||||
template<typename Expected, typename ...T>
|
||||
concept Var_Args = (std::is_same_v<Expected, T> && ...) && (sizeof...(T) >= 1);
|
||||
|
||||
struct Parser
|
||||
{
|
||||
std::vector<Token> tokens;
|
||||
@ -280,7 +306,7 @@ struct Parser
|
||||
|
||||
// Ensures that current token has one of types given.
|
||||
// Otherwise returns error
|
||||
Result<Unit> ensure(Token::Type type) const;
|
||||
Result<void> ensure(Token::Type type) const;
|
||||
};
|
||||
|
||||
namespace errors
|
||||
|
@ -49,13 +49,13 @@ bool Parser::expect(Token::Type type) const
|
||||
return token_id < tokens.size() && tokens[token_id].type == type;
|
||||
}
|
||||
|
||||
Result<Unit> Parser::ensure(Token::Type type) const
|
||||
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<Unit>{};
|
||||
: Result<void>{};
|
||||
}
|
||||
|
||||
Ast Ast::literal(Token token)
|
||||
|
Loading…
Reference in New Issue
Block a user