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

ArkScript-lang / Ark / 22587629131

02 Mar 2026 05:27PM UTC coverage: 93.519% (+0.03%) from 93.49%
22587629131

push

github

SuperFola
feat(format builtin): support integer format specifiers when the given number can be represented as one

17 of 17 new or added lines in 1 file covered. (100.0%)

4 existing lines in 1 file now uncovered.

9437 of 10091 relevant lines covered (93.52%)

272250.51 hits per line

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

98.11
/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>
13✔
25
{
26
private:
27
    fmt::basic_string_view<char> opening_bracket_ = fmt::detail::string_literal<char, '['> {};
13✔
28
    fmt::basic_string_view<char> closing_bracket_ = fmt::detail::string_literal<char, ']'> {};
13✔
29
    fmt::basic_string_view<char> separator_ = fmt::detail::string_literal<char, ' '> {};
13✔
30
    bool is_debug = false;
13✔
31
    bool is_literal_str = false;
13✔
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)
7✔
36
    {
7✔
37
        opening_bracket_ = open;
7✔
38
        closing_bracket_ = close;
7✔
39
    }
7✔
40

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

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

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

66
            case 'c':
67
                if (*it == 'c')
4✔
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'> {});
2✔
77
                ++it;
2✔
78
                return it;
3✔
79

80
            case '?':
81
                is_debug = true;
1✔
82
                set_brackets({}, {});
1✔
83
                ++it;
1✔
84
                if (it == end || *it != 's')
2✔
UNCOV
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✔
UNCOV
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
    }
10✔
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
19✔
132
    {
19✔
133
        auto out = ctx.out();
19✔
134
        auto it = fmt::detail::range_begin(value.value.constList());
19✔
135
        const auto end = fmt::detail::range_end(value.value.constList());
19✔
136
        if (is_debug)
19✔
137
            return write_debug_string(out, it, end, value.vm_ptr);
1✔
138

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

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

148
            auto&& item = *it;
63✔
149
            if (item.valueType() == Ark::ValueType::List)
63✔
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);
54✔
157
                out = underlying_.format(formatted, ctx);
54✔
158
            }
54✔
159

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

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

166
        return out;
18✔
167
    }
19✔
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)
262✔
200
    {
262✔
201
        if (n.size() < 2 || n[0].valueType() != ValueType::String)
262✔
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;
261✔
209

210
        for (auto it = n.begin() + 1, it_end = n.end(); it != it_end; ++it)
648✔
211
        {
212
            if (it->valueType() == ValueType::String)
387✔
213
                store.push_back(it->stringRef());
145✔
214
            else if (it->valueType() == ValueType::Number)
242✔
215
            {
216
                double int_part;
225✔
217
                if (std::modf(it->number(), &int_part) == 0.0)
225✔
218
                    store.push_back(static_cast<long>(it->number()));
190✔
219
                else
220
                    store.push_back(it->number());
35✔
221
            }
225✔
222
            else if (it->valueType() == ValueType::Nil)
17✔
223
                store.push_back("nil");
1✔
224
            else if (it->valueType() == ValueType::True)
16✔
225
                store.push_back("true");
1✔
226
            else if (it->valueType() == ValueType::False)
15✔
227
                store.push_back("false");
1✔
228
            else if (it->valueType() == ValueType::List)
14✔
229
            {
230
                // std::vector<value_wrapper> r;
231
                // std::ranges::transform(
232
                //     it->list(),
233
                //     std::back_inserter(r),
234
                //     [&vm](const Value& val) -> value_wrapper {
235
                //         return value_wrapper { val, vm };
236
                //     });
237
                store.push_back(value_wrapper { *it, vm });
13✔
238
            }
13✔
239
            else
240
                store.push_back(it->toString(*vm));
1✔
241
        }
387✔
242

243
        try
244
        {
245
            return Value(fmt::vformat(n[0].stringRef(), store));
261✔
246
        }
15✔
247
        catch (fmt::format_error& e)
248
        {
249
            throw std::runtime_error(
30✔
250
                fmt::format("format: can not format \"{}\" ({} argument{} provided) because of {}",
45✔
251
                            n[0].stringRef(),
15✔
252
                            n.size() - 1,
15✔
253
                            // if we have more than one argument (not counting the string to format), plural form
254
                            n.size() > 2 ? "s" : "",
15✔
255
                            e.what()));
15✔
256
        }
15✔
257
    }
292✔
258

259
    Value findSubStr(std::vector<Value>& n, VM* vm [[maybe_unused]])
352✔
260
    {
352✔
261
        if (!types::check(n, ValueType::String, ValueType::String) &&
352✔
262
            !types::check(n, ValueType::String, ValueType::String, ValueType::Number))
192✔
263
            throw types::TypeCheckingError(
2✔
264
                "string:find",
1✔
265
                { { types::Contract {
3✔
266
                        { types::Typedef("string", ValueType::String),
2✔
267
                          types::Typedef("substr", ValueType::String) } },
1✔
268
                    types::Contract {
1✔
269
                        { types::Typedef("string", ValueType::String),
3✔
270
                          types::Typedef("substr", ValueType::String),
1✔
271
                          types::Typedef("startIndex", ValueType::Number) } } } },
1✔
272
                n);
1✔
273

274
        const std::size_t start = n.size() == 3 ? static_cast<std::size_t>(n[2].number()) : 0;
351✔
275
        const std::size_t index = n[0].stringRef().find(n[1].stringRef(), start);
351✔
276
        if (index != std::string::npos)
351✔
277
            return Value(static_cast<int>(index));
285✔
278
        return Value(-1);
66✔
279
    }
352✔
280

281
    Value removeAtStr(std::vector<Value>& n, VM* vm [[maybe_unused]])
11✔
282
    {
11✔
283
        if (!types::check(n, ValueType::String, ValueType::Number))
11✔
284
            throw types::TypeCheckingError(
3✔
285
                "string:removeAt",
1✔
286
                { { types::Contract { { types::Typedef("string", ValueType::String), types::Typedef("index", ValueType::Number) } } } },
1✔
287
                n);
1✔
288

289
        long num = static_cast<long>(n[1].number());
10✔
290
        const auto i = static_cast<std::size_t>(num < 0 ? static_cast<long>(n[0].stringRef().size()) + num : num);
10✔
291
        if (i < n[0].stringRef().size())
10✔
292
        {
293
            n[0].stringRef().erase(i, 1);
9✔
294
            return n[0];
9✔
295
        }
296
        else
297
            throw std::runtime_error(fmt::format("string:removeAt: index {} out of range (length: {})", num, n[0].stringRef().size()));
1✔
298
    }
12✔
299

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

308
        return Value(utf8::codepoint(n[0].stringRef().c_str()));
7,378✔
309
    }
1✔
310

311
    // cppcheck-suppress constParameterReference
312
    Value chr(std::vector<Value>& n, VM* vm [[maybe_unused]])
2,472✔
313
    {
2,472✔
314
        if (!types::check(n, ValueType::Number))
2,472✔
315
            throw types::TypeCheckingError(
2✔
316
                "string:chr",
1✔
317
                { { types::Contract { { types::Typedef("codepoint", ValueType::Number) } } } },
1✔
318
                n);
1✔
319

320
        std::array<char, 5> utf8 {};
2,471✔
321
        utf8::codepointToUtf8(static_cast<int>(n[0].number()), utf8.data());
2,471✔
322
        return Value(std::string(utf8.data()));
2,471✔
323
    }
2,472✔
324

325
    Value setStringAt(std::vector<Value>& n, VM* vm [[maybe_unused]])
7✔
326
    {
7✔
327
        if (!types::check(n, ValueType::String, ValueType::Number, ValueType::String))
7✔
328
            throw types::TypeCheckingError(
3✔
329
                "string:setAt",
1✔
330
                { { types::Contract { { types::Typedef("string", ValueType::String),
3✔
331
                                        types::Typedef("index", ValueType::Number),
1✔
332
                                        types::Typedef("value", ValueType::String) } } } },
1✔
333
                n);
1✔
334

335
        auto& string = n[0].stringRef();
6✔
336

337
        const std::size_t size = string.size();
6✔
338
        long idx = static_cast<long>(n[1].number());
6✔
339
        idx = idx < 0 ? static_cast<long>(size) + idx : idx;
6✔
340
        if (std::cmp_greater_equal(idx, size))
6✔
341
            throw std::runtime_error(
1✔
342
                fmt::format("IndexError: string:setAt index ({}) out of range (string size: {})", idx, size));
1✔
343

344
        string[static_cast<std::size_t>(idx)] = n[2].string()[0];
5✔
345
        return n[0];
5✔
346
    }
8✔
347
}
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