diff --git a/CMakeLists.txt b/CMakeLists.txt index 12ddd56c..74f7e614 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,13 +4,6 @@ if(NOT ${CMAKE_VERSION} VERSION_LESS "3.1") cmake_policy(SET CMP0054 NEW) endif() -IF(BIICODE) - INIT_BIICODE_BLOCK() - ADD_BIICODE_TARGETS() -ELSE() - # Your regular CMakeLists configuration here - - project(chaiscript) option(MULTITHREAD_SUPPORT_ENABLED "Multithreaded Support Enabled" TRUE) @@ -503,5 +496,3 @@ install(FILES "${chaiscript_BINARY_DIR}/lib/pkgconfig/chaiscript.pc" DESTINATION lib/pkgconfig) -ENDIF() - diff --git a/cheatsheet.md b/cheatsheet.md index 266ddc71..907fe329 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -370,6 +370,25 @@ if (expression) { } if (statement; expression) { } ``` +## Switch Statements + +``` chaiscript +var myvalue = 2 +switch (myvalue) { + case (1) { + print("My Value is 1"); + break; + } + case (2) { + print("My Value is 2"); + break; + } + default { + print("My Value is something else."; + } +} +``` + ## Built in Types ``` diff --git a/cmake/Catch.cmake b/cmake/Catch.cmake new file mode 100644 index 00000000..486e3233 --- /dev/null +++ b/cmake/Catch.cmake @@ -0,0 +1,175 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +Catch +----- + +This module defines a function to help use the Catch test framework. + +The :command:`catch_discover_tests` discovers tests by asking the compiled test +executable to enumerate its tests. This does not require CMake to be re-run +when tests change. However, it may not work in a cross-compiling environment, +and setting test properties is less convenient. + +This command is intended to replace use of :command:`add_test` to register +tests, and will create a separate CTest test for each Catch test case. Note +that this is in some cases less efficient, as common set-up and tear-down logic +cannot be shared by multiple test cases executing in the same instance. +However, it provides more fine-grained pass/fail information to CTest, which is +usually considered as more beneficial. By default, the CTest test name is the +same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. + +.. command:: catch_discover_tests + + Automatically add tests with CTest by querying the compiled test executable + for available tests:: + + catch_discover_tests(target + [TEST_SPEC arg1...] + [EXTRA_ARGS arg1...] + [WORKING_DIRECTORY dir] + [TEST_PREFIX prefix] + [TEST_SUFFIX suffix] + [PROPERTIES name1 value1...] + [TEST_LIST var] + ) + + ``catch_discover_tests`` sets up a post-build command on the test executable + that generates the list of tests by parsing the output from running the test + with the ``--list-test-names-only`` argument. This ensures that the full + list of tests is obtained. Since test discovery occurs at build time, it is + not necessary to re-run CMake when the list of tests changes. + However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set + in order to function in a cross-compiling environment. + + Additionally, setting properties on tests is somewhat less convenient, since + the tests are not available at CMake time. Additional test properties may be + assigned to the set of tests as a whole using the ``PROPERTIES`` option. If + more fine-grained test control is needed, custom content may be provided + through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES` + directory property. The set of discovered tests is made accessible to such a + script via the ``_TESTS`` variable. + + The options are: + + ``target`` + Specifies the Catch executable, which must be a known CMake executable + target. CMake will substitute the location of the built executable when + running the test. + + ``TEST_SPEC arg1...`` + Specifies test cases, wildcarded test cases, tags and tag expressions to + pass to the Catch executable with the ``--list-test-names-only`` argument. + + ``EXTRA_ARGS arg1...`` + Any extra arguments to pass on the command line to each test case. + + ``WORKING_DIRECTORY dir`` + Specifies the directory in which to run the discovered test cases. If this + option is not provided, the current binary directory is used. + + ``TEST_PREFIX prefix`` + Specifies a ``prefix`` to be prepended to the name of each discovered test + case. This can be useful when the same test executable is being used in + multiple calls to ``catch_discover_tests()`` but with different + ``TEST_SPEC`` or ``EXTRA_ARGS``. + + ``TEST_SUFFIX suffix`` + Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of + every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may + be specified. + + ``PROPERTIES name1 value1...`` + Specifies additional properties to be set on all tests discovered by this + invocation of ``catch_discover_tests``. + + ``TEST_LIST var`` + Make the list of tests available in the variable ``var``, rather than the + default ``_TESTS``. This can be useful when the same test + executable is being used in multiple calls to ``catch_discover_tests()``. + Note that this variable is only available in CTest. + +#]=======================================================================] + +#------------------------------------------------------------------------------ +function(catch_discover_tests TARGET) + cmake_parse_arguments( + "" + "" + "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST" + "TEST_SPEC;EXTRA_ARGS;PROPERTIES" + ${ARGN} + ) + + if(NOT _WORKING_DIRECTORY) + set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + endif() + if(NOT _TEST_LIST) + set(_TEST_LIST ${TARGET}_TESTS) + endif() + + ## Generate a unique name based on the extra arguments + string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS}") + string(SUBSTRING ${args_hash} 0 7 args_hash) + + # Define rule to generate test list for aforementioned test executable + set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake") + set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake") + get_property(crosscompiling_emulator + TARGET ${TARGET} + PROPERTY CROSSCOMPILING_EMULATOR + ) + add_custom_command( + TARGET ${TARGET} POST_BUILD + BYPRODUCTS "${ctest_tests_file}" + COMMAND "${CMAKE_COMMAND}" + -D "TEST_TARGET=${TARGET}" + -D "TEST_EXECUTABLE=$" + -D "TEST_EXECUTOR=${crosscompiling_emulator}" + -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" + -D "TEST_SPEC=${_TEST_SPEC}" + -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" + -D "TEST_PROPERTIES=${_PROPERTIES}" + -D "TEST_PREFIX=${_TEST_PREFIX}" + -D "TEST_SUFFIX=${_TEST_SUFFIX}" + -D "TEST_LIST=${_TEST_LIST}" + -D "CTEST_FILE=${ctest_tests_file}" + -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" + VERBATIM + ) + + file(WRITE "${ctest_include_file}" + "if(EXISTS \"${ctest_tests_file}\")\n" + " include(\"${ctest_tests_file}\")\n" + "else()\n" + " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" + "endif()\n" + ) + + if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0") + # Add discovered tests to directory TEST_INCLUDE_FILES + set_property(DIRECTORY + APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) + else() + # Add discovered tests as directory TEST_INCLUDE_FILE if possible + get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) + if (NOT ${test_include_file_set}) + set_property(DIRECTORY + PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" + ) + else() + message(FATAL_ERROR + "Cannot set more than one TEST_INCLUDE_FILE" + ) + endif() + endif() + +endfunction() + +############################################################################### + +set(_CATCH_DISCOVER_TESTS_SCRIPT + ${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake +) diff --git a/cmake/CatchAddTests.cmake b/cmake/CatchAddTests.cmake new file mode 100644 index 00000000..c68921e4 --- /dev/null +++ b/cmake/CatchAddTests.cmake @@ -0,0 +1,77 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +set(prefix "${TEST_PREFIX}") +set(suffix "${TEST_SUFFIX}") +set(spec ${TEST_SPEC}) +set(extra_args ${TEST_EXTRA_ARGS}) +set(properties ${TEST_PROPERTIES}) +set(script) +set(suite) +set(tests) + +function(add_command NAME) + set(_args "") + foreach(_arg ${ARGN}) + if(_arg MATCHES "[^-./:a-zA-Z0-9_]") + set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument + else() + set(_args "${_args} ${_arg}") + endif() + endforeach() + set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) +endfunction() + +# Run test executable to get list of available tests +if(NOT EXISTS "${TEST_EXECUTABLE}") + message(FATAL_ERROR + "Specified test executable '${TEST_EXECUTABLE}' does not exist" + ) +endif() +execute_process( + COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-test-names-only + OUTPUT_VARIABLE output + RESULT_VARIABLE result +) +# Catch --list-test-names-only reports the number of tests, so 0 is... surprising +if(${result} EQUAL 0) + message(WARNING + "Test executable '${TEST_EXECUTABLE}' contains no tests!\n" + ) +elseif(${result} LESS 0) + message(FATAL_ERROR + "Error running test executable '${TEST_EXECUTABLE}':\n" + " Result: ${result}\n" + " Output: ${output}\n" + ) +endif() + +string(REPLACE "\n" ";" output "${output}") + +# Parse output +foreach(line ${output}) + # Test name; strip spaces to get just the name... + string(REGEX REPLACE " +" "" test "${line}") + # ...and add to script + add_command(add_test + "${prefix}${test}${suffix}" + ${TEST_EXECUTOR} + "${TEST_EXECUTABLE}" + "${test}" + ${extra_args} + ) + add_command(set_tests_properties + "${prefix}${test}${suffix}" + PROPERTIES + WORKING_DIRECTORY "${TEST_WORKING_DIR}" + ${properties} + ) + list(APPEND tests "${prefix}${test}${suffix}") +endforeach() + +# Create a list of all discovered tests, which users may use to e.g. set +# properties on the tests +add_command(set ${TEST_LIST} ${tests}) + +# Write CTest script +file(WRITE "${CTEST_FILE}" "${script}") diff --git a/cmake/ParseAndAddCatchTests.cmake b/cmake/ParseAndAddCatchTests.cmake new file mode 100644 index 00000000..cb2846d0 --- /dev/null +++ b/cmake/ParseAndAddCatchTests.cmake @@ -0,0 +1,185 @@ +#==================================================================================================# +# supported macros # +# - TEST_CASE, # +# - SCENARIO, # +# - TEST_CASE_METHOD, # +# - CATCH_TEST_CASE, # +# - CATCH_SCENARIO, # +# - CATCH_TEST_CASE_METHOD. # +# # +# Usage # +# 1. make sure this module is in the path or add this otherwise: # +# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # +# 2. make sure that you've enabled testing option for the project by the call: # +# enable_testing() # +# 3. add the lines to the script for testing target (sample CMakeLists.txt): # +# project(testing_target) # +# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # +# enable_testing() # +# # +# find_path(CATCH_INCLUDE_DIR "catch.hpp") # +# include_directories(${INCLUDE_DIRECTORIES} ${CATCH_INCLUDE_DIR}) # +# # +# file(GLOB SOURCE_FILES "*.cpp") # +# add_executable(${PROJECT_NAME} ${SOURCE_FILES}) # +# # +# include(ParseAndAddCatchTests) # +# ParseAndAddCatchTests(${PROJECT_NAME}) # +# # +# The following variables affect the behavior of the script: # +# # +# PARSE_CATCH_TESTS_VERBOSE (Default OFF) # +# -- enables debug messages # +# PARSE_CATCH_TESTS_NO_HIDDEN_TESTS (Default OFF) # +# -- excludes tests marked with [!hide], [.] or [.foo] tags # +# PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME (Default ON) # +# -- adds fixture class name to the test name # +# PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME (Default ON) # +# -- adds cmake target name to the test name # +# PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS (Default OFF) # +# -- causes CMake to rerun when file with tests changes so that new tests will be discovered # +# # +#==================================================================================================# + +cmake_minimum_required(VERSION 2.8.8) + +option(PARSE_CATCH_TESTS_VERBOSE "Print Catch to CTest parser debug messages" OFF) +option(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS "Exclude tests with [!hide], [.] or [.foo] tags" OFF) +option(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME "Add fixture class name to the test name" ON) +option(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME "Add target name to the test name" ON) +option(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS "Add test file to CMAKE_CONFIGURE_DEPENDS property" OFF) + +function(PrintDebugMessage) + if(PARSE_CATCH_TESTS_VERBOSE) + message(STATUS "ParseAndAddCatchTests: ${ARGV}") + endif() +endfunction() + +# This removes the contents between +# - block comments (i.e. /* ... */) +# - full line comments (i.e. // ... ) +# contents have been read into '${CppCode}'. +# !keep partial line comments +function(RemoveComments CppCode) + string(ASCII 2 CMakeBeginBlockComment) + string(ASCII 3 CMakeEndBlockComment) + string(REGEX REPLACE "/\\*" "${CMakeBeginBlockComment}" ${CppCode} "${${CppCode}}") + string(REGEX REPLACE "\\*/" "${CMakeEndBlockComment}" ${CppCode} "${${CppCode}}") + string(REGEX REPLACE "${CMakeBeginBlockComment}[^${CMakeEndBlockComment}]*${CMakeEndBlockComment}" "" ${CppCode} "${${CppCode}}") + string(REGEX REPLACE "\n[ \t]*//+[^\n]+" "\n" ${CppCode} "${${CppCode}}") + + set(${CppCode} "${${CppCode}}" PARENT_SCOPE) +endfunction() + +# Worker function +function(ParseFile SourceFile TestTarget) + # According to CMake docs EXISTS behavior is well-defined only for full paths. + get_filename_component(SourceFile ${SourceFile} ABSOLUTE) + if(NOT EXISTS ${SourceFile}) + message(WARNING "Cannot find source file: ${SourceFile}") + return() + endif() + PrintDebugMessage("parsing ${SourceFile}") + file(STRINGS ${SourceFile} Contents NEWLINE_CONSUME) + + # Remove block and fullline comments + RemoveComments(Contents) + + # Find definition of test names + string(REGEX MATCHALL "[ \t]*(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^\)]+\\)+[ \t\n]*{+[ \t]*(//[^\n]*[Tt][Ii][Mm][Ee][Oo][Uu][Tt][ \t]*[0-9]+)*" Tests "${Contents}") + + if(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS AND Tests) + PrintDebugMessage("Adding ${SourceFile} to CMAKE_CONFIGURE_DEPENDS property") + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS ${SourceFile} + ) + endif() + + foreach(TestName ${Tests}) + # Strip newlines + string(REGEX REPLACE "\\\\\n|\n" "" TestName "${TestName}") + + # Get test type and fixture if applicable + string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^,^\"]*" TestTypeAndFixture "${TestName}") + string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)" TestType "${TestTypeAndFixture}") + string(REPLACE "${TestType}(" "" TestFixture "${TestTypeAndFixture}") + + # Get string parts of test definition + string(REGEX MATCHALL "\"+([^\\^\"]|\\\\\")+\"+" TestStrings "${TestName}") + + # Strip wrapping quotation marks + string(REGEX REPLACE "^\"(.*)\"$" "\\1" TestStrings "${TestStrings}") + string(REPLACE "\";\"" ";" TestStrings "${TestStrings}") + + # Validate that a test name and tags have been provided + list(LENGTH TestStrings TestStringsLength) + if(TestStringsLength GREATER 2 OR TestStringsLength LESS 1) + message(FATAL_ERROR "You must provide a valid test name and tags for all tests in ${SourceFile}") + endif() + + # Assign name and tags + list(GET TestStrings 0 Name) + if("${TestType}" STREQUAL "SCENARIO") + set(Name "Scenario: ${Name}") + endif() + if(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME AND TestFixture) + set(CTestName "${TestFixture}:${Name}") + else() + set(CTestName "${Name}") + endif() + if(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME) + set(CTestName "${TestTarget}:${CTestName}") + endif() + # add target to labels to enable running all tests added from this target + set(Labels ${TestTarget}) + if(TestStringsLength EQUAL 2) + list(GET TestStrings 1 Tags) + string(TOLOWER "${Tags}" Tags) + # remove target from labels if the test is hidden + if("${Tags}" MATCHES ".*\\[!?(hide|\\.)\\].*") + list(REMOVE_ITEM Labels ${TestTarget}) + endif() + string(REPLACE "]" ";" Tags "${Tags}") + string(REPLACE "[" "" Tags "${Tags}") + endif() + + list(APPEND Labels ${Tags}) + + list(FIND Labels "!hide" IndexOfHideLabel) + set(HiddenTagFound OFF) + foreach(label ${Labels}) + string(REGEX MATCH "^!hide|^\\." result ${label}) + if(result) + set(HiddenTagFound ON) + break() + endif(result) + endforeach(label) + if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound}) + PrintDebugMessage("Skipping test \"${CTestName}\" as it has [!hide], [.] or [.foo] label") + else() + PrintDebugMessage("Adding test \"${CTestName}\"") + if(Labels) + PrintDebugMessage("Setting labels to ${Labels}") + endif() + + # Add the test and set its properties + add_test(NAME "\"${CTestName}\"" COMMAND ${TestTarget} ${Name} ${AdditionalCatchParameters}) + set_tests_properties("\"${CTestName}\"" PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran" + LABELS "${Labels}") + endif() + + endforeach() +endfunction() + +# entry point +function(ParseAndAddCatchTests TestTarget) + PrintDebugMessage("Started parsing ${TestTarget}") + get_target_property(SourceFiles ${TestTarget} SOURCES) + PrintDebugMessage("Found the following sources: ${SourceFiles}") + foreach(SourceFile ${SourceFiles}) + ParseFile(${SourceFile} ${TestTarget}) + endforeach() + PrintDebugMessage("Finished parsing ${TestTarget}") +endfunction() diff --git a/include/chaiscript/dispatchkit/bootstrap.hpp b/include/chaiscript/dispatchkit/bootstrap.hpp index bcaebfca..671a723a 100644 --- a/include/chaiscript/dispatchkit/bootstrap.hpp +++ b/include/chaiscript/dispatchkit/bootstrap.hpp @@ -23,10 +23,10 @@ namespace chaiscript void array(const std::string &type, Module& m) { typedef typename std::remove_extent::type ReturnType; - const auto extent = std::extent::value; m.add(user_type(), type); m.add(fun( - [extent](T& t, size_t index)->ReturnType &{ + [](T& t, size_t index)->ReturnType &{ + constexpr auto extent = std::extent::value; if (extent > 0 && index >= extent) { throw std::range_error("Array index out of range. Received: " + std::to_string(index) + " expected < " + std::to_string(extent)); } else { @@ -37,7 +37,8 @@ namespace chaiscript ); m.add(fun( - [extent](const T &t, size_t index)->const ReturnType &{ + [](const T &t, size_t index)->const ReturnType &{ + constexpr auto extent = std::extent::value; if (extent > 0 && index >= extent) { throw std::range_error("Array index out of range. Received: " + std::to_string(index) + " expected < " + std::to_string(extent)); } else { @@ -48,7 +49,8 @@ namespace chaiscript ); m.add(fun( - [extent](const T &) { + [](const T &) { + constexpr auto extent = std::extent::value; return extent; }), "size"); } diff --git a/include/chaiscript/dispatchkit/dispatchkit.hpp b/include/chaiscript/dispatchkit/dispatchkit.hpp index ae729819..05f02535 100644 --- a/include/chaiscript/dispatchkit/dispatchkit.hpp +++ b/include/chaiscript/dispatchkit/dispatchkit.hpp @@ -731,7 +731,7 @@ namespace chaiscript } if (t_throw) { - throw std::range_error("Type Not Known"); + throw std::range_error("Type Not Known: " + name); } else { return Type_Info(); } @@ -767,8 +767,8 @@ namespace chaiscript { uint_fast32_t method_missing_loc = m_method_missing_loc; auto method_missing_funs = get_function("method_missing", method_missing_loc); - if (method_missing_funs.first != method_missing_loc) { - m_method_missing_loc = uint_fast32_t(method_missing_funs.first); + if (method_missing_funs.first != method_missing_loc) { + m_method_missing_loc = uint_fast32_t(method_missing_funs.first); } return std::move(method_missing_funs.second); @@ -879,7 +879,7 @@ namespace chaiscript for (auto itr = stack.rbegin(); itr != stack.rend(); ++itr) { retval.insert(itr->begin(), itr->end()); - } + } // add the global values chaiscript::detail::threading::shared_lock l(m_mutex); @@ -963,7 +963,7 @@ namespace chaiscript const auto funs = get_function(t_name, loc); if (funs.first != loc) { t_loc = uint_fast32_t(funs.first); } - const auto do_attribute_call = + const auto do_attribute_call = [this](int l_num_params, const std::vector &l_params, const std::vector &l_funs, const Type_Conversions_State &l_conversions)->Boxed_Value { std::vector attr_params{l_params.begin(), l_params.begin() + l_num_params}; @@ -992,11 +992,11 @@ namespace chaiscript } catch (const chaiscript::exception::arity_error &) { } catch (const chaiscript::exception::guard_error &) { } - throw chaiscript::exception::dispatch_error({l_params.begin() + l_num_params, l_params.end()}, + throw chaiscript::exception::dispatch_error({l_params.begin() + l_num_params, l_params.end()}, std::vector{boxed_cast(bv)}); } catch (const chaiscript::exception::bad_boxed_cast &) { // unable to convert bv into a Proxy_Function_Base - throw chaiscript::exception::dispatch_error({l_params.begin() + l_num_params, l_params.end()}, + throw chaiscript::exception::dispatch_error({l_params.begin() + l_num_params, l_params.end()}, std::vector(l_funs.begin(), l_funs.end())); } } else { @@ -1056,7 +1056,7 @@ namespace chaiscript return dispatch::dispatch(functions, {params[0], var(t_name), var(std::vector(params.begin()+1, params.end()))}, t_conversions); } } catch (const dispatch::option_explicit_set &e) { - throw chaiscript::exception::dispatch_error(params, std::vector(funs.second->begin(), funs.second->end()), + throw chaiscript::exception::dispatch_error(params, std::vector(funs.second->begin(), funs.second->end()), e.what()); } } @@ -1147,7 +1147,7 @@ namespace chaiscript std::cout << type.first << ": " << type.second.bare_name() << '\n'; } - std::cout << '\n'; + std::cout << '\n'; std::cout << "Functions: \n"; for (auto const &func: get_functions()) @@ -1298,7 +1298,7 @@ namespace chaiscript return m_state.m_boxed_functions; } - std::vector> &get_boxed_functions_int() + std::vector> &get_boxed_functions_int() { return m_state.m_boxed_functions; } @@ -1308,7 +1308,7 @@ namespace chaiscript return m_state.m_function_objects; } - std::vector> &get_function_objects_int() + std::vector> &get_function_objects_int() { return m_state.m_function_objects; } @@ -1318,7 +1318,7 @@ namespace chaiscript return m_state.m_functions; } - std::vector>>> &get_functions_int() + std::vector>>> &get_functions_int() { return m_state.m_functions; } @@ -1426,7 +1426,7 @@ namespace chaiscript template static typename Container::iterator find_keyed_value(Container &t_c, const Key &t_key) { - return std::find_if(t_c.begin(), t_c.end(), + return std::find_if(t_c.begin(), t_c.end(), [&t_key](const typename Container::value_type &o) { return o.first == t_key; }); diff --git a/include/chaiscript/dispatchkit/register_function.hpp b/include/chaiscript/dispatchkit/register_function.hpp index e5dec699..2e56fe0e 100644 --- a/include/chaiscript/dispatchkit/register_function.hpp +++ b/include/chaiscript/dispatchkit/register_function.hpp @@ -86,7 +86,7 @@ namespace chaiscript // only compile this bit if noexcept is part of the type system // -#if __cpp_noexcept_function_type >= 201510 || (defined(_NOEXCEPT_TYPES_SUPPORTED) && _MSC_VER >= 1912) +#if (defined(__cpp_noexcept_function_type) && __cpp_noexcept_function_type >= 201510) || (defined(_NOEXCEPT_TYPES_SUPPORTED) && _MSC_VER >= 1912) template Proxy_Function fun(Ret (*func)(Param...) noexcept) { diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index 8f0c0aa1..fff85873 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -223,7 +223,7 @@ namespace chaiscript return std::string(); } else { std::vector v(static_cast(size)); - infile.read(&v[0], size); + infile.read(&v[0], static_cast(size)); return std::string(v.begin(), v.end()); } } diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index c82aff38..649f6f21 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -1038,7 +1038,7 @@ namespace chaiscript bool saw_interpolation_marker = false; bool is_octal = false; bool is_hex = false; - bool is_unicode = false; + std::size_t unicode_size = 0; const bool interpolation_allowed; string_type octal_matches; @@ -1062,12 +1062,12 @@ namespace chaiscript process_hex(); } - if (is_unicode) { + if (unicode_size > 0) { process_unicode(); } } catch (const std::invalid_argument &) { - // escape sequence was invalid somehow, we'll pick this - // up in the next part of parsing + } catch (const exception::eval_error &) { + // Something happened with parsing, we'll catch it later? } } @@ -1097,13 +1097,43 @@ namespace chaiscript void process_unicode() { - if (!hex_matches.empty()) { - auto val = stoll(hex_matches, nullptr, 16); - hex_matches.clear(); - match += detail::Char_Parser_Helper::str_from_ll(val); - } + const auto ch = static_cast(std::stoi(hex_matches, nullptr, 16)); + const auto match_size = hex_matches.size(); + hex_matches.clear(); is_escaped = false; - is_unicode = false; + const auto u_size = unicode_size; + unicode_size = 0; + + char buf[4]; + if (u_size != match_size) { + throw exception::eval_error("Incomplete unicode escape sequence"); + } + if (u_size == 4 && ch >= 0xD800 && ch <= 0xDFFF) { + throw exception::eval_error("Invalid 16 bit universal character"); + } + + + if (ch < 0x80) { + match += static_cast(ch); + } else if (ch < 0x800) { + buf[0] = static_cast(0xC0 | (ch >> 6)); + buf[1] = static_cast(0x80 | (ch & 0x3F)); + match.append(buf, 2); + } else if (ch < 0x10000) { + buf[0] = static_cast(0xE0 | (ch >> 12)); + buf[1] = static_cast(0x80 | ((ch >> 6) & 0x3F)); + buf[2] = static_cast(0x80 | (ch & 0x3F)); + match.append(buf, 3); + } else if (ch < 0x200000) { + buf[0] = static_cast(0xF0 | (ch >> 18)); + buf[1] = static_cast(0x80 | ((ch >> 12) & 0x3F)); + buf[2] = static_cast(0x80 | ((ch >> 6) & 0x3F)); + buf[3] = static_cast(0x80 | (ch & 0x3F)); + match.append(buf, 4); + } else { + // this must be an invalid escape sequence? + throw exception::eval_error("Invalid 32 bit universal character"); + } } void parse(const char_type t_char, const int line, const int col, const std::string &filename) { @@ -1139,16 +1169,16 @@ namespace chaiscript } else { process_hex(); } - } else if (is_unicode) { + } else if (unicode_size > 0) { if (is_hex_char) { hex_matches.push_back(t_char); - if(hex_matches.size() == 4) { - // Format is specified to be 'slash'uABCD - // on collecting from A to D do parsing - process_unicode(); - } - return; + if(hex_matches.size() == unicode_size) { + // Format is specified to be 'slash'uABCD + // on collecting from A to D do parsing + process_unicode(); + } + return; } else { // Not a unicode anymore, try parsing any way // May be someone used 'slash'uAA only @@ -1171,7 +1201,9 @@ namespace chaiscript } else if (t_char == 'x') { is_hex = true; } else if (t_char == 'u') { - is_unicode = true; + unicode_size = 4; + } else if (t_char == 'U') { + unicode_size = 8; } else { switch (t_char) { case ('\'') : match.push_back('\''); break; diff --git a/include/chaiscript/utility/json.hpp b/include/chaiscript/utility/json.hpp index d7c632c6..c6988d54 100644 --- a/include/chaiscript/utility/json.hpp +++ b/include/chaiscript/utility/json.hpp @@ -392,7 +392,7 @@ class JSON bool skip = true; for( auto &p : *internal.Map ) { if( !skip ) { s += ",\n"; } - s += ( pad + "\"" + p.first + "\" : " + p.second.dump( depth + 1, tab ) ); + s += ( pad + "\"" + json_escape(p.first) + "\" : " + p.second.dump( depth + 1, tab ) ); skip = false; } s += ( "\n" + pad.erase( 0, 2 ) + "}" ) ; diff --git a/include/chaiscript/utility/json_wrap.hpp b/include/chaiscript/utility/json_wrap.hpp index 05b9e404..656bf21d 100644 --- a/include/chaiscript/utility/json_wrap.hpp +++ b/include/chaiscript/utility/json_wrap.hpp @@ -110,7 +110,7 @@ namespace chaiscript { return json::JSON(bn.get_as()); } else { - return json::JSON(bn.get_as()); + return json::JSON(bn.get_as()); } } catch (const chaiscript::detail::exception::bad_any_cast &) { // not a number diff --git a/src/test_module.cpp b/src/test_module.cpp index fcb255e5..90a1154a 100644 --- a/src/test_module.cpp +++ b/src/test_module.cpp @@ -78,7 +78,7 @@ int to_int(TestEnum t) class TestDerivedType : public TestBaseType { public: - virtual ~TestDerivedType() {} + ~TestDerivedType() override {} TestDerivedType(const TestDerivedType &) = default; TestDerivedType() = default; virtual int func() override { return 1; } diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index a39c3f8d..3df1dbe2 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -4,7 +4,7 @@ #ifdef _MSC_VER #pragma warning(push) -#pragma warning(disable : 4062 4242 4640 4702 6330 28251) +#pragma warning(disable : 4062 4242 4566 4640 4702 6330 28251) #endif @@ -1271,6 +1271,16 @@ TEST_CASE("Test reference member being registered") CHECK(d == Approx(2.3)); } +TEST_CASE("Test unicode matches C++") +{ + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(),create_chaiscript_parser()); + CHECK(u8"\U000000AC" == chai.eval(R"("\U000000AC")")); + CHECK("\xF0\x9F\x8D\x8C" == chai.eval(R"("\xF0\x9F\x8D\x8C")")); + CHECK(u8"\U0001F34C" == chai.eval(R"("\U0001F34C")")); + CHECK(u8"\u2022" == chai.eval(R"("\u2022")")); + +} + const int add_3(const int &i) { diff --git a/unittests/json_15.chai b/unittests/json_15.chai index 7e8ad652..e15ace05 100644 --- a/unittests/json_15.chai +++ b/unittests/json_15.chai @@ -16,3 +16,4 @@ assert_equal(to_json(from_json("null")), "null") assert_equal(from_json(to_json(["a": 5, "b": "stuff"])), ["a": 5, "b": "stuff"]) auto x = [3.5, true, false, "test", [], Vector(), Map()] assert_equal(from_json(to_json(x)), x) +assert_equal(from_json(to_json(["aa\\zz":"aa\\zz"])), ["aa\\zz": "aa\\zz"]) diff --git a/unittests/json_3.chai b/unittests/json_3.chai index 11ce7dcb..1308c492 100644 --- a/unittests/json_3.chai +++ b/unittests/json_3.chai @@ -1,2 +1,3 @@ assert_equal(from_json("100"), 100) assert_equal(from_json("-100"), -100) +assert_equal(to_json(4294967295), "4294967295") diff --git a/unittests/static_chaiscript.cpp b/unittests/static_chaiscript.cpp index beeac2d1..7819d9eb 100644 --- a/unittests/static_chaiscript.cpp +++ b/unittests/static_chaiscript.cpp @@ -1,5 +1,7 @@ +#ifndef CHAISCRIPT_NO_THREADS #define CHAISCRIPT_NO_THREADS +#endif /// ChaiScript as a static is unsupported with thread support enabled /// diff --git a/unittests/string_unicode_parse.chai b/unittests/string_unicode_parse.chai index 50da68bb..ae2f247e 100644 --- a/unittests/string_unicode_parse.chai +++ b/unittests/string_unicode_parse.chai @@ -1,11 +1,4 @@ -assert_equal('\u00aa', '\u00AA') -assert_equal('\u00bb', '\uBB') -assert_equal('\ucc', '\u00CC') -assert_equal('\udd', '\uDD') +assert_equal("\u00aa", "\u00AA") +assert_equal("\u00bb", "\xC2\xBB") -assert_equal('\u0ee', '\uEE') -assert_equal('\ue', '\u000E') - -assert_equal("\u30\u31\u32", "012") -assert_equal("\u33Test", "3Test") assert_equal("Test\u0040", "Test@") diff --git a/unittests/string_unicode_unicode.chai b/unittests/string_unicode_unicode.chai index 2a237ed8..93364f05 100644 --- a/unittests/string_unicode_unicode.chai +++ b/unittests/string_unicode_unicode.chai @@ -1,5 +1,16 @@ -assert_equal("\uc39c", "Ü") -assert_equal("U for \uc39cmlauts", "U for Ümlauts") -assert_equal("More \uc39cml\uc3a4\uc3bcts", "More Ümläüts") +assert_equal("\uc39c", "쎜") +assert_equal("U for \u00dcmlauts", "U for Ümlauts") -assert_equal("Thorn \uc3be sign", "Thorn þ sign") +assert_equal("Thorn \u00fe sign", "Thorn þ sign") +assert_equal("Test\u0020Me", "Test Me") +assert_equal("Test\u2022Me", "Test•Me") + +assert_equal("\xF0\x9F\x8D\x8C", "🍌") +assert_equal("\U0001F34C", "🍌") + +assert_throws("Invalid 16 bit universal character", fun(){ parse("\"\\uD83C\""); }); +assert_throws("Incomplete unicode escape sequence", fun(){ parse("\"\\uD83 \""); }); + +assert_equal("\U00024B62", "𤭢") + +assert_equal("Test\U0001F534Me", "Test🔴Me")