From 97b447659f14492fd25961d433dc23b0a68baf42 Mon Sep 17 00:00:00 2001 From: Lexy Plateau Date: Thu, 27 Aug 2026 13:55:18 +0200 Subject: [PATCH 1/3] feat(syntax): adding raw strings --- CHANGELOG.md | 1 + include/Ark/Compiler/AST/Node.hpp | 24 +++++- src/arkreactor/Compiler/AST/Node.cpp | 75 +++++++++++-------- src/arkreactor/Compiler/AST/Parser.cpp | 16 ++-- src/arkscript/Formatter.cpp | 2 +- .../FormatterSuite/basics/raw_strings.ark | 6 ++ .../basics/raw_strings.expected | 6 ++ .../resources/ParserSuite/success/compact.ark | 2 + .../ParserSuite/success/compact.expected | 2 + .../resources/ParserSuite/success/strings.ark | 6 +- .../ParserSuite/success/strings.expected | 4 + 11 files changed, 102 insertions(+), 42 deletions(-) create mode 100644 tests/unittests/resources/FormatterSuite/basics/raw_strings.ark create mode 100644 tests/unittests/resources/FormatterSuite/basics/raw_strings.expected diff --git a/CHANGELOG.md b/CHANGELOG.md index 717d51e1a..5883418b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - sha256 sums of release artefacts in the releases - the debugger now display the instruction and page pointers in the traces - checking that we can properly create error messages when code is run from a string +- add support for raw strings: `r"string..."`, backslashes do not have to be escaped in them ### Changed diff --git a/include/Ark/Compiler/AST/Node.hpp b/include/Ark/Compiler/AST/Node.hpp index e2fde7cb8..e24244af0 100644 --- a/include/Ark/Compiler/AST/Node.hpp +++ b/include/Ark/Compiler/AST/Node.hpp @@ -195,6 +195,19 @@ namespace Ark::internal */ [[nodiscard]] bool isAnonymousFunction() const noexcept; + /** + * @brief Set the m_is_raw_string flag on the node + * @param is_raw_string true to mark the node as a raw string + */ + void setRawString(bool is_raw_string); + + /** + * @brief Check if a node is a raw string + * @return true if the node is a raw string + * @return false + */ + [[nodiscard]] bool isRawString() const noexcept; + /** * @brief Get the span of the node (start and end) * @@ -239,16 +252,19 @@ namespace Ark::internal friend class Parser; private: - NodeType m_type { NodeType::Unused }; Value m_value; + // 5 bits is more than enough, we can have 32 different nodes types + NodeType m_type : 5 = NodeType::Unused; + bool m_alt_syntax : 1 = false; ///< Used to tell if a node uses the alternative syntax (if available), eg (begin) / {}, (list) / [] + bool m_is_anonymous_function : 1 = true; ///< Function nodes are marked as anonymous/non-anonymous by the ASTLowerer, to enable some optimisations + bool m_is_raw_string : 1 = false; ///< Strings are by default not raw ; raw strings have all their backslashes escaped + std::optional m_unqualified_name { std::nullopt }; ///< Used by Capture nodes, to have the FQN in the value, and the captured name here // position of the node in the original code, useful when it comes to parser errors FileSpan m_pos; std::string m_filename; std::string m_comment; - std::string m_after_comment; ///< Comment after node - bool m_alt_syntax = false; ///< Used to tell if a node uses the alternative syntax (if available), eg (begin) / {}, (list) / [] - bool m_is_anonymous_function = true; ///< Function nodes are marked as anonymous/non-anonymous by the ASTLowerer, to enable some optimisations + std::string m_after_comment; ///< Comment after node }; const Node& getTrueNode(); diff --git a/src/arkreactor/Compiler/AST/Node.cpp b/src/arkreactor/Compiler/AST/Node.cpp index 3e6aff32a..4267474e2 100644 --- a/src/arkreactor/Compiler/AST/Node.cpp +++ b/src/arkreactor/Compiler/AST/Node.cpp @@ -9,7 +9,7 @@ namespace Ark::internal { Node::Node(const NodeType node_type, const std::string& value) : - m_type(node_type), m_value(value), m_pos() + m_value(value), m_type(node_type), m_pos() {} Node::Node(const NodeType node_type) : @@ -20,19 +20,19 @@ namespace Ark::internal } Node::Node(double value) : - m_type(NodeType::Number), m_value(value), m_pos() + m_value(value), m_type(NodeType::Number), m_pos() {} Node::Node(const long value) : - m_type(NodeType::Number), m_value(static_cast(value)), m_pos() + m_value(static_cast(value)), m_type(NodeType::Number), m_pos() {} Node::Node(Keyword value) : - m_type(NodeType::Keyword), m_value(value), m_pos() + m_value(value), m_type(NodeType::Keyword), m_pos() {} Node::Node(const Namespace& namespace_) : - m_type(NodeType::Namespace), m_value(namespace_), m_pos() + m_value(namespace_), m_type(NodeType::Namespace), m_pos() {} const std::string& Node::string() const noexcept @@ -156,6 +156,16 @@ namespace Ark::internal return m_is_anonymous_function; } + void Node::setRawString(const bool is_raw_string) + { + m_is_raw_string = is_raw_string; + } + + bool Node::isRawString() const noexcept + { + return m_is_raw_string; + } + FileSpan Node::position() const noexcept { return m_pos; @@ -202,7 +212,7 @@ namespace Ark::internal break; case NodeType::String: - data += "\"" + string() + "\""; + data += (m_is_raw_string ? "r\"" : "\"") + string() + "\""; break; case NodeType::Number: @@ -321,7 +331,10 @@ namespace Ark::internal break; case NodeType::String: - os << "String:" << string(); + if (m_is_raw_string) + os << "RawString:" << string(); + else + os << "String:" << string(); break; case NodeType::Number: @@ -367,30 +380,6 @@ namespace Ark::internal return os; } - const Node& getTrueNode() - { - static const Node TrueNode(NodeType::Symbol, "true"); - return TrueNode; - } - - const Node& getFalseNode() - { - static const Node FalseNode(NodeType::Symbol, "false"); - return FalseNode; - } - - const Node& getNilNode() - { - static const Node NilNode(NodeType::Symbol, "nil"); - return NilNode; - } - - const Node& getListNode() - { - static const Node ListNode(NodeType::Symbol, "list"); - return ListNode; - } - bool operator==(const Node& A, const Node& B) { if (A.m_type != B.m_type) // should have the same types @@ -419,4 +408,28 @@ namespace Ark::internal return false; } } + + const Node& getTrueNode() + { + static const Node TrueNode(NodeType::Symbol, "true"); + return TrueNode; + } + + const Node& getFalseNode() + { + static const Node FalseNode(NodeType::Symbol, "false"); + return FalseNode; + } + + const Node& getNilNode() + { + static const Node NilNode(NodeType::Symbol, "nil"); + return NilNode; + } + + const Node& getListNode() + { + static const Node ListNode(NodeType::Symbol, "list"); + return ListNode; + } } diff --git a/src/arkreactor/Compiler/AST/Parser.cpp b/src/arkreactor/Compiler/AST/Parser.cpp index ac40cb793..9b9d3f9d0 100644 --- a/src/arkreactor/Compiler/AST/Parser.cpp +++ b/src/arkreactor/Compiler/AST/Parser.cpp @@ -840,14 +840,16 @@ namespace Ark::internal std::optional Parser::string(const FilePosition filepos) { - std::string res; - if (accept(IsChar('"'))) + if (const bool is_normal_string = accept(IsChar('"')); is_normal_string || (accept(IsChar('r')) && accept(IsChar('"')))) { + const bool is_raw_string = !is_normal_string; + + std::string res; while (true) { const auto pos = getCursor(); - if (accept(IsChar('\\'))) + if (!is_raw_string && accept(IsChar('\\'))) { if (m_mode != ParserMode::Interpret) res += '\\'; @@ -960,16 +962,20 @@ namespace Ark::internal error("Unknown escape sequence", pos); } } + else if (is_raw_string) + accept(IsNot(IsChar('"')), &res); else accept(IsNot(IsEither(IsChar('\\'), IsChar('"'))), &res); if (accept(IsChar('"'))) break; if (isEOF()) - expectSuffixOrError('"', "after string"); + expectSuffixOrError('"', fmt::format("after {}string", is_raw_string ? "raw " : "")); } - return positioned(Node(NodeType::String, res), filepos); + Node str_node = Node(NodeType::String, res); + str_node.setRawString(is_raw_string); + return positioned(str_node, filepos); } return std::nullopt; } diff --git a/src/arkscript/Formatter.cpp b/src/arkscript/Formatter.cpp index 247e90805..b680df9d5 100644 --- a/src/arkscript/Formatter.cpp +++ b/src/arkscript/Formatter.cpp @@ -238,7 +238,7 @@ std::string Formatter::format(const Node& node, std::size_t indent, bool after_n result += std::string(keywords[static_cast(node.keyword())]); break; case NodeType::String: - result += fmt::format("\"{}\"", node.string()); + result += fmt::format("{}\"{}\"", node.isRawString() ? "r" : "", node.string()); break; case NodeType::Number: result += fmt::format("{}", node.number()); diff --git a/tests/unittests/resources/FormatterSuite/basics/raw_strings.ark b/tests/unittests/resources/FormatterSuite/basics/raw_strings.ark new file mode 100644 index 000000000..14f846244 --- /dev/null +++ b/tests/unittests/resources/FormatterSuite/basics/raw_strings.ark @@ -0,0 +1,6 @@ +(print a"test") +(print var"test") +(print r"") +(print r "") +(print r"hello world") +(print r"hello world\a\b\c") diff --git a/tests/unittests/resources/FormatterSuite/basics/raw_strings.expected b/tests/unittests/resources/FormatterSuite/basics/raw_strings.expected new file mode 100644 index 000000000..850be55c8 --- /dev/null +++ b/tests/unittests/resources/FormatterSuite/basics/raw_strings.expected @@ -0,0 +1,6 @@ +(print a "test") +(print var "test") +(print r"") +(print r "") +(print r"hello world") +(print r"hello world\a\b\c") diff --git a/tests/unittests/resources/ParserSuite/success/compact.ark b/tests/unittests/resources/ParserSuite/success/compact.ark index 2f0ecbf8e..131e47d88 100644 --- a/tests/unittests/resources/ParserSuite/success/compact.ark +++ b/tests/unittests/resources/ParserSuite/success/compact.ark @@ -3,3 +3,5 @@ (* 0c) (print 1"hello"d) (concat[1][2]) +(print a"123") +(print var"test") diff --git a/tests/unittests/resources/ParserSuite/success/compact.expected b/tests/unittests/resources/ParserSuite/success/compact.expected index 9a5a7d23e..ef4c9061f 100644 --- a/tests/unittests/resources/ParserSuite/success/compact.expected +++ b/tests/unittests/resources/ParserSuite/success/compact.expected @@ -3,3 +3,5 @@ ( Symbol:* Number:0 Symbol:c ) ( Symbol:print Number:1 String:hello Symbol:d ) ( Symbol:concat ( Symbol:list Number:1 ) ( Symbol:list Number:2 ) ) +( Symbol:print Symbol:a String:123 ) +( Symbol:print Symbol:var String:test ) diff --git a/tests/unittests/resources/ParserSuite/success/strings.ark b/tests/unittests/resources/ParserSuite/success/strings.ark index 0a83b60e0..c8f62633b 100644 --- a/tests/unittests/resources/ParserSuite/success/strings.ark +++ b/tests/unittests/resources/ParserSuite/success/strings.ark @@ -1,2 +1,6 @@ (print "abc" "123\"test") -(print "\\ 123aéoÒ") \ No newline at end of file +(print "\\ 123aéoÒ") +(print r"abcd") +(print r"\a\b\c\d") +(print r"\\") +(print r"") diff --git a/tests/unittests/resources/ParserSuite/success/strings.expected b/tests/unittests/resources/ParserSuite/success/strings.expected index f22fbeba8..27928e5b7 100644 --- a/tests/unittests/resources/ParserSuite/success/strings.expected +++ b/tests/unittests/resources/ParserSuite/success/strings.expected @@ -1,2 +1,6 @@ ( Symbol:print String:abc String:123"test ) ( Symbol:print String:\ 123aéoÒ ) +( Symbol:print RawString:abcd ) +( Symbol:print RawString:\a\b\c\d ) +( Symbol:print RawString:\\ ) +( Symbol:print RawString: ) From 1a22a5778e96a4096fcb7b4629d6c74fe9d510e1 Mon Sep 17 00:00:00 2001 From: Lexy Plateau Date: Thu, 27 Aug 2026 14:13:27 +0200 Subject: [PATCH 2/3] feat: @ can be used with dict --- CHANGELOG.md | 3 + docs/arkdoc/Dict.txt | 12 ++++ include/Ark/VM/Helpers.hpp | 69 ++++++++++--------- lib/modules | 2 +- lib/std | 2 +- .../typeChecking/at_list_str.expected | 7 ++ .../typeChecking/at_num_num.expected | 9 ++- .../typeChecking/builtin_at_num_zero.expected | 9 ++- 8 files changed, 78 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5883418b8..ca5f47f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,11 @@ - the debugger now display the instruction and page pointers in the traces - checking that we can properly create error messages when code is run from a string - add support for raw strings: `r"string..."`, backslashes do not have to be escaped in them +- stdlib: + - new draft module `re`, using `google/re2` as the regex engine ### Changed +- `@` can be used with dictionaries: `(@ dict key)` will behave the same as `(dict:get dict key)` ### Removed - removed `std.Range` which had been deprecated in 4.6.0 diff --git a/docs/arkdoc/Dict.txt b/docs/arkdoc/Dict.txt index 2ac2181e5..ad552fe05 100644 --- a/docs/arkdoc/Dict.txt +++ b/docs/arkdoc/Dict.txt @@ -17,3 +17,15 @@ * (print (len (dict "a" 1 "b" 2 "c" 3))) # 3 * =end #-- + +--# +* @name @ +# @brief Get a value from a given dictionary using a key, or nil if it doesn't exist +# @param _D dictionary +# @param _key key to get +# =begin +# (let data (dict "key" "value")) +# (print (@ data "key")) # value +# =end +* =end +#-- diff --git a/include/Ark/VM/Helpers.hpp b/include/Ark/VM/Helpers.hpp index a13d4f56f..5bf7ec47a 100644 --- a/include/Ark/VM/Helpers.hpp +++ b/include/Ark/VM/Helpers.hpp @@ -73,41 +73,48 @@ namespace Ark::helper inline ARK_ALWAYS_INLINE Value at(Value& container, Value& index, VM& vm) { - if (index.valueType() != ValueType::Number) - throw types::TypeCheckingError( - "@", - { { types::Contract { { types::Typedef("src", ValueType::List), types::Typedef("idx", ValueType::Number) } }, - types::Contract { { types::Typedef("src", ValueType::String), types::Typedef("idx", ValueType::Number) } } } }, - { container, index }); - - const auto num = static_cast(index.number()); - - if (container.valueType() == ValueType::List) + if (container.valueType() != ValueType::Dict) { - const auto i = static_cast(num < 0 ? static_cast(container.list().size()) + num : num); - if (i < container.list().size()) - return container.list()[i]; + if (index.valueType() != ValueType::Number) + throw types::TypeCheckingError( + "@", + { { types::Contract { { types::Typedef("src", ValueType::List), types::Typedef("idx", ValueType::Number) } }, + types::Contract { { types::Typedef("src", ValueType::String), types::Typedef("idx", ValueType::Number) } }, + types::Contract { { types::Typedef("src", ValueType::Dict), types::Typedef("key", ValueType::Any) } } } }, + { container, index }); + + const auto num = static_cast(index.number()); + + if (container.valueType() == ValueType::List) + { + const auto i = static_cast(num < 0 ? static_cast(container.list().size()) + num : num); + if (i < container.list().size()) + return container.list()[i]; + else + VM::throwVMError( + ErrorKind::Index, + fmt::format("{} out of range {} (length {})", num, container.toString(vm), container.list().size())); + } + else if (container.valueType() == ValueType::String) + { + const auto i = static_cast(num < 0 ? static_cast(container.string().size()) + num : num); + if (i < container.string().size()) + return Value(std::string(1, container.string()[i])); + else + VM::throwVMError( + ErrorKind::Index, + fmt::format("{} out of range \"{}\" (length {})", num, container.string(), container.string().size())); + } else - VM::throwVMError( - ErrorKind::Index, - fmt::format("{} out of range {} (length {})", num, container.toString(vm), container.list().size())); - } - else if (container.valueType() == ValueType::String) - { - const auto i = static_cast(num < 0 ? static_cast(container.string().size()) + num : num); - if (i < container.string().size()) - return Value(std::string(1, container.string()[i])); - else - VM::throwVMError( - ErrorKind::Index, - fmt::format("{} out of range \"{}\" (length {})", num, container.string(), container.string().size())); + throw types::TypeCheckingError( + "@", + { { types::Contract { { types::Typedef("src", ValueType::List), types::Typedef("idx", ValueType::Number) } }, + types::Contract { { types::Typedef("src", ValueType::String), types::Typedef("idx", ValueType::Number) } }, + types::Contract { { types::Typedef("src", ValueType::Dict), types::Typedef("key", ValueType::Any) } } } }, + { container, index }); } else - throw types::TypeCheckingError( - "@", - { { types::Contract { { types::Typedef("src", ValueType::List), types::Typedef("idx", ValueType::Number) } }, - types::Contract { { types::Typedef("src", ValueType::String), types::Typedef("idx", ValueType::Number) } } } }, - { container, index }); + return container.dictRef().get(index); } inline ARK_ALWAYS_INLINE Value atAt(const Value* x, const Value* y, Value& list) diff --git a/lib/modules b/lib/modules index d639f56ef..8444ec016 160000 --- a/lib/modules +++ b/lib/modules @@ -1 +1 @@ -Subproject commit d639f56effb0c6da193c74e2b902ae250d5d4ec7 +Subproject commit 8444ec016176c5c8a41c759900f81cfcaa9b4819 diff --git a/lib/std b/lib/std index 505e54a5c..85b907da9 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 505e54a5c7cf8331e9d08cd6715740a730734f2c +Subproject commit 85b907da943719942d68258c97823df846b549db diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_list_str.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_list_str.expected index 6b425201c..fb9ff6617 100644 --- a/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_list_str.expected +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_list_str.expected @@ -14,6 +14,13 @@ Arguments → `src' (expected String), got [] (List) → `idx' (expected Number), got "1" (String) +Alternative 3: +Signature + ↳ (@ src key) +Arguments + → `src' (expected Dict), got [] (List) + → `key' (expected any) ✓ + In file tests/unittests/resources/DiagnosticsSuite/typeChecking/at_list_str.ark:1 1 | (@ [] "1") | ^~~~~~~~~ diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_num_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_num_num.expected index 47efb37e0..b97f63c34 100644 --- a/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_num_num.expected +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/at_num_num.expected @@ -14,7 +14,14 @@ Arguments → `src' (expected String), got 1 (Number) → `idx' (expected Number) ✓ +Alternative 3: +Signature + ↳ (@ src key) +Arguments + → `src' (expected Dict), got 1 (Number) + → `key' (expected any) ✓ + In file tests/unittests/resources/DiagnosticsSuite/typeChecking/at_num_num.ark:1 1 | (@ 1 2) | ^~~~~~ - 2 | \ No newline at end of file + 2 | diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_at_num_zero.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_at_num_zero.expected index 5888c42e7..c60e74bac 100644 --- a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_at_num_zero.expected +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_at_num_zero.expected @@ -14,7 +14,14 @@ Arguments → `src' (expected String), got 1 (Number) → `idx' (expected Number) ✓ +Alternative 3: +Signature + ↳ (@ src key) +Arguments + → `src' (expected Dict), got 1 (Number) + → `key' (expected any) ✓ + In file tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_at_num_zero.ark:1 1 | (print (apply @ [1 0])) | ^~~~~~~~~~~~~~~~~~~~~~ - 2 | \ No newline at end of file + 2 | From c05d5899c9e072eb8d4c639fbef1af94cd3ca37d Mon Sep 17 00:00:00 2001 From: Lexy Plateau Date: Thu, 27 Aug 2026 15:56:11 +0200 Subject: [PATCH 3/3] config(cmake): set minimum C++ version required --- CMakeLists.txt | 11 ++++++++--- lib/modules | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2459d542f..33aa05ac0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,11 @@ else () set(ARK_BUILD_DATE "2026-01-02T17:56:00Z") endif () +if (CMAKE_SOURCE_DIR STREQUAL ${PROJECT_SOURCE_DIR}) + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif () + # needed so that cppcheck has a cache folder, when launched through pre-commit file(MAKE_DIRECTORY cache-cppcheck) @@ -83,7 +88,7 @@ file(GLOB_RECURSE SOURCE_FILES if (NOT ARK_EMSCRIPTEN AND NOT ARK_STATIC) add_library(ArkReactor SHARED ${SOURCE_FILES}) - # enable_lto(ArkReactor) + enable_lto(ArkReactor) set_target_properties(ArkReactor PROPERTIES INSTALL_NAME_DIR "@rpath" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_INSTALL_BINDIR}) @@ -336,7 +341,7 @@ if (ARK_BENCHMARKS) target_link_libraries(bench PUBLIC ArkReactor benchmark::benchmark) target_compile_features(bench PRIVATE cxx_std_20) target_compile_definitions(bench PRIVATE ARK_TESTS_ROOT="${CMAKE_CURRENT_SOURCE_DIR}/") - # enable_lto(bench) + enable_lto(bench) endif () if (ARK_BUILD_EXE) @@ -367,7 +372,7 @@ if (ARK_BUILD_EXE) INSTALL_RPATH_USE_LINK_PATH ON) set_target_rpath(arkscript) - # enable_lto(arkscript) + enable_lto(arkscript) # Installs the arkscript executable. install(TARGETS arkscript diff --git a/lib/modules b/lib/modules index 8444ec016..ff667b379 160000 --- a/lib/modules +++ b/lib/modules @@ -1 +1 @@ -Subproject commit 8444ec016176c5c8a41c759900f81cfcaa9b4819 +Subproject commit ff667b3790505cf8e6e20850ed5ce523c630e88c