Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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

Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/cpp-bindings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -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=
58 changes: 0 additions & 58 deletions cpp/headers.cpp

This file was deleted.

129 changes: 129 additions & 0 deletions cpp/node_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#include "prism_test.hpp"

#include <algorithm>
#include <iterator>
#include <type_traits>
#if __cplusplus >= 202002L
#include <ranges>
#endif

/* Every node struct maps to its node type. */
static_assert(prism::node_type_v<pm_call_node_t> == PM_CALL_NODE);
static_assert(prism::node_type_v<pm_program_node_t> == PM_PROGRAM_NODE);
static_assert(prism::node_type_v<pm_yield_node_t> == PM_YIELD_NODE);

/* Only the node structs declared in ast.h are nodes. */
static_assert(prism::is_node_v<pm_call_node_t>);
static_assert(prism::is_node_v<pm_statements_node_t>);
static_assert(!prism::is_node_v<pm_node_t>);
static_assert(!prism::is_node_v<pm_location_t>);
static_assert(!prism::is_node_v<int>);

/* Casts keep the constness of their argument. */
static_assert(std::is_same_v<decltype(prism::cast<pm_call_node_t>(std::declval<pm_node_t *>())), pm_call_node_t *>);
static_assert(std::is_same_v<decltype(prism::cast<pm_call_node_t>(std::declval<const pm_node_t *>())), const pm_call_node_t *>);
static_assert(std::is_same_v<decltype(prism::dyn_cast<pm_call_node_t>(std::declval<pm_node_t *>())), pm_call_node_t *>);
static_assert(std::is_same_v<decltype(prism::dyn_cast<pm_call_node_t>(std::declval<const pm_node_t *>())), const pm_call_node_t *>);
static_assert(std::is_same_v<decltype(prism::upcast(std::declval<pm_call_node_t *>())), pm_node_t *>);
static_assert(std::is_same_v<decltype(prism::upcast(std::declval<const pm_call_node_t *>())), const pm_node_t *>);

/* None of the casts can throw. */
static_assert(noexcept(prism::isa<pm_call_node_t>(std::declval<const pm_node_t *>())));
static_assert(noexcept(prism::cast<pm_call_node_t>(std::declval<pm_node_t *>())));
static_assert(noexcept(prism::dyn_cast<pm_call_node_t>(std::declval<pm_node_t *>())));
static_assert(noexcept(prism::upcast(std::declval<pm_call_node_t *>())));

/* A node_range is a contiguous range of node pointers. */
static_assert(std::is_same_v<decltype(*std::declval<prism::node_range>().begin()), pm_node_t *const &>);
#if __cplusplus >= 202002L
static_assert(std::ranges::contiguous_range<prism::node_range>);
static_assert(std::ranges::sized_range<prism::node_range>);
#endif

PRISM_TEST(isa_matches_only_the_node_type) {
prism_test::parse_result result("foo; 1");

CHECK(prism::isa<pm_program_node_t>(result.root));
CHECK(prism::isa<pm_call_node_t>(result.statement(0)));
CHECK(!prism::isa<pm_integer_node_t>(result.statement(0)));
CHECK(prism::isa<pm_integer_node_t>(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<pm_call_node_t>(node);
const pm_call_node_t *const_call = prism::cast<pm_call_node_t>(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<pm_call_node_t>(node) == prism::cast<pm_call_node_t>(node));
CHECK(prism::dyn_cast<pm_call_node_t>(const_node) == prism::cast<pm_call_node_t>(const_node));
CHECK(prism::dyn_cast<pm_integer_node_t>(node) == nullptr);
CHECK(prism::dyn_cast<pm_integer_node_t>(const_node) == nullptr);
CHECK(prism::dyn_cast<pm_call_node_t>(static_cast<pm_node_t *>(nullptr)) == nullptr);
CHECK(prism::dyn_cast<pm_call_node_t>(static_cast<const pm_node_t *>(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<pm_call_node_t>(result.statement(0));
const pm_call_node_t *without_receiver = prism::cast<pm_call_node_t>(result.statement(1));

CHECK(prism::dyn_cast<pm_call_node_t>(with_receiver->receiver) != nullptr);
CHECK(prism::dyn_cast<pm_call_node_t>(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<pm_integer_node_t>) == 2);
CHECK(std::distance(statements.begin(), statements.end()) == 5);

auto found = std::find_if(statements.begin(), statements.end(), prism::isa<pm_integer_node_t>);
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<pm_call_node_t>(result.statement(0));
prism::node_range arguments(call->arguments->arguments);

CHECK(arguments.size() == 3);
CHECK(std::all_of(arguments.begin(), arguments.end(), prism::isa<pm_integer_node_t>));
}
88 changes: 88 additions & 0 deletions cpp/ownership_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "prism_test.hpp"

#include <string_view>
#include <type_traits>
#include <utility>

/* 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<prism::arena_deleter, pm_arena_t *>);
static_assert(std::is_nothrow_invocable_v<prism::parser_deleter, pm_parser_t *>);
static_assert(std::is_nothrow_invocable_v<prism::options_deleter, pm_options_t *>);
static_assert(std::is_nothrow_invocable_v<prism::buffer_deleter, pm_buffer_t *>);

/* 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<prism::unique_parser>);
static_assert(!std::is_copy_constructible_v<prism::unique_parser>);

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<pm_program_node_t>(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()));
}
Loading
Loading