From 61831ebc0ea037286f4dd485fd2a8c6aec4f7b08 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Wed, 4 Feb 2026 18:58:35 +0100 Subject: [PATCH 1/2] feat(vm)!: add support for dict in 'len' and 'empty?', and always return false when comparing two different types --- .gitignore | 1 + CHANGELOG.md | 6 +++- include/Ark/VM/Value/Value.hpp | 31 ++----------------- lib/std | 2 +- src/arkreactor/VM/VM.cpp | 15 ++++++--- src/arkreactor/VM/Value/Value.cpp | 30 ++++++++++++++++++ .../typeChecking/empty_num.expected | 6 ++++ .../typeChecking/len_num.expected | 8 ++++- .../resources/LangSuite/vm-tests.ark | 8 ++++- 9 files changed, 70 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 810ff099b..c60942fd7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ build_emscripten/ ninja/ cmake-build-*/ out/ +__pycache__/ # Prerequisites *.d diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbccf797..2db8539cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,16 +3,20 @@ ## [Unreleased changes] - 20XX-XX-XX ### Breaking changes - `assert` is no longer an instruction but a builtin +- when comparing values of different types using `<`, `>`, `<=`, `>=` and `=`, the result will always be `false` (it used to rely on the type index) ### Added - added `BREAKPOINT` instruction - breakpoints can be placed using `(breakpoint)` and `(breakpoint condition)` - added a debugger that can be triggered on error or on breakpoint by passing `-fdebugger` to the CLI (see [the docs for the debugger](https://arkscript-lang.dev/docs/tutorials/debugging/)) - diagnostics can now be generated when using `State.doString`, using `Diagnostics::generateWithCode` (as the original code must be passed to the diagnostics generator) +- `empty?` can now take a dict, and returns `true` if it has no key/value pairs +- `len` can now work on dictionaries, counting the number of keys ### Changed - changed the runpath of `arkscript` to look for `libArkReactor` under its (arkscript's) directory, {arkscript}/bin, {arkscript}/lib, and {arkscript}/../lib - `and` and `or` require valid expressions, so `(or 1 (mut x 3))` is no longer valid code, as `(mut x 3)` doesn't return a value +- `(not (dict "a" 2))` now returns `false`, as `not` checks if the dict is empty ## [4.1.2] - 2026-01-09 ### Added @@ -876,7 +880,7 @@ - moved the VM FFI into include/Ark/VM ## [1.0.0-dev] - 2019 -## Added +### Added - beginning of the documentation - compiler (ark code to ark bytecode) - bytecode reader (human readable format) diff --git a/include/Ark/VM/Value/Value.hpp b/include/Ark/VM/Value/Value.hpp index cb306c9d4..27fba55cd 100644 --- a/include/Ark/VM/Value/Value.hpp +++ b/include/Ark/VM/Value/Value.hpp @@ -196,7 +196,7 @@ namespace Ark friend ARK_API bool operator==(const Value& A, const Value& B) noexcept; friend ARK_API_INLINE bool operator<(const Value& A, const Value& B) noexcept; - friend ARK_API_INLINE bool operator!(const Value& A) noexcept; + friend ARK_API bool operator!(const Value& A) noexcept; friend class Ark::VM; friend class Ark::BytecodeReader; @@ -216,7 +216,7 @@ namespace Ark inline bool operator<(const Value& A, const Value& B) noexcept { if (A.m_type != B.m_type) - return (A.typeNum() - B.typeNum()) < 0; + return false; return A.m_value < B.m_value; } @@ -224,33 +224,6 @@ namespace Ark { return !(A == B); } - - inline bool operator!(const Value& A) noexcept - { - switch (A.valueType()) - { - case ValueType::List: - return A.constList().empty(); - - case ValueType::Number: - return A.number() == 0.0; - - case ValueType::String: - return A.string().empty(); - - case ValueType::User: - [[fallthrough]]; - case ValueType::Nil: - [[fallthrough]]; - case ValueType::False: - return true; - - case ValueType::True: - [[fallthrough]]; - default: - return false; - } - } } namespace std diff --git a/lib/std b/lib/std index c6100e52e..bd5e41444 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit c6100e52e154f0d475002366b9e46dd3c043ea3b +Subproject commit bd5e414440a4ef6bebf8b740b78a0dd549086620 diff --git a/src/arkreactor/VM/VM.cpp b/src/arkreactor/VM/VM.cpp index cd2023719..a9e134047 100644 --- a/src/arkreactor/VM/VM.cpp +++ b/src/arkreactor/VM/VM.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace Ark { @@ -1287,14 +1288,14 @@ namespace Ark TARGET(LE) { const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context); - push((((*a < *b) || (*a == *b)) ? Builtins::trueSym : Builtins::falseSym), context); + push((*a < *b || *a == *b) ? Builtins::trueSym : Builtins::falseSym, context); DISPATCH(); } TARGET(GE) { const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context); - push(!(*a < *b) ? Builtins::trueSym : Builtins::falseSym, context); + push((*b < *a || *a == *b) ? Builtins::trueSym : Builtins::falseSym, context); DISPATCH(); } @@ -1320,11 +1321,14 @@ namespace Ark push(Value(static_cast(a->constList().size())), context); else if (a->valueType() == ValueType::String) push(Value(static_cast(a->string().size())), context); + else if (a->valueType() == ValueType::Dict) + push(Value(static_cast(a->dict().size())), context); else throw types::TypeCheckingError( "len", { { types::Contract { { types::Typedef("value", ValueType::List) } }, - types::Contract { { types::Typedef("value", ValueType::String) } } } }, + types::Contract { { types::Typedef("value", ValueType::String) } }, + types::Contract { { types::Typedef("value", ValueType::Dict) } } } }, { *a }); DISPATCH(); } @@ -1337,6 +1341,8 @@ namespace Ark push(a->constList().empty() ? Builtins::trueSym : Builtins::falseSym, context); else if (a->valueType() == ValueType::String) push(a->string().empty() ? Builtins::trueSym : Builtins::falseSym, context); + else if (a->valueType() == ValueType::Dict) + push(std::cmp_equal(a->dict().size(), 0) ? Builtins::trueSym : Builtins::falseSym, context); else if (a->valueType() == ValueType::Nil) push(Builtins::trueSym, context); else @@ -1344,7 +1350,8 @@ namespace Ark "empty?", { { types::Contract { { types::Typedef("value", ValueType::List) } }, types::Contract { { types::Typedef("value", ValueType::Nil) } }, - types::Contract { { types::Typedef("value", ValueType::String) } } } }, + types::Contract { { types::Typedef("value", ValueType::String) } }, + types::Contract { { types::Typedef("value", ValueType::Dict) } } } }, { *a }); DISPATCH(); } diff --git a/src/arkreactor/VM/Value/Value.cpp b/src/arkreactor/VM/Value/Value.cpp index 90f84a38b..dc064c6f7 100644 --- a/src/arkreactor/VM/Value/Value.cpp +++ b/src/arkreactor/VM/Value/Value.cpp @@ -155,4 +155,34 @@ namespace Ark return A.m_value == B.m_value; } + + bool operator!(const Value& A) noexcept + { + switch (A.valueType()) + { + case ValueType::List: + return A.constList().empty(); + + case ValueType::Number: + return A.number() == 0.0; + + case ValueType::String: + return A.string().empty(); + + case ValueType::Dict: + return std::cmp_equal(A.dict().size(), 0); + + case ValueType::User: + [[fallthrough]]; + case ValueType::Nil: + [[fallthrough]]; + case ValueType::False: + return true; + + case ValueType::True: + [[fallthrough]]; + default: + return false; + } + } } diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/empty_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/empty_num.expected index 6c3832ad0..b1eb773f9 100644 --- a/tests/unittests/resources/DiagnosticsSuite/typeChecking/empty_num.expected +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/empty_num.expected @@ -18,6 +18,12 @@ Signature Arguments → `value' (expected String), got 1 (Number) +Alternative 4: +Signature + ↳ (empty? value) +Arguments + → `value' (expected Dict), got 1 (Number) + In file tests/unittests/resources/DiagnosticsSuite/typeChecking/empty_num.ark:1 1 | (empty? 1) | ^~~~~~~~~ diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/len_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/len_num.expected index 03713d9c7..13bdc411b 100644 --- a/tests/unittests/resources/DiagnosticsSuite/typeChecking/len_num.expected +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/len_num.expected @@ -12,7 +12,13 @@ Signature Arguments → `value' (expected String), got 1 (Number) +Alternative 3: +Signature + ↳ (len value) +Arguments + → `value' (expected Dict), got 1 (Number) + In file tests/unittests/resources/DiagnosticsSuite/typeChecking/len_num.ark:1 1 | (len 1) | ^~~~~~ - 2 | \ No newline at end of file + 2 | diff --git a/tests/unittests/resources/LangSuite/vm-tests.ark b/tests/unittests/resources/LangSuite/vm-tests.ark index e67de926e..ffe332da0 100644 --- a/tests/unittests/resources/LangSuite/vm-tests.ark +++ b/tests/unittests/resources/LangSuite/vm-tests.ark @@ -52,7 +52,12 @@ (test:expect (>= 0 -4)) (test:expect (>= "hello" "hello")) (test:expect (>= "hello" "abc")) - (test:expect (< 1 "a")) # comparisons between types are ordered on type id... + (test:expect (not (< 1 "a"))) # comparisons between types are always false + (test:expect (not (> 1 "a"))) + (test:expect (not (<= 1 "a"))) + (test:expect (not (>= 1 "a"))) + (test:expect (not (= 1 "a"))) + (test:expect (!= 1 "a")) (test:neq "hello" "abc") (test:neq nil true) (test:neq nil false) @@ -62,6 +67,7 @@ (test:neq "" nil) (test:neq "" true) (test:neq "" false) + (test:expect (not (dict))) (test:expect (< parent bob)) }) (test:case "lengths and list operations" { From dfc8e0ea3de650a78f48a625245df9d0e8fb757e Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Wed, 4 Feb 2026 18:59:01 +0100 Subject: [PATCH 2/2] chore(docs): ARK-236, add documentation for ArkScript's builtins --- src/arkreactor/Builtins/Builtins.txt | 106 +++++++++++++++++++++++++++ src/arkreactor/Builtins/Dict.txt | 19 +++++ src/arkreactor/Builtins/List.txt | 79 ++++++++++++++++++++ src/arkreactor/Builtins/Math.txt | 56 ++++++++++++++ src/arkreactor/Builtins/String.txt | 79 ++++++++++++++++++++ 5 files changed, 339 insertions(+) create mode 100644 src/arkreactor/Builtins/Builtins.txt create mode 100644 src/arkreactor/Builtins/Dict.txt create mode 100644 src/arkreactor/Builtins/List.txt create mode 100644 src/arkreactor/Builtins/Math.txt create mode 100644 src/arkreactor/Builtins/String.txt diff --git a/src/arkreactor/Builtins/Builtins.txt b/src/arkreactor/Builtins/Builtins.txt new file mode 100644 index 000000000..f9415e0c7 --- /dev/null +++ b/src/arkreactor/Builtins/Builtins.txt @@ -0,0 +1,106 @@ +--# +* @name toString +* @brief Convert a value to a string +* @param value anything +* =begin +* (print (toString "abc")) # "abc" +* (print (toString 1)) # "1" +* (print (toString (fun () ()))) # "Function@1" +* (print (toString print)) # "CProcedure" +* (print (toString [])) # "[]" +* (let x 1) (print (toString (fun (&x) ()))) # "(.x=1)" +* (print (toString (dict 1 2 3 4))) # "{1: 2, 3: 4}" +* (print (toString nil)) # "nil" +* (print (toString true)) # "true" +* (print (toString false)) # "false" +* =end +#-- + +--# +* @name type +* @brief Get the type of a given value as a string +* @param value anything +* =begin +* (print (type "abc")) # "String" +* (print (type 1)) # "Number" +* (print (type (fun () ()))) # "Function" +* (print (type print)) # "CProc" +* (print (type [])) # "List" +* (let x 1) (print (type (fun (&x) ()))) # "Closure" +* (print (type (dict 1 2 3 4))) # "Dict" +* (print (type nil)) # "Nil" +* (print (type true)) # "Bool" +* (print (type false)) # "Bool" +* =end +#-- + +--# +* @name nil? +* @brief Check if a value is nil +* @param value anything +* =begin +* (print (nil? "abc")) # false +* (print (nil? 1)) # false +* (print (nil? (fun () ()))) # false +* (print (nil? print)) # false +* (print (nil? [])) # false +* (print (nil? (dict 1 2 3 4))) # false +* (print (nil? nil)) # true +* (print (nil? true)) # false +* (print (nil? false)) # false +* =end +#-- + +--# +* @name comparison +* @brief Compare two values and return true or false +* @details Comparing two values (using `<`, `>`, `<=`, `>=` and `=`) with a different type will always return false +* @param a first value +* @param b second value +* =begin +* (print (< "abc" "def")) # true, string are compared lexicographically +* (print (< 2 1)) # false +* (print (> 3 -5.5) # true +* (print (> "Hello" "")) +* (print (<= [] [1 2])) # true, lists are compared lexicographically +* (print (<= [1 2] [1 0])) # false +* (print (<= [1 2] [10])) # true +* (print (>= 5 5)) # true +* (print (= false 5)) # false +* (print (!= false 5)) # true +* =end +#-- + +--# +* @name not +* @brief Convert a value to a boolean and invert it +* @param value anything +* =begin +* (print (not "")) # true +* (print (not "a")) # false +* (print (not 0)) # true +* (print (not 1)) # false +* (print (not [])) # true +* (print (not [1 2])) # false +* (print (not nil)) # true +* (print (not true)) # false +* (print (not false)) # true +* (print (not (dict))) # true +* (print (not (dict "a" 1))) # false +* =end +#-- + +--# +* @name hasField +* @brief Check if a closure has a given field +* @param c closure +* @param field string, field name to look for +* =begin +* (let x 1) +* (let b "hello") +* (let closure (fun (&x &b) ())) +* (print (hasField closure "x")) # true +* (print (hasField closure "b")) # true +* (print (hasField closure "B")) # false, field names are case-sensitive +* =end +#-- diff --git a/src/arkreactor/Builtins/Dict.txt b/src/arkreactor/Builtins/Dict.txt new file mode 100644 index 000000000..2ac2181e5 --- /dev/null +++ b/src/arkreactor/Builtins/Dict.txt @@ -0,0 +1,19 @@ +--# +* @name empty? +* @brief Check if a dict is empty +* @param a dict +* =begin +* (print (empty? (dict "a" 2))) # false +* (print (empty? (dict))) # true +* =end +#-- + +--# +* @name len +* @brief Return the length of a dictionary +* @param a dict +* =begin +* (print (len (dict)) # 0 +* (print (len (dict "a" 1 "b" 2 "c" 3))) # 3 +* =end +#-- diff --git a/src/arkreactor/Builtins/List.txt b/src/arkreactor/Builtins/List.txt new file mode 100644 index 000000000..f0d589765 --- /dev/null +++ b/src/arkreactor/Builtins/List.txt @@ -0,0 +1,79 @@ +--# +* @name len +* @brief Return the length of a list +* @param a list +* =begin +* (print (len [])) # 0 +* (print (len (list)) # 0 +* (print (len [1 2 3])) # 3 +* =end +#-- + +--# +* @name empty? +* @brief Check if a list is empty +* @details `nil` is also considered empty, as it is the void value that can be returned by `head` +* @param a list +* =begin +* (print (empty? [])) # true +* (print (empty? (list)) # true +* (print (empty? nil)) # true +* (print (empty? [1 2 3])) # false +* =end +#-- + +--# +* @name head +* @brief Return the first element of a list, or nil if empty +* @param a list +* =begin +* (print (head [])) # nil +* (print (head (list)) # nil +* (print (head [1])) # 1 +* (print (head [1 2 3])) # 1 +* =end +#-- + +--# +* @name tail +* @brief Return the tail of a list, or empty list if it has one or less element +* @param a list +* =begin +* (print (tail [])) # [] +* (print (tail (list)) # [] +* (print (tail [1])) # [] +* (print (tail [1 2 3])) # [2 3] +* =end +#-- + +--# +* @name @ +* @brief Get an element in a list +* @details Raise an error if the index is out of range +* @param lst list +* @param i index (can be negative to start from the end) +* =begin +* (print (@ [1 2 3] 0)) # 1 +* (print (@ [1 2 3] 1)) # 2 +* (print (@ [1 2 3] 2)) # 3 +* (print (@ [1 2 3] -1)) # 3 +* (print (@ [1 2 3] -2)) # 2 +* =end +#-- + +--# +* @name @@ +* @brief Get an element in a list of lists, or list of strings +* @details Raise an error if an index is out of range +* @param lst list of collections (lists or strings, can be mixed) +* @param y index (can be negative to start from the end) +* @param x index (can be negative to start from the end) +* =begin +* (print (@@ [[1 2 3] [4 5 6] [7 8 9]] 0 1)) # 2 +* (print (@@ [[1 2 3] [4 5 6] "ghi"] -1 1)) # h +* (print (@@ [[1 2 3] [4 5 6] [7 8 9]] 0 -1)) # 3 +* (print (@@ ["abc" "def" "ghi"] 0 1)) # b +* (print (@@ ["abc" "def" "ghi"] -1 1)) # h +* (print (@@ ["abc" "def" "ghi"] 0 -1)) # c +* =end +#-- diff --git a/src/arkreactor/Builtins/Math.txt b/src/arkreactor/Builtins/Math.txt new file mode 100644 index 000000000..00ccf8234 --- /dev/null +++ b/src/arkreactor/Builtins/Math.txt @@ -0,0 +1,56 @@ +--# +* @name + +* @brief Add two or more numbers together +* @param a number +* @param b... more numbers +* =begin +* (print (+ 1 2.5)) # 3.5 +* (print (+ 5 -7.6 12)) # 9.4 +* =end +#-- + +--# +* @name - +* @brief Subtract two or more numbers together +* @param a first number to subtract other numbers to +* @param b... more numbers +* =begin +* (print (- 1 2.5)) # -1.5 +* (print (- 5 11 1)) # -7 +* =end +#-- + +--# +* @name * +* @brief Multiply two or more numbers together +* @param a number +* @param b... more numbers +* =begin +* (print (* 2 2.5)) # 5 +* (print (* 5 4 3)) # 60 +* =end +#-- + +--# +* @name / +* @brief Divide two or more numbers together +* @details The result is always a floating point number. For an integer division, see **math:floordiv** +* @param a first number to divide by other numbers +* @param b... more numbers +* =begin +* (print (/ 10 3)) # 3.33333... +* (print (/ 99 5 4)) # (99 / 5) / 4 = 4.95 +* =end +#-- + +--# +* @name mod +* @brief Compute the modulus of two numbers +* @details The result is always a floating point number +* @param a number +* @param b number +* =begin +* (print (mod 10 3)) # 1 +* (print (mod 12.9 4)) # 0.9000000000000004 +* =end +#-- diff --git a/src/arkreactor/Builtins/String.txt b/src/arkreactor/Builtins/String.txt new file mode 100644 index 000000000..f0eea36a6 --- /dev/null +++ b/src/arkreactor/Builtins/String.txt @@ -0,0 +1,79 @@ +--# +* @name + +* @brief Add two or more strings together +* @param a string +* @param b... more strings +* =begin +* (print (+ "a" "b")) # ab +* (print (+ "Hello" ", " "World" "!")) # Hello, World! +* =end +#-- + +--# +* @name len +* @brief Return the length of a string (in bytes) +* @param a string +* =begin +* (print (len "abc")) # 3 +* (print (len "🏳️‍⚧️")) # 16 +* =end +#-- + +--# +* @name empty? +* @brief Check if a string is empty +* @param a string +* =begin +* (print (empty? "abc")) # false +* (print (empty? "")) # true +* =end +#-- + +--# +* @name head +* @brief Return the first character of a string, or emtpy string if empty +* @param a string +* =begin +* (print (head "abc")) # a +* (print (head "")) # "" +* =end +#-- + +--# +* @name tail +* @brief Return the tail of a string, or emtpy string if it has one or less character +* @param a string +* =begin +* (print (tail "abc")) # bc +* (print (tail "a")) # "" +* (print (tail "")) # "" +* =end +#-- + +--# +* @name toNumber +* @brief Convert a string to a number +* @details Return nil if the conversion failed +* @param a string +* =begin +* (print (toNumber "abc")) # nil +* (print (toNumber "1")) # 1 +* (print (toNumber "1e3")) # 1000 +* (print (toNumber "3.14159")) # 3.14159 +* =end +#-- + +--# +* @name @ +* @brief Get a character in a string +* @details Raise an error if the index is out of range +* @param str string +* @param i index (can be negative to start from the end) +* =begin +* (print (@ "abc" 0)) # "a" +* (print (@ "abc" 1)) # "b" +* (print (@ "abc" 2)) # "c" +* (print (@ "abc" -1)) # "c" +* (print (@ "abc" -2)) # "b" +* =end +#--