• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ArkScript-lang / Ark / 23809216591

31 Mar 2026 04:52PM UTC coverage: 93.837% (+0.08%) from 93.76%
23809216591

Pull #671

github

web-flow
Merge 70f63b126 into 4dd99f445
Pull Request #671: fix(builtins): string:ord need to check it get only one utf8 char

49 of 55 new or added lines in 2 files covered. (89.09%)

9805 of 10449 relevant lines covered (93.84%)

652046.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.79
/src/arkreactor/Builtins/String.cpp
1
#include <Ark/Builtins/Builtins.hpp>
2

3
#include <utility>
4
#include <cmath>
5
#include <utf8.hpp>
6
#include <fmt/args.h>
7
#include <fmt/base.h>
8
#include <fmt/ostream.h>
9
#include <fmt/core.h>
10
#include <fmt/ranges.h>
11
#include <fmt/format.h>
12

13
#include <Ark/TypeChecker.hpp>
14
#include <Ark/VM/VM.hpp>
15

16
struct value_wrapper
17
{
18
    const Ark::Value& value;
19
    Ark::VM* vm_ptr;
20
    bool nested = false;
21
};
22

23
template <>
24
struct fmt::formatter<value_wrapper>
14✔
25
{
26
private:
27
    fmt::basic_string_view<char> opening_bracket_ = fmt::detail::string_literal<char, '['> {};
14✔
28
    fmt::basic_string_view<char> closing_bracket_ = fmt::detail::string_literal<char, ']'> {};
14✔
29
    fmt::basic_string_view<char> separator_ = fmt::detail::string_literal<char, ' '> {};
14✔
30
    bool is_debug = false;
14✔
31
    bool is_literal_str = false;
14✔
32
    fmt::formatter<std::string> underlying_;
33

34
public:
35
    void set_brackets(const fmt::basic_string_view<char> open, const fmt::basic_string_view<char> close)
8✔
36
    {
8✔
37
        opening_bracket_ = open;
8✔
38
        closing_bracket_ = close;
8✔
39
    }
8✔
40

41
    void set_separator(const fmt::basic_string_view<char> sep)
6✔
42
    {
6✔
43
        separator_ = sep;
6✔
44
    }
6✔
45

46
    format_parse_context::iterator parse(fmt::format_parse_context& ctx)
13✔
47
    {
13✔
48
        auto it = ctx.begin();
13✔
49
        const auto end = ctx.end();
13✔
50
        if (it == end)
13✔
51
            return underlying_.parse(ctx);
1✔
52

53
        switch (detail::to_ascii(*it))
12✔
54
        {
4✔
55
            case 'n':
56
                set_brackets({}, {});
4✔
57
                ++it;
4✔
58
                if (it == end)
4✔
59
                    report_error("invalid format specifier");
×
60
                if (*it == '}')
4✔
61
                    return it;
1✔
62
                if (*it != 'c' && *it != 'l')
4✔
63
                    report_error("invalid format specifier. Expected either :nc or :nl");
×
64
                [[fallthrough]];
65

66
            case 'c':
67
                if (*it == 'c')
5✔
68
                {
69
                    set_separator(fmt::detail::string_literal<char, ',', ' '> {});
2✔
70
                    ++it;
2✔
71
                    return it;
2✔
72
                }
73
                [[fallthrough]];
74

75
            case 'l':
76
                set_separator(fmt::detail::string_literal<char, '\n'> {});
3✔
77
                ++it;
3✔
78
                return it;
4✔
79

80
            case '?':
81
                is_debug = true;
1✔
82
                set_brackets({}, {});
1✔
83
                ++it;
1✔
84
                if (it == end || *it != 's')
2✔
85
                    report_error("invalid format specifier. Expected :?s, not :?");
×
86
                [[fallthrough]];
87

88
            case 's':
89
                if (!is_debug)
2✔
90
                {
91
                    set_brackets(fmt::detail::string_literal<char, '"'> {},
2✔
92
                                 fmt::detail::string_literal<char, '"'> {});
1✔
93
                    set_separator({});
1✔
94
                    is_literal_str = true;
1✔
95
                }
1✔
96
                ++it;
2✔
97
                return it;
6✔
98

99
            default:
100
                break;
4✔
101
        }
4✔
102

103
        if (it != end && *it != '}')
4✔
104
        {
105
            if (*it != ':')
2✔
106
                report_error("invalid format specifier");
×
107
            ++it;
2✔
108
        }
2✔
109

110
        ctx.advance_to(it);
4✔
111
        return underlying_.parse(ctx);
2✔
112
    }
11✔
113

114
    template <typename Output, typename It, typename Sentinel>
115
    auto write_debug_string(Output& out, It it, Sentinel end, Ark::VM* vm_ptr) const -> Output
1✔
116
    {
1✔
117
        auto buf = fmt::basic_memory_buffer<char>();
1✔
118
        for (; it != end; ++it)
5✔
119
        {
120
            auto formatted = it->toString(*vm_ptr);
4✔
121
            buf.append(formatted);
4✔
122
        }
4✔
123
        auto specs = fmt::format_specs();
1✔
124
        specs.set_type(fmt::presentation_type::debug);
1✔
125
        return fmt::detail::write<char>(
1✔
126
            out,
1✔
127
            fmt::basic_string_view<char>(buf.data(), buf.size()),
1✔
128
            specs);
129
    }
1✔
130

131
    fmt::format_context::iterator format(const value_wrapper& value, fmt::format_context& ctx) const
20✔
132
    {
20✔
133
        auto out = ctx.out();
20✔
134
        auto it = fmt::detail::range_begin(value.value.constList());
20✔
135
        const auto end = fmt::detail::range_end(value.value.constList());
20✔
136
        if (is_debug)
20✔
137
            return write_debug_string(out, it, end, value.vm_ptr);
1✔
138

139
        if ((is_literal_str && !value.nested) || !is_literal_str)
19✔
140
            out = fmt::detail::copy<char>(opening_bracket_, out);
18✔
141

142
        for (int i = 0; it != end; ++it)
98✔
143
        {
144
            if (i > 0)
79✔
145
                out = fmt::detail::copy<char>(separator_, out);
60✔
146
            ctx.advance_to(out);
79✔
147

148
            auto&& item = *it;
79✔
149
            if (item.valueType() == Ark::ValueType::List)
79✔
150
            {
151
                // if :s, do not put surrounding "" here
152
                format({ item, value.vm_ptr, /* nested= */ true }, ctx);
9✔
153
            }
9✔
154
            else
155
            {
156
                std::string formatted = item.toString(*value.vm_ptr);
70✔
157
                out = underlying_.format(formatted, ctx);
70✔
158
            }
70✔
159

160
            ++i;
79✔
161
        }
79✔
162

163
        if ((is_literal_str && !value.nested) || !is_literal_str)
19✔
164
            out = detail::copy<char>(closing_bracket_, out);
18✔
165

166
        return out;
19✔
167
    }
20✔
168
};
169

170
namespace Ark::internal::Builtins::String
171
{
172
    /**
173
     * @name format
174
     * @brief Format a String given replacements
175
     * @details See [fmt.dev](https://fmt.dev/12.0/syntax/) for syntax.
176
     * =details-begin
177
     * In the case of lists, we have custom specifiers:
178
     * - `n` removes surrounding brackets, uses ' ' as a separator
179
     * - `?s` debug format. The list is formatted as an escaped string
180
     * - `s` string format. The list is formatted as a string
181
     * - `c` changes the separator to ', '
182
     * - `l` changes the separator to '\n'
183
     *
184
     * `n` can be combined with either `c` and `l` (which are mutually exclusive): `nc`, `nl`.
185
     *
186
     * The underlying formatter is the one of strings, so you can write `::<10` to align all elements left in a 10 char wide block each.
187
     * =details-end
188
     * @param format the String to format
189
     * @param values as any argument as you need, of any valid ArkScript type
190
     * =begin
191
     * (format "Hello {}, my name is {}" "world" "ArkScript")
192
     * # Hello world, my name is ArkScript
193
     *
194
     * (format "Test {} with {{}}" "1")
195
     * # Test 1 with {}
196
     * =end
197
     * @author https://github.com/SuperFola
198
     */
199
    Value format(std::vector<Value>& n, VM* vm)
395✔
200
    {
395✔
201
        if (n.size() < 2 || n[0].valueType() != ValueType::String)
395✔
202
            throw types::TypeCheckingError(
17✔
203
                "format",
1✔
204
                { { types::Contract { { types::Typedef("string", ValueType::String),
2✔
205
                                        types::Typedef("value", ValueType::Any, /* variadic */ true) } } } },
1✔
206
                n);
1✔
207

208
        fmt::dynamic_format_arg_store<fmt::format_context> store;
394✔
209

210
        for (auto it = n.begin() + 1, it_end = n.end(); it != it_end; ++it)
995✔
211
        {
212
            if (it->valueType() == ValueType::String)
601✔
213
                store.push_back(it->stringRef());
356✔
214
            else if (it->valueType() == ValueType::Number)
245✔
215
            {
216
                double int_part;
227✔
217
                if (std::modf(it->number(), &int_part) == 0.0)
227✔
218
                    store.push_back(static_cast<long>(it->number()));
191✔
219
                else
220
                    store.push_back(it->number());
36✔
221
            }
227✔
222
            else if (it->valueType() == ValueType::Nil)
18✔
223
                store.push_back("nil");
1✔
224
            else if (it->valueType() == ValueType::True)
17✔
225
                store.push_back("true");
1✔
226
            else if (it->valueType() == ValueType::False)
16✔
227
                store.push_back("false");
1✔
228
            else if (it->valueType() == ValueType::List)
15✔
229
                store.push_back(value_wrapper { *it, vm });
14✔
230
            else
231
                store.push_back(it->toString(*vm));
1✔
232
        }
601✔
233

234
        try
235
        {
236
            return Value(fmt::vformat(n[0].stringRef(), store));
394✔
237
        }
15✔
238
        catch (fmt::format_error& e)
239
        {
240
            throw std::runtime_error(
30✔
241
                fmt::format("format: can not format \"{}\" ({} argument{} provided) because of {}",
45✔
242
                            n[0].stringRef(),
15✔
243
                            n.size() - 1,
15✔
244
                            // if we have more than one argument (not counting the string to format), plural form
245
                            n.size() > 2 ? "s" : "",
15✔
246
                            e.what()));
15✔
247
        }
15✔
248
    }
425✔
249

250
    Value findSubStr(std::vector<Value>& n, VM* vm [[maybe_unused]])
381✔
251
    {
381✔
252
        if (!types::check(n, ValueType::String, ValueType::String) &&
381✔
253
            !types::check(n, ValueType::String, ValueType::String, ValueType::Number))
218✔
254
            throw types::TypeCheckingError(
2✔
255
                "string:find",
1✔
256
                { { types::Contract {
3✔
257
                        { types::Typedef("string", ValueType::String),
2✔
258
                          types::Typedef("substr", ValueType::String) } },
1✔
259
                    types::Contract {
1✔
260
                        { types::Typedef("string", ValueType::String),
3✔
261
                          types::Typedef("substr", ValueType::String),
1✔
262
                          types::Typedef("startIndex", ValueType::Number) } } } },
1✔
263
                n);
1✔
264

265
        const std::size_t start = n.size() == 3 ? static_cast<std::size_t>(n[2].number()) : 0;
380✔
266
        const std::size_t index = n[0].stringRef().find(n[1].stringRef(), start);
380✔
267
        if (index != std::string::npos)
380✔
268
            return Value(static_cast<int>(index));
311✔
269
        return Value(-1);
69✔
270
    }
381✔
271

272
    Value removeAtStr(std::vector<Value>& n, VM* vm [[maybe_unused]])
11✔
273
    {
11✔
274
        if (!types::check(n, ValueType::String, ValueType::Number))
11✔
275
            throw types::TypeCheckingError(
3✔
276
                "string:removeAt",
1✔
277
                { { types::Contract { { types::Typedef("string", ValueType::String), types::Typedef("index", ValueType::Number) } } } },
1✔
278
                n);
1✔
279

280
        long num = static_cast<long>(n[1].number());
10✔
281
        const auto i = static_cast<std::size_t>(num < 0 ? static_cast<long>(n[0].stringRef().size()) + num : num);
10✔
282
        if (i < n[0].stringRef().size())
10✔
283
        {
284
            n[0].stringRef().erase(i, 1);
9✔
285
            return n[0];
9✔
286
        }
287
        else
288
            throw std::runtime_error(fmt::format("string:removeAt: index {} out of range (length: {})", num, n[0].stringRef().size()));
1✔
289
    }
12✔
290

291
    Value utf8len(std::vector<Value>& n, VM* vm [[maybe_unused]])
7✔
292
    {
7✔
293
        if (!types::check(n, ValueType::String))
7✔
294
            throw types::TypeCheckingError(
2✔
295
                "string:utf8len",
1✔
296
                { { types::Contract { { types::Typedef("string", ValueType::String) } } } },
1✔
297
                n);
1✔
298

299
        const std::size_t len = utf8::length(n[0].stringRef().c_str());
6✔
300
        return Value(static_cast<double>(len));
6✔
301
    }
7✔
302

303
    Value ord(std::vector<Value>& n, VM* vm [[maybe_unused]])
7,622✔
304
    {
7,622✔
305
        if (!types::check(n, ValueType::String))
7,622✔
306
            throw types::TypeCheckingError(
4✔
307
                "string:ord",
1✔
308
                { { types::Contract { { types::Typedef("string", ValueType::String) } } } },
1✔
309
                n);
1✔
310

311
        if (const std::size_t len = utf8::length(n[0].stringRef().c_str()); len != 1)
7,623✔
312
            throw std::runtime_error(fmt::format("string:ord: invalid string '{}', expected a single character, got {}", n[0].string(), len));
2✔
313

314
        const int32_t codepoint = utf8::codepoint(n[0].stringRef().c_str());
7,619✔
315
        if (codepoint == -1)
7,619✔
NEW
316
            throw std::runtime_error(fmt::format("string:ord: invalid string '{}'", n[0].string()));
×
317
        return Value(codepoint);
7,619✔
318
    }
7,622✔
319

320
    // cppcheck-suppress constParameterReference
321
    Value chr(std::vector<Value>& n, VM* vm [[maybe_unused]])
2,685✔
322
    {
2,685✔
323
        if (!types::check(n, ValueType::Number))
2,685✔
324
            throw types::TypeCheckingError(
2✔
325
                "string:chr",
1✔
326
                { { types::Contract { { types::Typedef("codepoint", ValueType::Number) } } } },
1✔
327
                n);
1✔
328

329
        std::array<char, 5> utf8 {};
2,684✔
330
        utf8::codepointToUtf8(static_cast<int>(n[0].number()), utf8.data());
2,684✔
331
        return Value(std::string(utf8.data()));
2,684✔
332
    }
2,685✔
333

334
    Value setStringAt(std::vector<Value>& n, VM* vm [[maybe_unused]])
7✔
335
    {
7✔
336
        if (!types::check(n, ValueType::String, ValueType::Number, ValueType::String))
7✔
337
            throw types::TypeCheckingError(
3✔
338
                "string:setAt",
1✔
339
                { { types::Contract { { types::Typedef("string", ValueType::String),
3✔
340
                                        types::Typedef("index", ValueType::Number),
1✔
341
                                        types::Typedef("value", ValueType::String) } } } },
1✔
342
                n);
1✔
343

344
        auto& string = n[0].stringRef();
6✔
345

346
        const std::size_t size = string.size();
6✔
347
        long idx = static_cast<long>(n[1].number());
6✔
348
        idx = idx < 0 ? static_cast<long>(size) + idx : idx;
6✔
349
        if (std::cmp_greater_equal(idx, size))
6✔
350
            throw std::runtime_error(
1✔
351
                fmt::format("IndexError: string:setAt index ({}) out of range (string size: {})", idx, size));
1✔
352

353
        string[static_cast<std::size_t>(idx)] = n[2].string()[0];
5✔
354
        return n[0];
5✔
355
    }
8✔
356
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc