diff --git a/.gitattributes b/.gitattributes index 9af238d85f..92321ae574 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ templates/**/*.c.erb linguist-language=C templates/**/*.h.erb linguist-language=C +templates/**/*.hpp.erb linguist-language=C++ templates/**/*.java.erb linguist-language=Java templates/**/*.rb.erb linguist-language=Ruby diff --git a/.github/workflows/cpp-bindings.yml b/.github/workflows/cpp-bindings.yml index 47bda28b05..adff3ea55b 100644 --- a/.github/workflows/cpp-bindings.yml +++ b/.github/workflows/cpp-bindings.yml @@ -19,6 +19,16 @@ jobs: name: test strategy: fail-fast: false + matrix: + compiler: + - "g++" + - "clang++" + standard: + - "-std=c++17" + - "-std=c++20" + exceptions: + - "" + - "-fno-exceptions" runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -29,11 +39,7 @@ jobs: bundler-cache: true - name: Compile prism run: bundle exec rake compile - - name: Compile C++ test - run: g++ -o ./cpp_test cpp/test.cpp build/static/*.o -Iinclude - - name: Run C++ test + - name: Compile tests + run: ${{ matrix.compiler }} ${{ matrix.standard }} ${{ matrix.exceptions }} -Wall -Wextra -pedantic -Werror -o ./cpp_test cpp/*_test.cpp build/static/*.o -Iinclude + - name: Run run: ./cpp_test - - name: Compile C++ headers - run: g++ -o ./cpp_headers cpp/headers.cpp build/static/*.o -Iinclude - - name: Run C++ headers - run: ./cpp_headers diff --git a/.gitignore b/.gitignore index a21e09b834..02e31f31df 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ out.svg /fuzz/output/ /gemfiles/typecheck/bin/ /include/prism/ast.h +/include/prism/ast.hpp /include/prism/internal/diagnostic.h /javascript/node_modules/ /javascript/package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a32c82b894..7da1e9f3b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - Many documentation improvements, including a new playground. - Prism now ships accurate and complete RBS and RBI definitions. - The license is now owned by the Ruby developers. +- Prism now ships an official C++ interface. ### Added diff --git a/Doxyfile b/Doxyfile index adfd78c431..410b05d728 100644 --- a/Doxyfile +++ b/Doxyfile @@ -31,3 +31,8 @@ GENERATE_LATEX = NO WARN_NO_PARAMDOC = YES WARN_AS_ERROR = FAIL_ON_WARNINGS DOT_GRAPH_MAX_NODES = 200 + +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +EXPAND_AS_DEFINED += PRISM_LIFETIMEBOUND +PREDEFINED += PRISM_LIFETIMEBOUND= diff --git a/cpp/headers.cpp b/cpp/headers.cpp deleted file mode 100644 index c9afcf76f1..0000000000 --- a/cpp/headers.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* Each public header is included on its own, without going through prism.h, - * and one function from each is referenced. If a header does not declare its - * functions with C linkage, the reference is to a C++-mangled name and this - * file fails to link. */ -#include "prism/arena.h" -#include "prism/ast.h" -#include "prism/buffer.h" -#include "prism/comments.h" -#include "prism/constant_pool.h" -#include "prism/diagnostic.h" -#include "prism/errors_format.h" -#include "prism/json.h" -#include "prism/line_offset_list.h" -#include "prism/magic_comments.h" -#include "prism/node.h" -#include "prism/options.h" -#include "prism/parser.h" -#include "prism/prettyprint.h" -#include "prism/serialize.h" -#include "prism/source.h" -#include "prism/stream.h" -#include "prism/string_query.h" -#include "prism/stringy.h" -#include "prism/version.h" - -#include - -typedef void (*Function)(void); - -/* This has external linkage so that every entry is emitted and must resolve at - * link time, regardless of optimization level. */ -Function functions[] = { - reinterpret_cast(pm_arena_new), - reinterpret_cast(pm_token_type), - reinterpret_cast(pm_buffer_new), - reinterpret_cast(pm_comment_location), - reinterpret_cast(pm_constant_start), - reinterpret_cast(pm_diagnostic_type), - reinterpret_cast(pm_errors_format), - reinterpret_cast(pm_dump_json), - reinterpret_cast(pm_line_offset_list_line_column), - reinterpret_cast(pm_magic_comment_key), - reinterpret_cast(pm_node_type), - reinterpret_cast(pm_options_new), - reinterpret_cast(pm_parser_new), - reinterpret_cast(pm_prettyprint), - reinterpret_cast(pm_serialize), - reinterpret_cast(pm_source_constant_new), - reinterpret_cast(pm_parse_stream), - reinterpret_cast(pm_string_query_local), - reinterpret_cast(pm_string_init), - reinterpret_cast(pm_version) -}; - -int main() { - std::cout << pm_version() << std::endl; - return 0; -} diff --git a/cpp/node_test.cpp b/cpp/node_test.cpp new file mode 100644 index 0000000000..1583b2eb73 --- /dev/null +++ b/cpp/node_test.cpp @@ -0,0 +1,129 @@ +#include "prism_test.hpp" + +#include +#include +#include +#if __cplusplus >= 202002L +#include +#endif + +/* Every node struct maps to its node type. */ +static_assert(prism::node_type_v == PM_CALL_NODE); +static_assert(prism::node_type_v == PM_PROGRAM_NODE); +static_assert(prism::node_type_v == PM_YIELD_NODE); + +/* Only the node structs declared in ast.h are nodes. */ +static_assert(prism::is_node_v); +static_assert(prism::is_node_v); +static_assert(!prism::is_node_v); +static_assert(!prism::is_node_v); +static_assert(!prism::is_node_v); + +/* Casts keep the constness of their argument. */ +static_assert(std::is_same_v(std::declval())), pm_call_node_t *>); +static_assert(std::is_same_v(std::declval())), const pm_call_node_t *>); +static_assert(std::is_same_v(std::declval())), pm_call_node_t *>); +static_assert(std::is_same_v(std::declval())), const pm_call_node_t *>); +static_assert(std::is_same_v())), pm_node_t *>); +static_assert(std::is_same_v())), const pm_node_t *>); + +/* None of the casts can throw. */ +static_assert(noexcept(prism::isa(std::declval()))); +static_assert(noexcept(prism::cast(std::declval()))); +static_assert(noexcept(prism::dyn_cast(std::declval()))); +static_assert(noexcept(prism::upcast(std::declval()))); + +/* A node_range is a contiguous range of node pointers. */ +static_assert(std::is_same_v().begin()), pm_node_t *const &>); +#if __cplusplus >= 202002L +static_assert(std::ranges::contiguous_range); +static_assert(std::ranges::sized_range); +#endif + +PRISM_TEST(isa_matches_only_the_node_type) { + prism_test::parse_result result("foo; 1"); + + CHECK(prism::isa(result.root)); + CHECK(prism::isa(result.statement(0))); + CHECK(!prism::isa(result.statement(0))); + CHECK(prism::isa(result.statement(1))); +} + +PRISM_TEST(cast_points_at_the_same_node) { + prism_test::parse_result result("foo"); + pm_node_t *node = result.statement(0); + const pm_node_t *const_node = node; + + pm_call_node_t *call = prism::cast(node); + const pm_call_node_t *const_call = prism::cast(const_node); + + CHECK(prism::upcast(call) == node); + CHECK(prism::upcast(const_call) == const_node); + CHECK(prism::constant(result.parser.get(), call->name) == "foo"); +} + +PRISM_TEST(dyn_cast_returns_null_on_a_mismatch_or_null) { + prism_test::parse_result result("foo"); + pm_node_t *node = result.statement(0); + const pm_node_t *const_node = node; + + CHECK(prism::dyn_cast(node) == prism::cast(node)); + CHECK(prism::dyn_cast(const_node) == prism::cast(const_node)); + CHECK(prism::dyn_cast(node) == nullptr); + CHECK(prism::dyn_cast(const_node) == nullptr); + CHECK(prism::dyn_cast(static_cast(nullptr)) == nullptr); + CHECK(prism::dyn_cast(static_cast(nullptr)) == nullptr); +} + +PRISM_TEST(dyn_cast_reads_an_optional_child) { + prism_test::parse_result result("foo.bar; baz"); + const pm_call_node_t *with_receiver = prism::cast(result.statement(0)); + const pm_call_node_t *without_receiver = prism::cast(result.statement(1)); + + CHECK(prism::dyn_cast(with_receiver->receiver) != nullptr); + CHECK(prism::dyn_cast(without_receiver->receiver) == nullptr); +} + +PRISM_TEST(node_range_iterates_in_order) { + prism_test::parse_result result("a; b; c"); + prism::node_range statements(result.statements()); + + CHECK(statements.size() == 3); + CHECK(!statements.empty()); + + const char *names[] = { "a", "b", "c" }; + std::size_t index = 0; + for (pm_node_t *statement : statements) { + CHECK(prism::slice(result.parser.get(), statement->location) == names[index++]); + } + CHECK(index == 3); +} + +PRISM_TEST(node_range_works_with_algorithms) { + prism_test::parse_result result("a; 1; b; 2; c"); + prism::node_range statements(result.statements()); + + CHECK(std::count_if(statements.begin(), statements.end(), prism::isa) == 2); + CHECK(std::distance(statements.begin(), statements.end()) == 5); + + auto found = std::find_if(statements.begin(), statements.end(), prism::isa); + CHECK(found != statements.end() && prism::slice(result.parser.get(), (*found)->location) == "1"); +} + +PRISM_TEST(node_range_of_an_empty_list) { + pm_node_list_t list = {}; + prism::node_range nodes(list); + + CHECK(nodes.empty()); + CHECK(nodes.size() == 0); + CHECK(nodes.begin() == nodes.end()); +} + +PRISM_TEST(node_range_reads_child_lists) { + prism_test::parse_result result("foo(1, 2, 3)"); + const pm_call_node_t *call = prism::cast(result.statement(0)); + prism::node_range arguments(call->arguments->arguments); + + CHECK(arguments.size() == 3); + CHECK(std::all_of(arguments.begin(), arguments.end(), prism::isa)); +} diff --git a/cpp/ownership_test.cpp b/cpp/ownership_test.cpp new file mode 100644 index 0000000000..2b118aeb3e --- /dev/null +++ b/cpp/ownership_test.cpp @@ -0,0 +1,88 @@ +#include "prism_test.hpp" + +#include +#include +#include + +/* The deleters are empty, so each owned handle is the size of a pointer. */ +static_assert(sizeof(prism::unique_arena) == sizeof(pm_arena_t *)); +static_assert(sizeof(prism::unique_parser) == sizeof(pm_parser_t *)); +static_assert(sizeof(prism::unique_options) == sizeof(pm_options_t *)); +static_assert(sizeof(prism::unique_buffer) == sizeof(pm_buffer_t *)); + +/* Freeing never throws, which unique_ptr's destructor relies on. */ +static_assert(std::is_nothrow_invocable_v); +static_assert(std::is_nothrow_invocable_v); +static_assert(std::is_nothrow_invocable_v); +static_assert(std::is_nothrow_invocable_v); + +/* Allocation failure aborts instead of throwing. */ +static_assert(noexcept(prism::make_arena())); +static_assert(noexcept(prism::make_options())); +static_assert(noexcept(prism::make_buffer())); +static_assert(noexcept(prism::make_parser(nullptr, std::string_view()))); + +/* Owned handles move but do not copy. */ +static_assert(std::is_nothrow_move_constructible_v); +static_assert(!std::is_copy_constructible_v); + +PRISM_TEST(make_returns_owned_handles) { + prism::unique_arena arena = prism::make_arena(); + prism::unique_options options = prism::make_options(); + prism::unique_buffer buffer = prism::make_buffer(); + prism::unique_parser parser = prism::make_parser(arena.get(), "1 + 2", options.get()); + + CHECK(arena != nullptr); + CHECK(options != nullptr); + CHECK(buffer != nullptr); + CHECK(parser != nullptr); +} + +PRISM_TEST(make_parser_parses_the_given_source) { + prism::unique_arena arena = prism::make_arena(); + prism::unique_parser parser = prism::make_parser(arena.get(), "foo"); + pm_node_t *root = pm_parse(parser.get()); + + CHECK(prism::isa(root)); + CHECK(prism::slice(parser.get(), root->location) == "foo"); +} + +PRISM_TEST(moving_a_parser_keeps_its_tree_usable) { + prism::unique_arena arena = prism::make_arena(); + prism::unique_parser original = prism::make_parser(arena.get(), "foo"); + pm_node_t *root = pm_parse(original.get()); + + prism::unique_parser moved = std::move(original); + CHECK(original == nullptr); + CHECK(prism::slice(moved.get(), root->location) == "foo"); +} + +PRISM_TEST(options_reach_the_parser) { + prism::unique_options options = prism::make_options(); + pm_options_filepath_set(options.get(), "example.rb"); + pm_options_line_set(options.get(), 10); + + prism_test::parse_result result("foo\nbar", options.get()); + CHECK(prism::view(*pm_parser_filepath(result.parser.get())) == "example.rb"); + CHECK(prism::line_column(result.parser.get(), result.statement(1)->location.start).line == 11); +} + +PRISM_TEST(default_options_start_at_line_one) { + prism::unique_options options = prism::make_options(); + prism_test::parse_result with_options("foo\nbar", options.get()); + prism_test::parse_result without_options("foo\nbar"); + + CHECK(prism::line_column(with_options.parser.get(), with_options.statement(1)->location.start).line == 2); + CHECK(prism::line_column(without_options.parser.get(), without_options.statement(1)->location.start).line == 2); +} + +PRISM_TEST(buffer_view_holds_what_was_written) { + prism_test::parse_result result("foo"); + prism::unique_buffer buffer = prism::make_buffer(); + CHECK(prism::view(buffer.get()).empty()); + + pm_prettyprint(buffer.get(), result.parser.get(), result.root); + std::string_view text = prism::view(buffer.get()); + CHECK(text.substr(0, 14) == "@ ProgramNode "); + CHECK(text.size() == pm_buffer_length(buffer.get())); +} diff --git a/cpp/parser_test.cpp b/cpp/parser_test.cpp new file mode 100644 index 0000000000..31b5d8d1ae --- /dev/null +++ b/cpp/parser_test.cpp @@ -0,0 +1,169 @@ +#include "prism_test.hpp" + +#include +#include +#include +#include +#include +#include +#if __cplusplus >= 202002L +#include +#endif + +/* The ranges are forward ranges of pointers to the C structs. */ +using comment_iterator = prism::comment_range::iterator; +static_assert(std::is_same_v::iterator_category, std::forward_iterator_tag>); +static_assert(std::is_same_v::value_type, const pm_comment_t *>); +static_assert(std::is_default_constructible_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +#if __cplusplus >= 202002L +static_assert(std::forward_iterator); +static_assert(std::ranges::forward_range); +static_assert(std::ranges::sized_range); +#endif + +/* Counts the items in a range by walking it. */ +template static std::size_t +walked(const R &range) { + std::size_t count = 0; + for (auto item : range) { + (void) item; + count++; + } + return count; +} + +PRISM_TEST(constant_names_a_node) { + prism_test::parse_result result("foo; Bar"); + + CHECK(prism::constant(result.parser.get(), prism::cast(result.statement(0))->name) == "foo"); + CHECK(prism::constant(result.parser.get(), prism::cast(result.statement(1))->name) == "Bar"); +} + +PRISM_TEST(slice_returns_the_source_of_a_location) { + prism_test::parse_result result("foo.bar(1, 2)"); + const pm_call_node_t *call = prism::cast(result.statement(0)); + const pm_parser_t *parser = result.parser.get(); + + CHECK(prism::slice(parser, result.root->location) == "foo.bar(1, 2)"); + CHECK(prism::slice(parser, call->message_loc) == "bar"); + CHECK(prism::slice(parser, call->receiver->location) == "foo"); + CHECK(prism::slice(parser, call->arguments->base.location) == "1, 2"); +} + +PRISM_TEST(line_column_locates_an_offset) { + prism_test::parse_result result("foo\n bar\n"); + const pm_parser_t *parser = result.parser.get(); + pm_line_column_t first = prism::line_column(parser, result.statement(0)->location.start); + pm_line_column_t second = prism::line_column(parser, result.statement(1)->location.start); + + CHECK(first.line == 1 && first.column == 0); + CHECK(second.line == 2 && second.column == 2); +} + +PRISM_TEST(errors_are_all_visited) { + std::string_view source = "def (; 1 +; ["; + prism_test::parse_result result(source); + prism::diagnostic_range errors = prism::errors(result.parser.get()); + + CHECK(!errors.empty()); + CHECK(walked(errors) == errors.size()); + + for (const pm_diagnostic_t *error : errors) { + pm_location_t location = pm_diagnostic_location(error); + CHECK(std::strlen(pm_diagnostic_message(error)) > 0); + CHECK(location.start + location.length <= source.size()); + } +} + +PRISM_TEST(valid_source_has_no_errors) { + prism_test::parse_result result("x = 1\nx"); + prism::diagnostic_range errors = prism::errors(result.parser.get()); + + CHECK(errors.empty()); + CHECK(errors.size() == 0); + CHECK(errors.begin() == errors.end()); +} + +PRISM_TEST(warnings_are_separate_from_errors) { + /* Warns that the addition is useless in a void context. */ + prism_test::parse_result result("1 + 2"); + + CHECK(prism::errors(result.parser.get()).empty()); + CHECK(prism::warnings(result.parser.get()).size() == 1); + CHECK(walked(prism::warnings(result.parser.get())) == 1); +} + +PRISM_TEST(comments_are_visited_in_source_order) { + prism_test::parse_result result("# one\nfoo # two\n=begin\nthree\n=end\n"); + const pm_parser_t *parser = result.parser.get(); + prism::comment_range comments = prism::comments(parser); + + std::vector texts; + for (const pm_comment_t *comment : comments) texts.push_back(prism::slice(parser, pm_comment_location(comment))); + + CHECK(comments.size() == 3); + CHECK(texts.size() == 3); + CHECK(texts[0] == "# one"); + CHECK(texts[1] == "# two"); + CHECK(texts[2].substr(0, 6) == "=begin"); + + auto embdocs = std::count_if(comments.begin(), comments.end(), [](const pm_comment_t *comment) { + return pm_comment_type(comment) == PM_COMMENT_EMBDOC; + }); + CHECK(embdocs == 1); +} + +PRISM_TEST(magic_comments_have_keys_and_values) { + prism_test::parse_result result("# frozen_string_literal: true\n# encoding: ascii\n"); + const pm_parser_t *parser = result.parser.get(); + prism::magic_comment_range magic = prism::magic_comments(parser); + + CHECK(magic.size() == 2); + + auto encoding = std::find_if(magic.begin(), magic.end(), [&](const pm_magic_comment_t *comment) { + return prism::slice(parser, pm_magic_comment_key(comment)) == "encoding"; + }); + CHECK(encoding != magic.end()); + CHECK(encoding != magic.end() && prism::slice(parser, pm_magic_comment_value(*encoding)) == "ascii"); +} + +PRISM_TEST(ranges_stop_early_with_break) { + prism_test::parse_result result("# a\n# b\n# c\n# d\n"); + std::size_t seen = 0; + + for (const pm_comment_t *comment : prism::comments(result.parser.get())) { + seen++; + if (prism::slice(result.parser.get(), pm_comment_location(comment)) == "# b") break; + } + + CHECK(seen == 2); +} + +PRISM_TEST(range_iterators_advance_both_ways) { + prism_test::parse_result result("# a\n# b\n"); + prism::comment_range comments = prism::comments(result.parser.get()); + + comment_iterator prefix = comments.begin(); + comment_iterator postfix = comments.begin(); + comment_iterator previous = postfix++; + + CHECK(previous == comments.begin()); + CHECK(++prefix == postfix); + CHECK(++prefix == comments.end()); +} + +#if __cplusplus >= 202002L +PRISM_TEST(ranges_compose_with_views) { + prism_test::parse_result result("# a\n=begin\nb\n=end\n# c\n"); + auto inline_comments = prism::comments(result.parser.get()) | std::views::filter([](const pm_comment_t *comment) { + return pm_comment_type(comment) == PM_COMMENT_INLINE; + }); + + CHECK(std::ranges::distance(inline_comments) == 2); +} +#endif diff --git a/cpp/prism_test.cpp b/cpp/prism_test.cpp new file mode 100644 index 0000000000..7dc6e0c65a --- /dev/null +++ b/cpp/prism_test.cpp @@ -0,0 +1,14 @@ +#include "prism_test.hpp" + +#include + +int main() { + for (const prism_test::test_case &test : prism_test::registry()) { + int before = prism_test::failures(); + test.function(); + std::printf("%s %s\n", prism_test::failures() == before ? "ok " : "FAILED", test.name); + } + + std::printf("%zu tests, %d failed checks\n", prism_test::registry().size(), prism_test::failures()); + return prism_test::failures() == 0 ? 0 : 1; +} diff --git a/cpp/prism_test.hpp b/cpp/prism_test.hpp new file mode 100644 index 0000000000..5e9419bef1 --- /dev/null +++ b/cpp/prism_test.hpp @@ -0,0 +1,89 @@ +/* A minimal test harness for the C++ interface, so that the tests build with + * nothing but a C++17 compiler and prism itself. Each *_test.cpp file + * registers its tests with PRISM_TEST, and prism_test.cpp runs them all. */ +#ifndef PRISM_TEST_HPP +#define PRISM_TEST_HPP + +#include "prism.hpp" + +#include +#include +#include + +namespace prism_test { + +/* A registered test. */ +struct test_case { + const char *name; + void (*function)(); +}; + +/* Every registered test, in the order the files registered them. This is a + * function-local static so that it exists before any registration runs, + * whatever order the files are initialized in. */ +inline std::vector & +registry() { + static std::vector cases; + return cases; +} + +/* The number of checks that have failed so far. */ +inline int & +failures() { + static int count = 0; + return count; +} + +/* Adds a test to the registry when the file that defines it is initialized. */ +struct registrar { + registrar(const char *name, void (*function)()) { + registry().push_back(test_case { name, function }); + } +}; + +/* Records a failed check without stopping the test, so one run reports every + * failure. */ +inline void +check(bool passed, const char *expression, const char *file, int line) { + if (!passed) { + failures()++; + std::fprintf(stderr, "%s:%d: check failed: %s\n", file, line, expression); + } +} + +/* A parsed source, owning everything its tree refers to. The arena is declared + * first so that it is destroyed after the parser that allocates from it. */ +struct parse_result { + prism::unique_arena arena = prism::make_arena(); + prism::unique_parser parser; + pm_node_t *root; + + explicit parse_result(std::string_view source, const pm_options_t *options = nullptr) : + parser(prism::make_parser(arena.get(), source, options)), + root(pm_parse(parser.get())) {} + + /* The statements at the top level of the program. */ + pm_node_list_t & + statements() const { + return prism::cast(root)->statements->body; + } + + /* The top-level statement at the given index. */ + pm_node_t * + statement(std::size_t index) const { + return statements().nodes[index]; + } +}; + +} + +/* Defines and registers a test. */ +#define PRISM_TEST(name) \ + static void name(); \ + static const prism_test::registrar name##_registrar(#name, name); \ + static void name() + +/* Checks that the given condition holds, recording a failure otherwise. */ +#define CHECK(condition) prism_test::check((condition), #condition, __FILE__, __LINE__) + +#endif diff --git a/cpp/stringy_test.cpp b/cpp/stringy_test.cpp new file mode 100644 index 0000000000..2021202b02 --- /dev/null +++ b/cpp/stringy_test.cpp @@ -0,0 +1,32 @@ +#include "prism_test.hpp" + +#include +#include +#include + +static_assert(noexcept(prism::view(std::declval()))); + +PRISM_TEST(view_returns_the_unescaped_bytes) { + prism_test::parse_result result("\"a\\nb\"; :sym; 'raw\\n'"); + + CHECK(prism::view(prism::cast(result.statement(0))->unescaped) == "a\nb"); + CHECK(prism::view(prism::cast(result.statement(1))->unescaped) == "sym"); + CHECK(prism::view(prism::cast(result.statement(2))->unescaped) == "raw\\n"); +} + +PRISM_TEST(view_keeps_embedded_null_bytes) { + prism_test::parse_result result("\"a\\0b\""); + std::string_view bytes = prism::view(prism::cast(result.statement(0))->unescaped); + + CHECK(bytes.size() == 3); + CHECK(bytes == std::string_view("a\0b", 3)); +} + +PRISM_TEST(view_of_an_initialized_string) { + static const uint8_t bytes[] = { 'h', 'i' }; + pm_string_t string; + pm_string_init(&string, bytes, sizeof(bytes)); + + CHECK(prism::view(string) == "hi"); + CHECK(prism::view(string).data() == reinterpret_cast(bytes)); +} diff --git a/cpp/test.cpp b/cpp/test.cpp deleted file mode 100644 index fec4db7c0f..0000000000 --- a/cpp/test.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "prism.h" - -#include - -int main() { - pm_arena_t *arena = pm_arena_new(); - pm_parser_t *parser = pm_parser_new(arena, reinterpret_cast("1 + 2"), 5, NULL); - - pm_node_t *root = pm_parse(parser); - pm_buffer_t *buffer = pm_buffer_new(); - - pm_prettyprint(buffer, parser, root); - - std::string_view view(pm_buffer_value(buffer), pm_buffer_length(buffer)); - std::cout << view << std::endl; - - pm_buffer_free(buffer); - pm_parser_free(parser); - pm_arena_free(arena); - - return 0; -} diff --git a/cpp/visit_test.cpp b/cpp/visit_test.cpp new file mode 100644 index 0000000000..c22dd5014a --- /dev/null +++ b/cpp/visit_test.cpp @@ -0,0 +1,191 @@ +#include "prism_test.hpp" + +#include +#include +#include +#include + +static pm_visit_result_t +descend(const pm_node_t *) { + return PM_VISIT_DESCEND; +} + +/* A visitor that keeps state in itself, to check that the walk calls the + * caller's object rather than a copy of it. */ +struct counter { + std::size_t count = 0; + + pm_visit_result_t + operator()(const pm_node_t *) { + count++; + return PM_VISIT_DESCEND; + } +}; + +PRISM_TEST(visit_node_visits_every_node) { + /* ProgramNode, StatementsNode, CallNode, ArgumentsNode, IntegerNode */ + prism_test::parse_result result("foo(1)"); + std::size_t count = 0; + + bool completed = prism::visit_node(result.root, [&](const pm_node_t *) { + count++; + return PM_VISIT_DESCEND; + }); + + CHECK(completed); + CHECK(count == 5); +} + +PRISM_TEST(visit_node_visits_in_pre_order) { + prism_test::parse_result result("foo(bar)"); + std::vector types; + + prism::visit_node(result.root, [&](const pm_node_t *node) { + types.push_back(node->type); + return PM_VISIT_DESCEND; + }); + + std::vector expected = { PM_PROGRAM_NODE, PM_STATEMENTS_NODE, PM_CALL_NODE, PM_ARGUMENTS_NODE, PM_CALL_NODE }; + CHECK(types == expected); +} + +PRISM_TEST(visit_node_skips_children) { + prism_test::parse_result result("foo(bar(baz)); qux"); + std::size_t count = 0; + + bool completed = prism::visit_node(result.root, [&](const pm_node_t *node) { + count++; + return prism::isa(node) ? PM_VISIT_SKIP : PM_VISIT_DESCEND; + }); + + /* ProgramNode, StatementsNode, and the two top-level calls. */ + CHECK(completed); + CHECK(count == 4); +} + +PRISM_TEST(visit_node_stops_the_whole_walk) { + prism_test::parse_result result("a; b(1); c; d"); + std::size_t count = 0; + const pm_node_t *found = nullptr; + + bool completed = prism::visit_node(result.root, [&](const pm_node_t *node) { + count++; + const pm_call_node_t *call = prism::dyn_cast(node); + + if (call != nullptr && call->arguments != nullptr) { + found = node; + return PM_VISIT_STOP; + } + + return PM_VISIT_DESCEND; + }); + + /* ProgramNode, StatementsNode, a, and then b, where the walk stops. */ + CHECK(!completed); + CHECK(count == 4); + CHECK(found != nullptr && prism::slice(result.parser.get(), found->location) == "b(1)"); +} + +PRISM_TEST(visit_child_nodes_skips_the_node_itself) { + prism_test::parse_result result("a; b"); + std::size_t count = 0; + + bool completed = prism::visit_child_nodes(result.root, [&](const pm_node_t *node) { + count++; + CHECK(node != result.root); + return PM_VISIT_DESCEND; + }); + + /* StatementsNode and the two calls. */ + CHECK(completed); + CHECK(count == 3); +} + +PRISM_TEST(visit_child_nodes_stops_the_whole_walk) { + prism_test::parse_result result("a; b; c"); + std::size_t count = 0; + + bool completed = prism::visit_child_nodes(result.root, [&](const pm_node_t *node) { + count++; + return prism::isa(node) ? PM_VISIT_STOP : PM_VISIT_DESCEND; + }); + + CHECK(!completed); + CHECK(count == 2); +} + +PRISM_TEST(visit_node_accepts_any_callable) { + prism_test::parse_result result("foo(1)"); + + /* A function by name and by pointer. */ + CHECK(prism::visit_node(result.root, descend)); + CHECK(prism::visit_node(result.root, &descend)); + + /* An object passed by reference keeps the state it accumulates. */ + counter visitor; + prism::visit_node(result.root, visitor); + CHECK(visitor.count == 5); + + /* A const lambda and a mutable one. */ + const auto const_lambda = [](const pm_node_t *) { return PM_VISIT_DESCEND; }; + CHECK(prism::visit_node(result.root, const_lambda)); + + std::size_t count = 0; + auto mutable_lambda = [count](const pm_node_t *) mutable { count++; return PM_VISIT_DESCEND; }; + CHECK(prism::visit_node(result.root, mutable_lambda)); +} + +#ifdef PRISM_HAS_EXCEPTIONS + +struct found_it : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +PRISM_TEST(visit_node_propagates_exceptions) { + prism_test::parse_result result("a; b(1); c"); + std::size_t count = 0; + bool caught = false; + + try { + prism::visit_node(result.root, [&](const pm_node_t *node) { + count++; + const pm_call_node_t *call = prism::dyn_cast(node); + if (call != nullptr && call->arguments != nullptr) throw found_it("b(1)"); + return PM_VISIT_DESCEND; + }); + } catch (const found_it &error) { + caught = std::string_view(error.what()) == "b(1)"; + } + + /* The walk stops at the throw: nothing after b(1) is visited. */ + CHECK(caught); + CHECK(count == 4); +} + +PRISM_TEST(visit_child_nodes_propagates_exceptions_of_any_type) { + prism_test::parse_result result("a; b"); + int caught = 0; + + try { + prism::visit_child_nodes(result.root, [](const pm_node_t *) -> pm_visit_result_t { throw 42; }); + } catch (int value) { + caught = value; + } + + CHECK(caught == 42); +} + +PRISM_TEST(visit_node_is_reusable_after_an_exception) { + prism_test::parse_result result("foo(1)"); + + try { + prism::visit_node(result.root, [](const pm_node_t *) -> pm_visit_result_t { throw 1; }); + } catch (int) { + } + + counter visitor; + CHECK(prism::visit_node(result.root, visitor)); + CHECK(visitor.count == 5); +} + +#endif diff --git a/include/prism.hpp b/include/prism.hpp new file mode 100644 index 0000000000..d0a6e4225c --- /dev/null +++ b/include/prism.hpp @@ -0,0 +1,24 @@ +/** + * @file prism.hpp + * + * The main header file for the C++ interface to the prism parser. It adds + * ownership, checked casts between node types, and callable-based iteration on + * top of the C API. It never wraps the C structs: every function in it takes + * and returns the same pm_*_t types that prism.h declares, so the two can be + * mixed freely. It requires C++17 or later. + */ +#ifndef PRISM_HPP +#define PRISM_HPP + +#include "prism.h" + +#include "prism/arena.hpp" +#include "prism/ast.hpp" +#include "prism/buffer.hpp" +#include "prism/node.hpp" +#include "prism/node_type.hpp" +#include "prism/options.hpp" +#include "prism/parser.hpp" +#include "prism/stringy.hpp" + +#endif diff --git a/include/prism/arena.hpp b/include/prism/arena.hpp new file mode 100644 index 0000000000..03acf08c66 --- /dev/null +++ b/include/prism/arena.hpp @@ -0,0 +1,42 @@ +/** + * @file arena.hpp + * + * Ownership of arenas from C++. + */ +#ifndef PRISM_ARENA_HPP +#define PRISM_ARENA_HPP + +#include "prism/compiler/cplusplus.hpp" + +#include "prism/arena.h" + +#include + +namespace prism { + +/** Frees an arena with pm_arena_free. */ +struct arena_deleter { + /** + * Frees the given arena. + * + * @param arena The arena to free. + */ + void operator()(pm_arena_t *arena) const noexcept { pm_arena_free(arena); } +}; + +/** An owned arena. */ +using unique_arena = std::unique_ptr; + +/** + * Allocate a new arena. Allocation failure aborts, so this never throws. + * + * @return A unique_arena managing the newly allocated arena. + */ +[[nodiscard]] inline unique_arena +make_arena() noexcept { + return unique_arena(pm_arena_new()); +} + +} + +#endif diff --git a/include/prism/buffer.hpp b/include/prism/buffer.hpp new file mode 100644 index 0000000000..8972cd42ac --- /dev/null +++ b/include/prism/buffer.hpp @@ -0,0 +1,55 @@ +/** + * @file buffer.hpp + * + * Ownership of buffers from C++, and access to their contents. + */ +#ifndef PRISM_BUFFER_HPP +#define PRISM_BUFFER_HPP + +#include "prism/compiler/cplusplus.hpp" +#include "prism/compiler/lifetimebound.hpp" + +#include "prism/buffer.h" + +#include +#include + +namespace prism { + +/** Frees a buffer with pm_buffer_free. */ +struct buffer_deleter { + /** + * Frees the given buffer. + * + * @param buffer The buffer to free. + */ + void operator()(pm_buffer_t *buffer) const noexcept { pm_buffer_free(buffer); } +}; + +/** An owned buffer. */ +using unique_buffer = std::unique_ptr; + +/** + * Allocate a new empty buffer. Allocation failure aborts, so this never throws. + * + * @return A unique_buffer managing the newly allocated buffer. + */ +[[nodiscard]] inline unique_buffer +make_buffer() noexcept { + return unique_buffer(pm_buffer_new()); +} + +/** + * The bytes written to the given buffer so far. + * + * @param buffer The buffer to view. + * @return A string_view of the contents of the buffer. + */ +[[nodiscard]] inline std::string_view +view(const pm_buffer_t *buffer PRISM_LIFETIMEBOUND) noexcept { + return std::string_view(pm_buffer_value(buffer), pm_buffer_length(buffer)); +} + +} + +#endif diff --git a/include/prism/compiler/cplusplus.hpp b/include/prism/compiler/cplusplus.hpp new file mode 100644 index 0000000000..59cca7dd5d --- /dev/null +++ b/include/prism/compiler/cplusplus.hpp @@ -0,0 +1,16 @@ +/** + * @file compiler/cplusplus.hpp + */ +#ifndef PRISM_COMPILER_CPLUSPLUS_HPP +#define PRISM_COMPILER_CPLUSPLUS_HPP + +/** + * The C++ interface relies on C++17 for inline variables, std::string_view, + * std::void_t, and std::is_invocable_r_v. MSVC reports __cplusplus as 199711L + * unless /Zc:__cplusplus is passed, so its own _MSVC_LANG is checked as well. + */ +#if !defined(__cplusplus) || ((__cplusplus < 201703L) && (!defined(_MSVC_LANG) || (_MSVC_LANG < 201703L))) +# error "the prism C++ interface requires C++17 or later" +#endif + +#endif diff --git a/include/prism/compiler/exceptions.hpp b/include/prism/compiler/exceptions.hpp new file mode 100644 index 0000000000..4ac688da35 --- /dev/null +++ b/include/prism/compiler/exceptions.hpp @@ -0,0 +1,17 @@ +/** + * @file compiler/exceptions.hpp + */ +#ifndef PRISM_COMPILER_EXCEPTIONS_HPP +#define PRISM_COMPILER_EXCEPTIONS_HPP + +/** + * Defined when C++ exceptions are enabled, so that code which catches them can + * still compile in builds that disable them, such as with -fno-exceptions. + * __cpp_exceptions is the standard feature test macro, and older versions of + * GCC and MSVC define __EXCEPTIONS and _CPPUNWIND instead. + */ +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) +# define PRISM_HAS_EXCEPTIONS +#endif + +#endif diff --git a/include/prism/compiler/lifetimebound.hpp b/include/prism/compiler/lifetimebound.hpp new file mode 100644 index 0000000000..145c09c236 --- /dev/null +++ b/include/prism/compiler/lifetimebound.hpp @@ -0,0 +1,25 @@ +/** + * @file compiler/lifetimebound.hpp + */ +#ifndef PRISM_COMPILER_LIFETIMEBOUND_HPP +#define PRISM_COMPILER_LIFETIMEBOUND_HPP + +/** + * Mark a parameter that the return value points into, so that the compiler + * warns if a temporary is passed and the return value outlives it. If it is + * placed after the qualifiers of a member function, it marks the object + * instead. + */ +#if defined(__has_cpp_attribute) +# if __has_cpp_attribute(clang::lifetimebound) +# define PRISM_LIFETIMEBOUND [[clang::lifetimebound]] +# elif __has_cpp_attribute(msvc::lifetimebound) +# define PRISM_LIFETIMEBOUND [[msvc::lifetimebound]] +# endif +#endif + +#ifndef PRISM_LIFETIMEBOUND +# define PRISM_LIFETIMEBOUND +#endif + +#endif diff --git a/include/prism/node.hpp b/include/prism/node.hpp new file mode 100644 index 0000000000..b2fa3d366b --- /dev/null +++ b/include/prism/node.hpp @@ -0,0 +1,272 @@ +/** + * @file node.hpp + * + * Checked casts between node structs, iteration over lists of nodes, and + * walking the tree from C++. + */ +#ifndef PRISM_NODE_HPP +#define PRISM_NODE_HPP + +#include "prism/compiler/cplusplus.hpp" +#include "prism/compiler/exceptions.hpp" +#include "prism/compiler/lifetimebound.hpp" + +#include "prism/ast.hpp" +#include "prism/node.h" +#include "prism/node_type.hpp" + +#include +#include +#include +#include +#include +#include + +namespace prism { + +/** + * Whether the given node, which must not be null, is of the given struct + * + * @tparam T The node struct type to check against. + * @param node The node to check. + * @return True if the node is of the given struct, false otherwise. + */ +template [[nodiscard]] bool +isa(const pm_node_t *node) noexcept { + static_assert(is_node_v, "isa requires a node struct declared in ast.h"); + assert(node != nullptr); + return node->type == node_type_v; +} + +/** + * Cast the given node, which must not be null and must be of the given struct, + * to that struct. The type is checked in debug builds. + * + * @tparam T The node struct type to cast to. + * @param node The node to cast. + * @return The node cast to the given struct. + */ +template [[nodiscard]] T * +cast(pm_node_t *node) noexcept { + assert(isa(node)); + return reinterpret_cast(node); +} + +/** + * Cast the given node, which must not be null and must be of the given struct, + * to that struct. The type is checked in debug builds. + * + * @tparam T The node struct type to cast to. + * @param node The node to cast. + * @return The node cast to the given struct. + */ +template [[nodiscard]] const T * +cast(const pm_node_t *node) noexcept { + assert(isa(node)); + return reinterpret_cast(node); +} + +/** + * Cast the given node to the given struct if it is of that struct, and return + * null otherwise. The node may be null, in which case this returns null. + * + * @tparam T The node struct type to cast to. + * @param node The node to cast. + * @return The node cast to the given struct if it is of that struct, or null + * otherwise. + */ +template [[nodiscard]] T * +dyn_cast(pm_node_t *node) noexcept { + return (node != nullptr && isa(node)) ? reinterpret_cast(node) : nullptr; +} + +/** + * Cast the given node to the given struct if it is of that struct, and return + * null otherwise. The node may be null, in which case this returns null. + * + * @tparam T The node struct type to cast to. + * @param node The node to cast. + * @return The node cast to the given struct if it is of that struct, or null + * otherwise. + */ +template [[nodiscard]] const T * +dyn_cast(const pm_node_t *node) noexcept { + return (node != nullptr && isa(node)) ? reinterpret_cast(node) : nullptr; +} + +/** + * Return the pm_node_t embedded at the start of the given node struct. + * + * @tparam T The node struct type. + * @param node The node struct to upcast. + * @return The pm_node_t embedded at the start of the node struct. + */ +template [[nodiscard]] pm_node_t * +upcast(T *node) noexcept { + static_assert(is_node_v, "upcast requires a node struct declared in ast.h"); + return &node->base; +} + +/** + * Return the pm_node_t embedded at the start of the given node struct. + * + * @tparam T The node struct type. + * @param node The node struct to upcast. + * @return The pm_node_t embedded at the start of the node struct. + */ +template [[nodiscard]] const pm_node_t * +upcast(const T *node) noexcept { + static_assert(is_node_v, "upcast requires a node struct declared in ast.h"); + return &node->base; +} + +/** + * The nodes in a list, usable in a range-based for loop. It refers to the list + * rather than copying it, so the list must outlive it. + */ +class node_range { +public: + /** + * Create a range over the nodes in the given list. + * + * @param list The list of nodes to create a range for. + */ + explicit node_range(const pm_node_list_t &list PRISM_LIFETIMEBOUND) noexcept : list_(&list) {} + + /** + * A pointer to the first node. + * + * @return A pointer to the first node, or null if the list is empty. + */ + pm_node_t *const *begin() const noexcept { return list_->nodes; } + + /** + * A pointer one past the last node. + * + * @return A pointer one past the last node, or null if the list is empty. + */ + pm_node_t *const *end() const noexcept { return list_->nodes + list_->size; } + + /** + * The number of nodes. + * + * @return The number of nodes. + */ + std::size_t size() const noexcept { return list_->size; } + + /** + * Whether there are no nodes. + * + * @return True if there are no nodes, false otherwise. + */ + [[nodiscard]] bool empty() const noexcept { return list_->size == 0; } + +private: + const pm_node_list_t *list_; +}; + +namespace detail { + +/** + * The state shared between a walk and its C callback, passed through the C + * function's void * data parameter. + */ +template struct visit_context { + /** + * The context for a visit, holding the visitor and any exception thrown. + * + * @param visitor The visitor callable to be used for each node. + */ + explicit visit_context(F *visitor) noexcept : callable(visitor) {} + + /** The visitor that the walk calls for each node. */ + F *callable; + +#ifdef PRISM_HAS_EXCEPTIONS + /** The exception thrown by the visitor, if any. */ + std::exception_ptr exception; +#endif +}; + +/** + * Forwards the C visitor callback to a C++ callable. An exception cannot unwind + * through the C frames that call this function, so one thrown by the callable + * is caught here and the walk is stopped. The function that started the walk + * rethrows it once the C walk has returned. Without exceptions, the callable + * cannot throw, so it is called directly. + * + * @tparam F The type of the visitor callable. + * @param node The current node being visited. + * @param data The visit context containing the visitor and exception state. + * @return The result of the visit, indicating whether to descend, skip, or stop. + */ +template pm_visit_result_t +visit_callback(const pm_node_t *node, void *data) noexcept { + visit_context *context = static_cast *>(data); + +#ifdef PRISM_HAS_EXCEPTIONS + try { + return std::invoke(*context->callable, node); + } catch (...) { + context->exception = std::current_exception(); + return PM_VISIT_STOP; + } +#else + return std::invoke(*context->callable, node); +#endif +} + +} + +/** + * Walk the tree rooted at the given node in pre-order, calling the visitor with + * each node. The visitor returns PM_VISIT_DESCEND to continue into that node's + * children, PM_VISIT_SKIP to continue past them, or PM_VISIT_STOP to end the + * walk. Returns false if the walk was stopped. If the visitor throws, the walk + * stops and the exception propagates from this function. + * + * @tparam F The type of the visitor callable. + * @param node The root node to start the walk from. + * @param visitor The visitor callable to invoke for each node. + * @return True if the walk completed, false if it was stopped. + */ +template bool +visit_node(const pm_node_t *node, F &&visitor) { + static_assert(std::is_invocable_r_v, "visit_node requires a callable taking const pm_node_t * and returning pm_visit_result_t"); + detail::visit_context> context(std::addressof(visitor)); + bool completed = pm_visit_node(node, detail::visit_callback>, &context); + +#ifdef PRISM_HAS_EXCEPTIONS + if (context.exception) std::rethrow_exception(context.exception); +#endif + + return completed; +} + +/** + * Walk the children of the given node, as visit_node does for every node whose + * visitor returns PM_VISIT_DESCEND. Returns false if the walk was stopped. If + * the visitor throws, the walk stops and the exception propagates from this + * function. + * + * @tparam F The type of the visitor callable. + * @param node The node whose children to walk. + * @param visitor The visitor callable to invoke for each child node. + * @return True if the walk completed, false if it was stopped. + */ +template bool +visit_child_nodes(const pm_node_t *node, F &&visitor) { + static_assert(std::is_invocable_r_v, "visit_child_nodes requires a callable taking const pm_node_t * and returning pm_visit_result_t"); + detail::visit_context> context(std::addressof(visitor)); + bool completed = pm_visit_child_nodes(node, detail::visit_callback>, &context); + +#ifdef PRISM_HAS_EXCEPTIONS + if (context.exception) std::rethrow_exception(context.exception); +#endif + + return completed; +} + +} + +#endif diff --git a/include/prism/node_type.hpp b/include/prism/node_type.hpp new file mode 100644 index 0000000000..e6227bfb5a --- /dev/null +++ b/include/prism/node_type.hpp @@ -0,0 +1,38 @@ +/** + * @file node_type.hpp + * + * The traits that map node structs to node types. The specialization for each + * node struct is generated into ast.hpp. + */ +#ifndef PRISM_NODE_TYPE_HPP +#define PRISM_NODE_TYPE_HPP + +#include "prism/compiler/cplusplus.hpp" + +#include "prism/ast.h" + +#include + +namespace prism { + +/** + * Maps a node struct to its node type, as in node_type::value + * being PM_CALL_NODE. Only the node structs declared in ast.h have a value. + */ +template struct node_type {}; + +/** The node type of the given node struct. */ +template inline constexpr pm_node_type_t node_type_v = node_type::value; + +/** Whether the given type is one of the node structs declared in ast.h. */ +template struct is_node : std::false_type {}; + +/** Whether the given type is one of the node structs declared in ast.h. */ +template struct is_node::value)>> : std::true_type {}; + +/** Whether the given type is one of the node structs declared in ast.h. */ +template inline constexpr bool is_node_v = is_node::value; + +} + +#endif diff --git a/include/prism/options.hpp b/include/prism/options.hpp new file mode 100644 index 0000000000..40ce341598 --- /dev/null +++ b/include/prism/options.hpp @@ -0,0 +1,43 @@ +/** + * @file options.hpp + * + * Ownership of parse options from C++. + */ +#ifndef PRISM_OPTIONS_HPP +#define PRISM_OPTIONS_HPP + +#include "prism/compiler/cplusplus.hpp" + +#include "prism/options.h" + +#include + +namespace prism { + +/** Frees options with pm_options_free. */ +struct options_deleter { + /** + * Free the given options. + * + * @param options The options to free. + */ + void operator()(pm_options_t *options) const noexcept { pm_options_free(options); } +}; + +/** An owned set of options. */ +using unique_options = std::unique_ptr; + +/** + * Allocate a new set of options with every option at its default. Allocation + * failure aborts, so this never throws. + * + * @return A unique_options owning the newly allocated options. + */ +[[nodiscard]] inline unique_options +make_options() noexcept { + return unique_options(pm_options_new()); +} + +} + +#endif diff --git a/include/prism/parser.hpp b/include/prism/parser.hpp new file mode 100644 index 0000000000..133b1dfbb2 --- /dev/null +++ b/include/prism/parser.hpp @@ -0,0 +1,274 @@ +/** + * @file parser.hpp + * + * Ownership of parsers from C++, and lookups through a parser into the source, + * the constant pool, comments, and diagnostics. + */ +#ifndef PRISM_PARSER_HPP +#define PRISM_PARSER_HPP + +#include "prism/compiler/cplusplus.hpp" +#include "prism/compiler/lifetimebound.hpp" + +#include "prism/arena.h" +#include "prism/constant_pool.h" +#include "prism/line_offset_list.h" +#include "prism/parser.h" + +#include +#include +#include +#include +#include +#include + +namespace prism { + +/** Frees a parser with pm_parser_free. */ +struct parser_deleter { + /** + * Free the given parser. + * + * @param parser The parser to free. + */ + void operator()(pm_parser_t *parser) const noexcept { pm_parser_free(parser); } +}; + +/** An owned parser. */ +using unique_parser = std::unique_ptr; + +/** + * Allocate a new parser over the given source. The source, the arena, and the + * options (which may be null) are not copied, so all three must outlive the + * parser. Allocation failure aborts, so this never throws. + * + * @param arena The arena to use for allocations. + * @param source The source code to parse. + * @param options The parse options, or null for defaults. + * @return A unique_parser owning the newly allocated parser. + */ +[[nodiscard]] inline unique_parser +make_parser(pm_arena_t *arena PRISM_LIFETIMEBOUND, std::string_view source PRISM_LIFETIMEBOUND, const pm_options_t *options PRISM_LIFETIMEBOUND = nullptr) noexcept { + return unique_parser(pm_parser_new(arena, reinterpret_cast(source.data()), source.size(), options)); +} + +/** + * The name of the constant with the given id, which must not be zero. Fields + * holding an optional constant use zero for none, so check those first. + * + * @param parser The parser containing the constant. + * @param constant_id The id of the constant. + * @return The value of the constant. + */ +[[nodiscard]] inline std::string_view +constant(const pm_parser_t *parser PRISM_LIFETIMEBOUND, pm_constant_id_t constant_id) noexcept { + assert(constant_id != 0); + const pm_constant_t *entry = pm_parser_constant(parser, constant_id); + return std::string_view(reinterpret_cast(pm_constant_start(entry)), pm_constant_length(entry)); +} + +/** + * The bytes of source that the given location covers. + * + * @param parser The parser containing the source. + * @param location The location in the source. + * @return The bytes of source covered by the location. + */ +[[nodiscard]] inline std::string_view +slice(const pm_parser_t *parser PRISM_LIFETIMEBOUND, pm_location_t location) noexcept { + return std::string_view(reinterpret_cast(pm_parser_start(parser) + location.start), location.length); +} + +/** + * The line and column of the given byte offset into the source. Lines start at + * the parser's start line, and columns are byte offsets from the line start. + * + * @param parser The parser containing the source. + * @param offset The byte offset into the source. + * @return The line and column of the byte offset. + */ +[[nodiscard]] inline pm_line_column_t +line_column(const pm_parser_t *parser, std::uint32_t offset) noexcept { + return pm_line_offset_list_line_column(pm_parser_line_offsets(parser), offset, pm_parser_start_line(parser)); +} + +namespace detail { + +/** + * The diagnostics, comments, or magic comments found while parsing, usable in a + * range-based for loop and with the standard algorithms. Each element is a + * pointer to the C struct, which the C accessors read, and Next is the C + * function that returns the item after a given one. It refers to the parser + * rather than copying from it, so the parser must outlive it. It is used + * through diagnostic_range, comment_range, and magic_comment_range. + */ +template class next_range { +public: + /** Steps through the items one after another. */ + class iterator { + public: + /** The iterator category, for the standard algorithms. */ + using iterator_category = std::forward_iterator_tag; + + /** The type of each element. */ + using value_type = const T *; + + /** The type of the distance between two iterators. */ + using difference_type = std::ptrdiff_t; + + /** The type of a pointer to an element. */ + using pointer = const value_type *; + + /** The type of a reference to an element. */ + using reference = const value_type &; + + /** Create the iterator that marks the end of the items. */ + iterator() noexcept = default; + + /** + * Create an iterator at the given item. + * + * @param current The current item. + */ + explicit iterator(const T *current) noexcept : current_(current) {} + + /** + * The item at this position, which must not be the end. + * + * @return The current item. + */ + reference operator*() const noexcept { return current_; } + + /** + * Advance to the next item. + * + * @return The iterator after advancing. + */ + iterator & + operator++() noexcept { + assert(current_ != nullptr); + current_ = Next(current_); + return *this; + } + + /** + * Advance to the next item, returning the iterator from before. + * + * @return The iterator from before advancing. + */ + iterator + operator++(int) noexcept { + iterator previous = *this; + ++*this; + return previous; + } + + /** Whether the two iterators are at the same item. */ + friend bool operator==(iterator left, iterator right) noexcept { return left.current_ == right.current_; } + + /** Whether the two iterators are at different items. */ + friend bool operator!=(iterator left, iterator right) noexcept { return left.current_ != right.current_; } + + private: + const T *current_ = nullptr; + }; + + /** + * Create a range of the given number of items starting at first. + * + * @param first The first item. + * @param size The number of items. + */ + next_range(const T *first, std::size_t size) noexcept : first_(first), size_(size) {} + + /** + * An iterator at the first item. + * + * @return An iterator at the first item. + */ + iterator begin() const noexcept { return iterator(first_); } + + /** + * An iterator past the last item. + * + * @return An iterator past the last item. + */ + iterator end() const noexcept { return iterator(); } + + /** + * The number of items. + * + * @return The number of items. + */ + std::size_t size() const noexcept { return size_; } + + /** + * Whether there are no items. + * + * @return Whether there are no items. + */ + [[nodiscard]] bool empty() const noexcept { return size_ == 0; } + +private: + const T *first_; + std::size_t size_; +}; + +} + +/** The errors or warnings found while parsing. */ +using diagnostic_range = detail::next_range; + +/** The comments found while parsing. */ +using comment_range = detail::next_range; + +/** The magic comments found while parsing. */ +using magic_comment_range = detail::next_range; + +/** + * The errors found while parsing. + * + * @param parser The parser containing the source. + * @return The range of errors found while parsing. + */ +[[nodiscard]] inline diagnostic_range +errors(const pm_parser_t *parser PRISM_LIFETIMEBOUND) noexcept { + return diagnostic_range(pm_parser_errors_first(parser), pm_parser_errors_size(parser)); +} + +/** + * The warnings found while parsing. + * + * @param parser The parser containing the source. + * @return The range of warnings found while parsing. + */ +[[nodiscard]] inline diagnostic_range +warnings(const pm_parser_t *parser PRISM_LIFETIMEBOUND) noexcept { + return diagnostic_range(pm_parser_warnings_first(parser), pm_parser_warnings_size(parser)); +} + +/** + * The comments found while parsing, in source order. + * + * @param parser The parser containing the source. + * @return The range of comments found while parsing. + */ +[[nodiscard]] inline comment_range +comments(const pm_parser_t *parser PRISM_LIFETIMEBOUND) noexcept { + return comment_range(pm_parser_comments_first(parser), pm_parser_comments_size(parser)); +} + +/** + * The magic comments found while parsing. + * + * @param parser The parser containing the source. + * @return The range of magic comments found while parsing. + */ +[[nodiscard]] inline magic_comment_range +magic_comments(const pm_parser_t *parser PRISM_LIFETIMEBOUND) noexcept { + return magic_comment_range(pm_parser_magic_comments_first(parser), pm_parser_magic_comments_size(parser)); +} + +} + +#endif diff --git a/include/prism/stringy.hpp b/include/prism/stringy.hpp new file mode 100644 index 0000000000..491f8ed5d7 --- /dev/null +++ b/include/prism/stringy.hpp @@ -0,0 +1,31 @@ +/** + * @file stringy.hpp + * + * Access to the contents of strings from C++. + */ +#ifndef PRISM_STRINGY_HPP +#define PRISM_STRINGY_HPP + +#include "prism/compiler/cplusplus.hpp" +#include "prism/compiler/lifetimebound.hpp" + +#include "prism/stringy.h" + +#include + +namespace prism { + +/** + * The bytes of the given string. + * + * @param string The string to view. + * @return A view of the bytes of the string. + */ +[[nodiscard]] inline std::string_view +view(const pm_string_t &string PRISM_LIFETIMEBOUND) noexcept { + return std::string_view(reinterpret_cast(pm_string_source(&string)), pm_string_length(&string)); +} + +} + +#endif diff --git a/prism.gemspec b/prism.gemspec index c31aabb88f..a98cfd46f9 100644 --- a/prism.gemspec +++ b/prism.gemspec @@ -46,12 +46,15 @@ Gem::Specification.new do |spec| "ext/prism/extension.c", "ext/prism/extension.h", "include/prism.h", + "include/prism.hpp", "include/prism/compiler/accel.h", "include/prism/compiler/align.h", "include/prism/compiler/assume.h", "include/prism/compiler/branch.h", "include/prism/compiler/bswap.h", + "include/prism/compiler/cplusplus.hpp", "include/prism/compiler/endianness.h", + "include/prism/compiler/exceptions.hpp", "include/prism/compiler/exported.h", "include/prism/compiler/fallthrough.h", "include/prism/compiler/filesystem.h", @@ -59,6 +62,7 @@ Gem::Specification.new do |spec| "include/prism/compiler/force_inline.h", "include/prism/compiler/format.h", "include/prism/compiler/inline.h", + "include/prism/compiler/lifetimebound.hpp", "include/prism/compiler/nodiscard.h", "include/prism/compiler/nonnull.h", "include/prism/compiler/unused.h", @@ -91,8 +95,11 @@ Gem::Specification.new do |spec| "include/prism/internal/tokens.h", "include/prism/internal/xxh3.h", "include/prism/arena.h", + "include/prism/arena.hpp", "include/prism/ast.h", + "include/prism/ast.hpp", "include/prism/buffer.h", + "include/prism/buffer.hpp", "include/prism/comments.h", "include/prism/constant_pool.h", "include/prism/diagnostic.h", @@ -103,14 +110,19 @@ Gem::Specification.new do |spec| "include/prism/line_offset_list.h", "include/prism/magic_comments.h", "include/prism/node.h", + "include/prism/node.hpp", + "include/prism/node_type.hpp", "include/prism/options.h", + "include/prism/options.hpp", "include/prism/parser.h", + "include/prism/parser.hpp", "include/prism/prettyprint.h", "include/prism/serialize.h", "include/prism/source.h", "include/prism/stream.h", "include/prism/string_query.h", "include/prism/stringy.h", + "include/prism/stringy.hpp", "include/prism/version.h", "lib/prism.rb", "lib/prism/compiler.rb", diff --git a/templates/include/prism/ast.hpp.erb b/templates/include/prism/ast.hpp.erb new file mode 100644 index 0000000000..b3793b6fa8 --- /dev/null +++ b/templates/include/prism/ast.hpp.erb @@ -0,0 +1,31 @@ +/** + * @file ast.hpp + * + * The mapping between node structs and node types, generated from the same + * configuration as ast.h so that it covers every node. + */ +#ifndef PRISM_AST_HPP +#define PRISM_AST_HPP + +#include "prism/compiler/cplusplus.hpp" + +#include "prism/ast.h" +#include "prism/node_type.hpp" + +#include +#include + +namespace prism { + +/* Each node struct is cast to and from pm_node_t, which C++ allows only + * because the struct is standard-layout with pm_node_t as its first member. */ +<%- nodes.each do |node| -%> + +/** Maps pm_<%= node.human %>_t to <%= node.type %>. */ +template <> struct node_type_t> : std::integral_constant> {}; +static_assert(std::is_standard_layout_v_t> && offsetof(pm_<%= node.human %>_t, base) == 0, "pm_<%= node.human %>_t must begin with its pm_node_t"); +<%- end -%> + +} + +#endif diff --git a/templates/template.rb b/templates/template.rb index 669536cff1..c43ee6430e 100755 --- a/templates/template.rb +++ b/templates/template.rb @@ -753,6 +753,7 @@ def locals TEMPLATES = [ "ext/prism/api_node.c", "include/prism/ast.h", + "include/prism/ast.hpp", "include/prism/internal/diagnostic.h", "javascript/src/deserialize.js", "javascript/src/nodes.js",