Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

### Added
- new debugger commands: `stack <n>` and `locals <n>` to print the values on the stack and in the current locals scope
- custom format specifiers for lists:
- `:n` to remove surrounding brackets,
- `:c` / `:nc` to use `, ` as a separator instead of ` `,
- `:l` / `:nl` to use `\n` as a separator,
- `:?s` to format as an escaped quoted string,
- `:s` to format as a quoted string
- `format` can use format specifiers for integers: `b`, `#b`, `B`, `#B`, `c`, `d`, `o`, `x`, `#x`, `X`, and `#X` if the argument is an integer

### Changed

Expand Down
191 changes: 189 additions & 2 deletions src/arkreactor/Builtins/String.cpp
Original file line number Diff line number Diff line change
@@ -1,20 +1,190 @@
#include <Ark/Builtins/Builtins.hpp>

#include <utility>
#include <cmath>
#include <utf8.hpp>
#include <fmt/args.h>
#include <fmt/base.h>
#include <fmt/ostream.h>
#include <fmt/core.h>
#include <fmt/ranges.h>
#include <fmt/format.h>

#include <Ark/TypeChecker.hpp>
#include <Ark/VM/VM.hpp>

struct value_wrapper
{
const Ark::Value& value;
Ark::VM* vm_ptr;
bool nested = false;
};

template <>
struct fmt::formatter<value_wrapper>
{
private:
fmt::basic_string_view<char> opening_bracket_ = fmt::detail::string_literal<char, '['> {};
fmt::basic_string_view<char> closing_bracket_ = fmt::detail::string_literal<char, ']'> {};
fmt::basic_string_view<char> separator_ = fmt::detail::string_literal<char, ' '> {};
bool is_debug = false;
bool is_literal_str = false;
fmt::formatter<std::string> underlying_;

public:
void set_brackets(const fmt::basic_string_view<char> open, const fmt::basic_string_view<char> close)
{
opening_bracket_ = open;
closing_bracket_ = close;
}

void set_separator(const fmt::basic_string_view<char> sep)
{
separator_ = sep;
}

format_parse_context::iterator parse(fmt::format_parse_context& ctx)
{
auto it = ctx.begin();
const auto end = ctx.end();
if (it == end)
return underlying_.parse(ctx);

switch (detail::to_ascii(*it))
{
case 'n':
set_brackets({}, {});
++it;
if (it == end)
report_error("invalid format specifier");
if (*it == '}')
return it;
if (*it != 'c' && *it != 'l')
report_error("invalid format specifier. Expected either :nc or :nl");
[[fallthrough]];

case 'c':
if (*it == 'c')
{
set_separator(fmt::detail::string_literal<char, ',', ' '> {});
++it;
return it;
}
[[fallthrough]];

case 'l':
set_separator(fmt::detail::string_literal<char, '\n'> {});
++it;
return it;

case '?':
is_debug = true;
set_brackets({}, {});
++it;
if (it == end || *it != 's')
report_error("invalid format specifier. Expected :?s, not :?");
[[fallthrough]];

case 's':
if (!is_debug)
{
set_brackets(fmt::detail::string_literal<char, '"'> {},
fmt::detail::string_literal<char, '"'> {});
set_separator({});
is_literal_str = true;
}
++it;
return it;

default:
break;
}

if (it != end && *it != '}')
{
if (*it != ':')
report_error("invalid format specifier");
++it;
}

ctx.advance_to(it);
return underlying_.parse(ctx);
}

template <typename Output, typename It, typename Sentinel>
auto write_debug_string(Output& out, It it, Sentinel end, Ark::VM* vm_ptr) const -> Output
{
auto buf = fmt::basic_memory_buffer<char>();
for (; it != end; ++it)
{
auto formatted = it->toString(*vm_ptr);
buf.append(formatted);
}
auto specs = fmt::format_specs();
specs.set_type(fmt::presentation_type::debug);
return fmt::detail::write<char>(
out,
fmt::basic_string_view<char>(buf.data(), buf.size()),
specs);
}

fmt::format_context::iterator format(const value_wrapper& value, fmt::format_context& ctx) const
{
auto out = ctx.out();
auto it = fmt::detail::range_begin(value.value.constList());
const auto end = fmt::detail::range_end(value.value.constList());
if (is_debug)
return write_debug_string(out, it, end, value.vm_ptr);

if ((is_literal_str && !value.nested) || !is_literal_str)
out = fmt::detail::copy<char>(opening_bracket_, out);

for (int i = 0; it != end; ++it)
{
if (i > 0)
out = fmt::detail::copy<char>(separator_, out);
ctx.advance_to(out);

auto&& item = *it;
if (item.valueType() == Ark::ValueType::List)
{
// if :s, do not put surrounding "" here
format({ item, value.vm_ptr, /* nested= */ true }, ctx);
}
else
{
std::string formatted = item.toString(*value.vm_ptr);
out = underlying_.format(formatted, ctx);
}

++i;
}

if ((is_literal_str && !value.nested) || !is_literal_str)
out = detail::copy<char>(closing_bracket_, out);

return out;
}
};

namespace Ark::internal::Builtins::String
{
/**
* @name format
* @brief Format a String given replacements
* @details https://fmt.dev/12.0/syntax/
* @details See [fmt.dev](https://fmt.dev/12.0/syntax/) for syntax.
* =details-begin
* In the case of lists, we have custom specifiers:
* - `n` removes surrounding brackets, uses ' ' as a separator
* - `?s` debug format. The list is formatted as an escaped string
* - `s` string format. The list is formatted as a string
* - `c` changes the separator to ', '
* - `l` changes the separator to '\n'
*
* `n` can be combined with either `c` and `l` (which are mutually exclusive): `nc`, `nl`.
*
* 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.
* =details-end
* @param format the String to format
* @param values as any argument as you need, of any valid ArkScript type
* =begin
Expand Down Expand Up @@ -42,13 +212,30 @@ namespace Ark::internal::Builtins::String
if (it->valueType() == ValueType::String)
store.push_back(it->stringRef());
else if (it->valueType() == ValueType::Number)
store.push_back(it->number());
{
double int_part;
if (std::modf(it->number(), &int_part) == 0.0)
store.push_back(static_cast<long>(it->number()));
else
store.push_back(it->number());
}
else if (it->valueType() == ValueType::Nil)
store.push_back("nil");
else if (it->valueType() == ValueType::True)
store.push_back("true");
else if (it->valueType() == ValueType::False)
store.push_back("false");
else if (it->valueType() == ValueType::List)
{
// std::vector<value_wrapper> r;
// std::ranges::transform(
// it->list(),
// std::back_inserter(r),
// [&vm](const Value& val) -> value_wrapper {
// return value_wrapper { val, vm };
// });
store.push_back(value_wrapper { *it, vm });
}
else
store.push_back(it->toString(*vm));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:b}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:b}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_b.ark:1
1 | (print (format "{:b}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:c}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:c}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_c.ark:1
1 | (print (format "{:c}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:d}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:d}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_d.ark:1
1 | (print (format "{:d}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:#b}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:#b}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_hashtag_b.ark:1
1 | (print (format "{:#b}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:#B}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:#B}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_hashtag_upper_b.ark:1
1 | (print (format "{:#B}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:#X}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:#X}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_hashtag_upper_x.ark:1
1 | (print (format "{:#X}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:#x}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:#x}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_hashtag_x.ark:1
1 | (print (format "{:#x}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:o}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:o}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_o.ark:1
1 | (print (format "{:o}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:B}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:B}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_upper_b.ark:1
1 | (print (format "{:B}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:X}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:X}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_upper_x.ark:1
1 | (print (format "{:X}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:x}" 65.1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:x}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_double_x.ark:1
1 | (print (format "{:x}" 65.1))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:d}" []))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:d}" (1 argument provided) because of invalid format specifier

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_list_invalid.ark:1
1 | (print (format "{:d}" []))
| ^~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:?}" []))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:?}" (1 argument provided) because of invalid format specifier. Expected :?s, not :?

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_list_invalid_debug.ark:1
1 | (print (format "{:?}" []))
| ^~~~~~~~~~~~~~~~~~~~~~~~~
2 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(print (format "{:na}" []))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
format: can not format "{:na}" (1 argument provided) because of invalid format specifier. Expected either :nc or :nl

In file tests/unittests/resources/DiagnosticsSuite/runtime/format_list_invalid_na.ark:1
1 | (print (format "{:na}" []))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
2 |
25 changes: 24 additions & 1 deletion tests/unittests/resources/LangSuite/string-tests.ark
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,27 @@

(test:case "format strings" {
(test:eq "nilfalsetrue" (format "{}{}{}" nil false true))
(test:eq "CProcedure" (format "{}" print)) })})
(test:eq "CProcedure" (format "{}" print))

(test:eq (format "{}" ["hel\tlo" 1 nil [true "a" 2]]) "[hel lo 1 nil [true a 2]]")
(test:eq (format "{::}" ["hel\tlo" 1 nil [true "a" 2]]) "[hel lo 1 nil [true a 2]]")
(test:eq (format "{:n}" ["hel\tlo" 1 nil [true "a" 2]]) "hel lo 1 nil true a 2")
(test:eq (format "{:?s}" ["hel\tlo" 1 nil [true "a" 2]]) "\"hel\\tlo1nil[true \\\"a\\\" 2]\"")
(test:eq (format "{:s}" ["hel\tlo" 1 nil [true "a" 2]]) "\"hel lo1niltruea2\"")
(test:eq (format "{:c}" ["hel\tlo" 1 nil [true "a" 2]]) "[hel lo, 1, nil, [true, a, 2]]")
(test:eq (format "{:l}" ["hel\tlo" 1 nil [true "a" 2]]) "[hel lo\n1\nnil\n[true\na\n2]]")
(test:eq (format "{:nc}" ["hel\tlo" 1 nil [true "a" 2]]) "hel lo, 1, nil, true, a, 2")
(test:eq (format "{:nl}" ["hel\tlo" 1 nil [true "a" 2]]) "hel lo\n1\nnil\ntrue\na\n2")
(test:eq (format "{::<5}" ["hello" 1 nil [true "a" 2]]) "[hello 1 nil [true a 2 ]]")

(test:eq (format "{:b}" 65) "1000001")
(test:eq (format "{:#b}" 65) "0b1000001")
(test:eq (format "{:B}" 65) "1000001")
(test:eq (format "{:#B}" 65) "0B1000001")
(test:eq (format "{:c}" 65) "A")
(test:eq (format "{:d}" 65) "65")
(test:eq (format "{:o}" 65) "101")
(test:eq (format "{:x}" 63) "3f")
(test:eq (format "{:#x}" 63) "0x3f")
(test:eq (format "{:X}" 63) "3F")
(test:eq (format "{:#X}" 63) "0X3F") })})
Loading