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

ArkScript-lang / Ark / 22574503723

02 Mar 2026 11:44AM UTC coverage: 93.501% (+0.01%) from 93.49%
22574503723

Pull #652

github

web-flow
Merge 23fb22e02 into 0bd5da88e
Pull Request #652: feat(format builtin): new format specifiers for lists

90 of 95 new or added lines in 1 file covered. (94.74%)

9424 of 10079 relevant lines covered (93.5%)

272244.74 hits per line

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

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

3
#include <utility>
4
#include <string_view>
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
};
21

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

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

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

44
    format_parse_context::iterator parse(fmt::format_parse_context& ctx)
11✔
45
    {
11✔
46
        auto it = ctx.begin();
11✔
47
        const auto end = ctx.end();
11✔
48
        if (it == end)
11✔
NEW
49
            return underlying_.parse(ctx);
×
50

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

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

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

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

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

96
            default:
97
                break;
4✔
98
        }
4✔
99

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

107
        ctx.advance_to(it);
4✔
108
        return underlying_.parse(ctx);
2✔
109
    }
9✔
110

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

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

136
        out = fmt::detail::copy<char>(opening_bracket_, out);
8✔
137
        for (int i = 0; it != end; ++it)
40✔
138
        {
139
            if (i > 0)
32✔
140
                out = fmt::detail::copy<char>(separator_, out);
24✔
141
            ctx.advance_to(out);
32✔
142
            auto&& item = *it;
32✔
143
            auto formatted = item.toString(*value.vm_ptr);
32✔
144
            out = underlying_.format(formatted, ctx);
32✔
145
            ++i;
32✔
146
        }
32✔
147
        out = detail::copy<char>(closing_bracket_, out);
8✔
148
        return out;
8✔
149
    }
9✔
150
};
151

152
namespace Ark::internal::Builtins::String
153
{
154
    /**
155
     * @name format
156
     * @brief Format a String given replacements
157
     * @details See [fmt.dev](https://fmt.dev/12.0/syntax/) for syntax.
158
     * =details-begin
159
     * In the case of lists, we have custom specifiers:
160
     * - `n` removes surrounding brackets, uses ' ' as a separator
161
     * - `?s` debug format. The list is formatted as an escaped string
162
     * - `s` string format. The list is formatted as a string
163
     * - `c` changes the separator to ', '
164
     * - `l` changes the separator to '\n'
165
     *
166
     * `n` can be combined with either `c` and `l` (which are mutually exclusive): `nc`, `nl`.
167
     *
168
     * 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.
169
     * =details-end
170
     * @param format the String to format
171
     * @param values as any argument as you need, of any valid ArkScript type
172
     * =begin
173
     * (format "Hello {}, my name is {}" "world" "ArkScript")
174
     * # Hello world, my name is ArkScript
175
     *
176
     * (format "Test {} with {{}}" "1")
177
     * # Test 1 with {}
178
     * =end
179
     * @author https://github.com/SuperFola
180
     */
181
    Value format(std::vector<Value>& n, VM* vm)
239✔
182
    {
239✔
183
        if (n.size() < 2 || n[0].valueType() != ValueType::String)
239✔
184
            throw types::TypeCheckingError(
6✔
185
                "format",
1✔
186
                { { types::Contract { { types::Typedef("string", ValueType::String),
2✔
187
                                        types::Typedef("value", ValueType::Any, /* variadic */ true) } } } },
1✔
188
                n);
1✔
189

190
        fmt::dynamic_format_arg_store<fmt::format_context> store;
238✔
191

192
        for (auto it = n.begin() + 1, it_end = n.end(); it != it_end; ++it)
602✔
193
        {
194
            if (it->valueType() == ValueType::String)
364✔
195
                store.push_back(it->stringRef());
145✔
196
            else if (it->valueType() == ValueType::Number)
219✔
197
                store.push_back(it->number());
203✔
198
            else if (it->valueType() == ValueType::Nil)
16✔
199
                store.push_back("nil");
1✔
200
            else if (it->valueType() == ValueType::True)
15✔
201
                store.push_back("true");
1✔
202
            else if (it->valueType() == ValueType::False)
14✔
203
                store.push_back("false");
1✔
204
            else if (it->valueType() == ValueType::List)
13✔
205
            {
206
                // std::vector<value_wrapper> r;
207
                // std::ranges::transform(
208
                //     it->list(),
209
                //     std::back_inserter(r),
210
                //     [&vm](const Value& val) -> value_wrapper {
211
                //         return value_wrapper { val, vm };
212
                //     });
213
                store.push_back(value_wrapper { *it, vm });
12✔
214
            }
12✔
215
            else
216
                store.push_back(it->toString(*vm));
1✔
217
        }
364✔
218

219
        try
220
        {
221
            return Value(fmt::vformat(n[0].stringRef(), store));
238✔
222
        }
4✔
223
        catch (fmt::format_error& e)
224
        {
225
            throw std::runtime_error(
8✔
226
                fmt::format("format: can not format \"{}\" ({} argument{} provided) because of {}",
12✔
227
                            n[0].stringRef(),
4✔
228
                            n.size() - 1,
4✔
229
                            // if we have more than one argument (not counting the string to format), plural form
230
                            n.size() > 2 ? "s" : "",
4✔
231
                            e.what()));
4✔
232
        }
4✔
233
    }
247✔
234

235
    Value findSubStr(std::vector<Value>& n, VM* vm [[maybe_unused]])
352✔
236
    {
352✔
237
        if (!types::check(n, ValueType::String, ValueType::String) &&
352✔
238
            !types::check(n, ValueType::String, ValueType::String, ValueType::Number))
192✔
239
            throw types::TypeCheckingError(
2✔
240
                "string:find",
1✔
241
                { { types::Contract {
3✔
242
                        { types::Typedef("string", ValueType::String),
2✔
243
                          types::Typedef("substr", ValueType::String) } },
1✔
244
                    types::Contract {
1✔
245
                        { types::Typedef("string", ValueType::String),
3✔
246
                          types::Typedef("substr", ValueType::String),
1✔
247
                          types::Typedef("startIndex", ValueType::Number) } } } },
1✔
248
                n);
1✔
249

250
        const std::size_t start = n.size() == 3 ? static_cast<std::size_t>(n[2].number()) : 0;
351✔
251
        const std::size_t index = n[0].stringRef().find(n[1].stringRef(), start);
351✔
252
        if (index != std::string::npos)
351✔
253
            return Value(static_cast<int>(index));
285✔
254
        return Value(-1);
66✔
255
    }
352✔
256

257
    Value removeAtStr(std::vector<Value>& n, VM* vm [[maybe_unused]])
11✔
258
    {
11✔
259
        if (!types::check(n, ValueType::String, ValueType::Number))
11✔
260
            throw types::TypeCheckingError(
3✔
261
                "string:removeAt",
1✔
262
                { { types::Contract { { types::Typedef("string", ValueType::String), types::Typedef("index", ValueType::Number) } } } },
1✔
263
                n);
1✔
264

265
        long num = static_cast<long>(n[1].number());
10✔
266
        const auto i = static_cast<std::size_t>(num < 0 ? static_cast<long>(n[0].stringRef().size()) + num : num);
10✔
267
        if (i < n[0].stringRef().size())
10✔
268
        {
269
            n[0].stringRef().erase(i, 1);
9✔
270
            return n[0];
9✔
271
        }
272
        else
273
            throw std::runtime_error(fmt::format("string:removeAt: index {} out of range (length: {})", num, n[0].stringRef().size()));
1✔
274
    }
12✔
275

276
    Value ord(std::vector<Value>& n, VM* vm [[maybe_unused]])
7,379✔
277
    {
7,379✔
278
        if (!types::check(n, ValueType::String))
7,379✔
279
            throw types::TypeCheckingError(
2✔
280
                "string:ord",
1✔
281
                { { types::Contract { { types::Typedef("string", ValueType::String) } } } },
1✔
282
                n);
1✔
283

284
        return Value(utf8::codepoint(n[0].stringRef().c_str()));
7,378✔
285
    }
1✔
286

287
    // cppcheck-suppress constParameterReference
288
    Value chr(std::vector<Value>& n, VM* vm [[maybe_unused]])
2,472✔
289
    {
2,472✔
290
        if (!types::check(n, ValueType::Number))
2,472✔
291
            throw types::TypeCheckingError(
2✔
292
                "string:chr",
1✔
293
                { { types::Contract { { types::Typedef("codepoint", ValueType::Number) } } } },
1✔
294
                n);
1✔
295

296
        std::array<char, 5> utf8 {};
2,471✔
297
        utf8::codepointToUtf8(static_cast<int>(n[0].number()), utf8.data());
2,471✔
298
        return Value(std::string(utf8.data()));
2,471✔
299
    }
2,472✔
300

301
    Value setStringAt(std::vector<Value>& n, VM* vm [[maybe_unused]])
7✔
302
    {
7✔
303
        if (!types::check(n, ValueType::String, ValueType::Number, ValueType::String))
7✔
304
            throw types::TypeCheckingError(
3✔
305
                "string:setAt",
1✔
306
                { { types::Contract { { types::Typedef("string", ValueType::String),
3✔
307
                                        types::Typedef("index", ValueType::Number),
1✔
308
                                        types::Typedef("value", ValueType::String) } } } },
1✔
309
                n);
1✔
310

311
        auto& string = n[0].stringRef();
6✔
312

313
        const std::size_t size = string.size();
6✔
314
        long idx = static_cast<long>(n[1].number());
6✔
315
        idx = idx < 0 ? static_cast<long>(size) + idx : idx;
6✔
316
        if (std::cmp_greater_equal(idx, size))
6✔
317
            throw std::runtime_error(
1✔
318
                fmt::format("IndexError: string:setAt index ({}) out of range (string size: {})", idx, size));
1✔
319

320
        string[static_cast<std::size_t>(idx)] = n[2].string()[0];
5✔
321
        return n[0];
5✔
322
    }
8✔
323
}
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