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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ jobs:
chmod u+x ./lib/modules/.github/run-tests
(./lib/modules/.github/run-tests src)

- uses: actions/setup-python@v5
if: ${{ ! startsWith(matrix.config.name, 'Windows') }}
with:
python-version: '3.13'

- name: REPL tests
if: ${{ ! startsWith(matrix.config.name, 'Windows') }}
shell: bash
run: |
pip install -r tests/repl/requirements.txt
python3 tests/repl/test.py

fuzzing:
runs-on: ubuntu-24.04
name: Fuzz testing
Expand Down
14 changes: 9 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,22 @@ if (ARK_BUILD_MODULES)
set_directory_properties(PROPERTIES COMPILE_OPTIONS "${old_dir_compile_options}")
endif ()

if (ARK_TESTS OR ARK_BUILD_EXE)
add_subdirectory("${ark_SOURCE_DIR}/lib/replxx" EXCLUDE_FROM_ALL)
endif ()

if (ARK_TESTS)
file(GLOB_RECURSE UT_SOURCES
file(GLOB_RECURSE SOURCES
${ark_SOURCE_DIR}/tests/unittests/*.cpp
${ark_SOURCE_DIR}/lib/fmt/src/format.cc
${ark_SOURCE_DIR}/src/arkscript/Formatter.cpp
${ark_SOURCE_DIR}/src/arkscript/JsonCompiler.cpp)
add_executable(unittests ${UT_SOURCES})
${ark_SOURCE_DIR}/src/arkscript/JsonCompiler.cpp
${ark_SOURCE_DIR}/src/arkscript/REPL/Utils.cpp)
add_executable(unittests ${SOURCES})

add_subdirectory(${ark_SOURCE_DIR}/lib/ut)
target_include_directories(unittests PUBLIC ${ark_SOURCE_DIR}/include)
target_link_libraries(unittests PUBLIC ArkReactor ut)
target_link_libraries(unittests PUBLIC ArkReactor ut replxx)

add_compile_definitions(BOOST_UT_DISABLE_MODULE)
target_compile_features(unittests PRIVATE cxx_std_20)
Expand Down Expand Up @@ -233,7 +238,6 @@ if (ARK_BUILD_EXE)
${ark_SOURCE_DIR}/lib/fmt/src/format.cc)
add_executable(arkscript ${EXE_SOURCES})

add_subdirectory("${ark_SOURCE_DIR}/lib/replxx" EXCLUDE_FROM_ALL)
add_subdirectory("${ark_SOURCE_DIR}/lib/clipp" EXCLUDE_FROM_ALL)

target_include_directories(arkscript SYSTEM PUBLIC "${ark_SOURCE_DIR}/lib/clipp/include")
Expand Down
13 changes: 13 additions & 0 deletions include/CLI/REPL/Utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ namespace Ark::internal
*/
void trimWhitespace(std::string& line);

/**
* @brief Compute a list of all the language keywords and builtins
*
* @return std::vector<std::string>
*/
std::vector<std::string> getAllKeywords();

/**
* @brief Compute a list of pairs (word -> color) to be used for coloration by the REPL
* @return std::vector<std::pair<std::string, replxx::Replxx::Color>>
*/
std::vector<std::pair<std::string, replxx::Replxx::Color>> getColorPerKeyword();

replxx::Replxx::completions_t hookCompletion(const std::vector<std::string>& words, const std::string& context, int& length);

void hookColor(const std::vector<std::pair<std::string, replxx::Replxx::Color>>& words_colors, const std::string& context, replxx::Replxx::colors_t& colors);
Expand Down
36 changes: 3 additions & 33 deletions src/arkscript/REPL/Repl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#include <ranges>

#include <Ark/Builtins/Builtins.hpp>
#include <Ark/Compiler/Common.hpp>

#include <CLI/REPL/Repl.hpp>
#include <CLI/REPL/Utils.hpp>
Expand All @@ -19,37 +18,8 @@ namespace Ark
m_old_ip(0), m_lib_env(lib_env),
m_state(m_lib_env), m_vm(m_state), m_has_init_vm(false)
{
m_keywords.reserve(keywords.size() + Language::listInstructions.size() + Language::operators.size() + Builtins::builtins.size() + 2);
for (auto keyword : keywords)
m_keywords.emplace_back(keyword);
for (auto inst : Language::listInstructions)
m_keywords.emplace_back(inst);
for (auto op : Language::operators)
m_keywords.emplace_back(op);
for (const auto& builtin : std::ranges::views::keys(Builtins::builtins))
m_keywords.push_back(builtin);
m_keywords.emplace_back("and");
m_keywords.emplace_back("or");

m_words_colors.reserve(keywords.size() + Language::listInstructions.size() + Language::operators.size() + Builtins::builtins.size() + 4);
for (auto keyword : keywords)
m_words_colors.emplace_back(keyword, Replxx::Color::BRIGHTRED);
for (auto inst : Language::listInstructions)
m_words_colors.emplace_back(inst, Replxx::Color::GREEN);
for (auto op : Language::operators)
{
auto safe_op = std::string(op);
if (const auto it = safe_op.find_first_of(R"(-+=/*<>[]()?")"); it != std::string::npos)
safe_op.insert(it, "\\");
m_words_colors.emplace_back(safe_op, Replxx::Color::BRIGHTBLUE);
}
for (const auto& builtin : std::ranges::views::keys(Builtins::builtins))
m_words_colors.emplace_back(builtin, Replxx::Color::GREEN);

m_words_colors.emplace_back("and", Replxx::Color::BRIGHTBLUE);
m_words_colors.emplace_back("or", Replxx::Color::BRIGHTBLUE);
m_words_colors.emplace_back("[\\-|+]?[0-9]+(\\.[0-9]+)?", Replxx::Color::YELLOW);
m_words_colors.emplace_back("\".*\"", Replxx::Color::MAGENTA);
m_keywords = getAllKeywords();
m_words_colors = getColorPerKeyword();
}

int Repl::run()
Expand All @@ -59,7 +29,7 @@ namespace Ark

while (m_running)
{
auto maybe_block = getCodeBlock();
std::optional<std::string> maybe_block = getCodeBlock();

// save a valid ip if execution failed
m_old_ip = m_vm.m_execution_contexts[0]->ip;
Expand Down
69 changes: 60 additions & 9 deletions src/arkscript/REPL/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

#include <regex>
#include <algorithm>
#include <numeric>
#include <ranges>

#include <Ark/Builtins/Builtins.hpp>
#include <Ark/Compiler/Common.hpp>

namespace Ark::internal
{
Expand All @@ -20,27 +25,73 @@ namespace Ark::internal
}
}

std::vector<std::string> getAllKeywords()
{
std::vector<std::string> output;
output.reserve(keywords.size() + Language::listInstructions.size() + Language::operators.size() + Builtins::builtins.size() + 2);
for (auto keyword : keywords)
output.emplace_back(keyword);
for (auto inst : Language::listInstructions)
output.emplace_back(inst);
for (auto op : Language::operators)
output.emplace_back(op);
for (const auto& builtin : std::ranges::views::keys(Builtins::builtins))
output.push_back(builtin);
output.emplace_back("and");
output.emplace_back("or");

return output;
}

std::vector<std::pair<std::string, replxx::Replxx::Color>> getColorPerKeyword()
{
using namespace replxx;

std::vector<std::pair<std::string, Replxx::Color>> output;
output.reserve(keywords.size() + Language::listInstructions.size() + Language::operators.size() + Builtins::builtins.size() + 4);
for (auto keyword : keywords)
output.emplace_back(keyword, Replxx::Color::BRIGHTRED);
for (auto inst : Language::listInstructions)
output.emplace_back(inst, Replxx::Color::GREEN);
for (auto op : Language::operators)
{
auto safe_op = std::string(op);
if (const auto it = safe_op.find_first_of(R"(-+=/*<>[]()?")"); it != std::string::npos)
safe_op.insert(it, "\\");
output.emplace_back(safe_op, Replxx::Color::BRIGHTBLUE);
}
for (const auto& builtin : std::ranges::views::keys(Builtins::builtins))
output.emplace_back(builtin, Replxx::Color::GREEN);

output.emplace_back("and", Replxx::Color::BRIGHTBLUE);
output.emplace_back("or", Replxx::Color::BRIGHTBLUE);
output.emplace_back("[\\-|+]?[0-9]+(\\.[0-9]+)?", Replxx::Color::YELLOW);
output.emplace_back("\".*\"", Replxx::Color::MAGENTA);

return output;
}

std::size_t codepointLength(const std::string& str)
{
std::size_t len = 0;
for (const auto c : str)
len += (c & 0xc0) != 0x80;
return len;
return std::accumulate(
str.begin(),
str.end(),
std::size_t { 0 },
[](const std::size_t acc, const char c) {
return acc + ((c & 0xc0) != 0x80);
});
}

std::size_t contextLen(const std::string& prefix)
{
const std::string word_break = " \t\n\r\v\f=+*&^%$#@!,./?<>;`~'\"[]{}()\\|";
long i = static_cast<long>(prefix.size()) - 1;
std::size_t count = 0;

while (i >= 0)
for (const auto c : std::ranges::views::reverse(prefix))
{
if (word_break.find(prefix[static_cast<std::size_t>(i)]) != std::string::npos)
if (word_break.find(c) != std::string::npos)
break;

++count;
--i;
}

return count;
Expand Down
1 change: 1 addition & 0 deletions tests/repl/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pexpect
53 changes: 53 additions & 0 deletions tests/repl/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pexpect
import sys
import os


def get_repl():
executable = None
for p in ["./arkscript", "cmake-build-debug/arkscript", "build/arkscript", "build/arkscript.exe"]:
if os.path.exists(p):
executable = p
break
if executable is None:
print("Couldn't find a valid path to 'arkscript' executable")
sys.exit(1)

child = pexpect.spawn(executable, timeout=2, encoding="utf-8")
child.logfile = sys.stdout
return child


def test(prompt: str, command: str, expected: str):
try:
repl.expect(prompt)
repl.send(command)
if expected is not None:
repl.expect(expected)
except pexpect.ExceptionPexpect:
print(f"Tried to match '{prompt}{command.strip()}' with '{expected}' but got {repl.after}")
raise


repl = get_repl()

# repl headers
repl.expect(r"ArkScript REPL -- Version [0-9]+\.[0-9]+\.[0-9]+-[a-f0-9]{8} \[LICENSE: Mozilla Public License 2\.0\]")
repl.expect(r"Type \"quit\" to quit\. Try \"help\" for more information")

# completion
test("main:001> ", "(le", "let")
repl.send("t a 5)\r\n")

test("main:002> ", "(print a)\r\n", "5")

test("main:003> ", "(im", "port")
repl.sendcontrol("c")

test("main:003> ", "(let foo (fun (bar) (* bar 7))) (print (foo 7))\r\n", "49")

test("main:004> ", "# just a comment\r\n", "main:005> ")

if repl.isalive():
repl.sendline("quit")
repl.close()
38 changes: 38 additions & 0 deletions tests/unittests/ReplSuite.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include <boost/ut.hpp>

#include <CLI/REPL/Utils.hpp>

using namespace boost;

ut::suite<"Repl"> repl_suite = [] {
using namespace ut;

"countOpenEnclosures"_test = [] {
expect(that % Ark::internal::countOpenEnclosures("", '(', ')') == 0);
expect(that % Ark::internal::countOpenEnclosures("(", '(', ')') == 1);
expect(that % Ark::internal::countOpenEnclosures(")", '(', ')') == -1);
expect(that % Ark::internal::countOpenEnclosures("{}", '(', ')') == 0);
expect(that % Ark::internal::countOpenEnclosures("{)(()}", '(', ')') == 0);
expect(that % Ark::internal::countOpenEnclosures("{)(()}", '{', '}') == 0);
};

"trimWhitespace"_test = [] {
const std::string expected = "hello world";
std::string line = expected;

Ark::internal::trimWhitespace(line);
expect(that % line == expected);

line = " \thello world";
Ark::internal::trimWhitespace(line);
expect(that % line == expected);

line = "hello world \t";
Ark::internal::trimWhitespace(line);
expect(that % line == expected);

line = " \thello world \t";
Ark::internal::trimWhitespace(line);
expect(that % line == expected);
};
};
Loading