feat(pipeline): the pure pipeline, checked against its goldens
The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b02de3ceff
commit
741e3a656d
70 changed files with 30565 additions and 21 deletions
|
|
@ -205,6 +205,46 @@ namespace Crafter::Review::Dyn {
|
|||
return Encode(std::u32string_view(decoded).substr(static_cast<std::size_t>(from), static_cast<std::size_t>(to - from)));
|
||||
}
|
||||
|
||||
std::pair<char32_t, std::size_t> CodePointAt(std::string_view text, std::size_t at) {
|
||||
return Next(text, at);
|
||||
}
|
||||
|
||||
std::pair<std::size_t, std::size_t> SliceBounds(std::size_t size, std::int64_t start, std::optional<std::int64_t> end) {
|
||||
const auto count = static_cast<std::int64_t>(size);
|
||||
auto clip = [count](std::int64_t index) {
|
||||
if (index < 0) index += count;
|
||||
return std::clamp<std::int64_t>(index, 0, count);
|
||||
};
|
||||
const std::int64_t from = clip(start);
|
||||
const std::int64_t to = end ? clip(*end) : count;
|
||||
return {static_cast<std::size_t>(from), static_cast<std::size_t>(std::max(from, to))};
|
||||
}
|
||||
|
||||
namespace {
|
||||
template <typename Predicate>
|
||||
std::size_t RunOf(std::string_view text, std::size_t at, Predicate matches) {
|
||||
std::size_t end = at;
|
||||
while (end < text.size()) {
|
||||
const auto [c, used] = Next(text, end);
|
||||
if (!matches(c)) break;
|
||||
end += used;
|
||||
}
|
||||
return end - at;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t DecimalRun(std::string_view text, std::size_t at) {
|
||||
return RunOf(text, at, [](char32_t c) { return IsDecimal(c); });
|
||||
}
|
||||
|
||||
std::size_t SpaceRun(std::string_view text, std::size_t at) {
|
||||
return RunOf(text, at, [](char32_t c) { return IsSpace(c); });
|
||||
}
|
||||
|
||||
std::size_t NonSpaceRun(std::string_view text, std::size_t at) {
|
||||
return RunOf(text, at, [](char32_t c) { return !IsSpace(c); });
|
||||
}
|
||||
|
||||
bool IsSpace(char32_t c) {
|
||||
return std::ranges::binary_search(Unicode::Space, c);
|
||||
}
|
||||
|
|
@ -230,6 +270,10 @@ namespace Crafter::Review::Dyn {
|
|||
return InRuns(Unicode::Printable, c);
|
||||
}
|
||||
|
||||
bool IsWord(char32_t c) {
|
||||
return InRuns(Unicode::Word, c);
|
||||
}
|
||||
|
||||
char IgnoreCaseLetter(char32_t c) {
|
||||
if (c >= 'a' && c <= 'z') return static_cast<char>(c);
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c - 'A' + 'a');
|
||||
|
|
@ -413,21 +457,305 @@ namespace Crafter::Review::Dyn {
|
|||
return out;
|
||||
}
|
||||
|
||||
std::string LJust(std::string_view text, std::size_t width) {
|
||||
const std::size_t length = Length(text);
|
||||
std::string out(text);
|
||||
if (length < width) out.append(width - length, ' ');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ReadText(const std::filesystem::path& path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) throw std::runtime_error(std::format("[Errno 2] No such file or directory: {}", Repr(std::string_view(path.string()))));
|
||||
const std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
for (std::size_t at = 0; at < bytes.size();) {
|
||||
const auto [c, used] = Next(bytes, at);
|
||||
if (c == 0xFFFD && (used != 3 || std::string_view(bytes).substr(at, 3) != "\xEF\xBF\xBD")) {
|
||||
throw std::runtime_error(std::format("'utf-8' codec can't decode byte 0x{:02x} in position {}: invalid utf-8 ({})",
|
||||
static_cast<std::uint8_t>(bytes[at]), at, path.string()));
|
||||
}
|
||||
at += used;
|
||||
if (!in || std::filesystem::is_directory(path)) {
|
||||
throw Error("FileNotFoundError", std::format("[Errno 2] No such file or directory: {}", Repr(std::string_view(path.string()))));
|
||||
}
|
||||
const std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
if (const std::optional<std::string> error = Utf8DecodeError(bytes)) throw Error("UnicodeDecodeError", *error);
|
||||
return UniversalNewlines(bytes);
|
||||
}
|
||||
|
||||
std::optional<std::string> Utf8DecodeError(std::string_view bytes) {
|
||||
// The decoder's reasons: a bad lead byte is "invalid start byte"; the
|
||||
// k-th continuation byte out of range is "invalid continuation byte"
|
||||
// over k bytes; the input ending inside a sequence is "unexpected end
|
||||
// of data" over the rest of it.
|
||||
for (std::size_t at = 0; at < bytes.size();) {
|
||||
const auto lead = static_cast<std::uint8_t>(bytes[at]);
|
||||
std::size_t need = 0;
|
||||
std::uint8_t low = 0x80;
|
||||
std::uint8_t high = 0xBF;
|
||||
if (lead < 0x80) {
|
||||
++at;
|
||||
continue;
|
||||
}
|
||||
if (lead >= 0xC2 && lead <= 0xDF) {
|
||||
need = 1;
|
||||
} else if (lead >= 0xE0 && lead <= 0xEF) {
|
||||
need = 2;
|
||||
if (lead == 0xE0) low = 0xA0;
|
||||
if (lead == 0xED) high = 0x9F;
|
||||
} else if (lead >= 0xF0 && lead <= 0xF4) {
|
||||
need = 3;
|
||||
if (lead == 0xF0) low = 0x90;
|
||||
if (lead == 0xF4) high = 0x8F;
|
||||
}
|
||||
std::string_view reason;
|
||||
std::size_t length = 1;
|
||||
if (need == 0) {
|
||||
reason = "invalid start byte";
|
||||
} else {
|
||||
for (std::size_t k = 1; k <= need; ++k) {
|
||||
if (at + k >= bytes.size()) {
|
||||
reason = "unexpected end of data";
|
||||
length = bytes.size() - at;
|
||||
break;
|
||||
}
|
||||
const auto next = static_cast<std::uint8_t>(bytes[at + k]);
|
||||
if (next < low || next > high) {
|
||||
reason = "invalid continuation byte";
|
||||
length = k;
|
||||
break;
|
||||
}
|
||||
low = 0x80;
|
||||
high = 0xBF;
|
||||
}
|
||||
}
|
||||
if (reason.empty()) {
|
||||
at += need + 1;
|
||||
continue;
|
||||
}
|
||||
if (length == 1) return std::format("'utf-8' codec can't decode byte 0x{:02x} in position {}: {}", lead, at, reason);
|
||||
return std::format("'utf-8' codec can't decode bytes in position {}-{}: {}", at, at + length - 1, reason);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct JsonFailure {
|
||||
std::string_view message;
|
||||
std::size_t position;
|
||||
};
|
||||
|
||||
bool IsJsonSpace(char32_t c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
||||
}
|
||||
|
||||
bool IsJsonDigit(char32_t c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
std::int32_t HexDigit(char32_t c) {
|
||||
if (c >= '0' && c <= '9') return static_cast<std::int32_t>(c - '0');
|
||||
if (c >= 'a' && c <= 'f') return static_cast<std::int32_t>(c - 'a' + 10);
|
||||
if (c >= 'A' && c <= 'F') return static_cast<std::int32_t>(c - 'A' + 10);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// A JSON scanner that fails where and why the recorded messages say,
|
||||
// step for step: a string, an object, an array, a number, then any
|
||||
// value. "Expecting value" is a position where no value starts.
|
||||
class JsonScanner {
|
||||
public:
|
||||
explicit JsonScanner(std::u32string_view text) : s(text) {}
|
||||
|
||||
std::optional<JsonFailure> Value(std::size_t idx, std::size_t& next) {
|
||||
if (idx >= s.size()) return JsonFailure{"Expecting value", idx};
|
||||
switch (s[idx]) {
|
||||
case '"':
|
||||
return String(idx + 1, next);
|
||||
case '{':
|
||||
return Object(idx + 1, next);
|
||||
case '[':
|
||||
return Array(idx + 1, next);
|
||||
case 'n':
|
||||
if (Word(idx, U"null", next)) return std::nullopt;
|
||||
break;
|
||||
case 't':
|
||||
if (Word(idx, U"true", next)) return std::nullopt;
|
||||
break;
|
||||
case 'f':
|
||||
if (Word(idx, U"false", next)) return std::nullopt;
|
||||
break;
|
||||
case 'N':
|
||||
if (Word(idx, U"NaN", next)) return std::nullopt;
|
||||
break;
|
||||
case 'I':
|
||||
if (Word(idx, U"Infinity", next)) return std::nullopt;
|
||||
break;
|
||||
case '-':
|
||||
if (Word(idx, U"-Infinity", next)) return std::nullopt;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Number(idx, next);
|
||||
}
|
||||
|
||||
private:
|
||||
std::u32string_view s;
|
||||
|
||||
bool Word(std::size_t idx, std::u32string_view word, std::size_t& next) const {
|
||||
if (s.substr(idx, word.size()) != word) return false;
|
||||
next = idx + word.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t SkipSpace(std::size_t idx) const {
|
||||
while (idx < s.size() && IsJsonSpace(s[idx])) ++idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
std::optional<JsonFailure> String(std::size_t end, std::size_t& next) const {
|
||||
const std::size_t begin = end - 1;
|
||||
const std::size_t len = s.size();
|
||||
while (true) {
|
||||
std::size_t at = end;
|
||||
char32_t c = 0;
|
||||
for (; at < len; ++at) {
|
||||
c = s[at];
|
||||
if (c == '"' || c == '\\') break;
|
||||
if (c <= 0x1F) return JsonFailure{"Invalid control character at", at};
|
||||
}
|
||||
if (at >= len) return JsonFailure{"Unterminated string starting at", begin};
|
||||
++at;
|
||||
if (c == '"') {
|
||||
next = at;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (at == len) return JsonFailure{"Unterminated string starting at", begin};
|
||||
c = s[at];
|
||||
if (c != 'u') {
|
||||
end = at + 1;
|
||||
if (!std::u32string_view(U"\"\\/bfnrt").contains(c)) return JsonFailure{"Invalid \\escape", end - 2};
|
||||
continue;
|
||||
}
|
||||
++at;
|
||||
end = at + 4;
|
||||
if (end >= len) return JsonFailure{"Invalid \\uXXXX escape", at - 1};
|
||||
std::uint32_t unit = 0;
|
||||
for (; at < end; ++at) {
|
||||
const std::int32_t digit = HexDigit(s[at]);
|
||||
if (digit < 0) return JsonFailure{"Invalid \\uXXXX escape", end - 5};
|
||||
unit = (unit << 4) | static_cast<std::uint32_t>(digit);
|
||||
}
|
||||
if (unit >= 0xD800 && unit <= 0xDBFF && end + 6 < len && s[at] == '\\' && s[at + 1] == 'u') {
|
||||
at += 2;
|
||||
end += 6;
|
||||
std::uint32_t second = 0;
|
||||
for (; at < end; ++at) {
|
||||
const std::int32_t digit = HexDigit(s[at]);
|
||||
if (digit < 0) return JsonFailure{"Invalid \\uXXXX escape", end - 5};
|
||||
second = (second << 4) | static_cast<std::uint32_t>(digit);
|
||||
}
|
||||
if (second < 0xDC00 || second > 0xDFFF) end -= 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<JsonFailure> Object(std::size_t idx, std::size_t& next) {
|
||||
idx = SkipSpace(idx);
|
||||
if (idx >= s.size() || s[idx] != '}') {
|
||||
while (true) {
|
||||
if (idx >= s.size() || s[idx] != '"') return JsonFailure{"Expecting property name enclosed in double quotes", idx};
|
||||
std::size_t after = 0;
|
||||
if (std::optional<JsonFailure> failure = String(idx + 1, after)) return failure;
|
||||
idx = SkipSpace(after);
|
||||
if (idx >= s.size() || s[idx] != ':') return JsonFailure{"Expecting ':' delimiter", idx};
|
||||
idx = SkipSpace(idx + 1);
|
||||
if (std::optional<JsonFailure> failure = Value(idx, after)) return failure;
|
||||
idx = SkipSpace(after);
|
||||
if (idx < s.size() && s[idx] == '}') break;
|
||||
if (idx >= s.size() || s[idx] != ',') return JsonFailure{"Expecting ',' delimiter", idx};
|
||||
const std::size_t comma = idx;
|
||||
idx = SkipSpace(idx + 1);
|
||||
if (idx < s.size() && s[idx] == '}') return JsonFailure{"Illegal trailing comma before end of object", comma};
|
||||
}
|
||||
}
|
||||
next = idx + 1;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<JsonFailure> Array(std::size_t idx, std::size_t& next) {
|
||||
idx = SkipSpace(idx);
|
||||
if (idx >= s.size() || s[idx] != ']') {
|
||||
while (true) {
|
||||
std::size_t after = 0;
|
||||
if (std::optional<JsonFailure> failure = Value(idx, after)) return failure;
|
||||
idx = SkipSpace(after);
|
||||
if (idx < s.size() && s[idx] == ']') break;
|
||||
if (idx >= s.size() || s[idx] != ',') return JsonFailure{"Expecting ',' delimiter", idx};
|
||||
const std::size_t comma = idx;
|
||||
idx = SkipSpace(idx + 1);
|
||||
if (idx < s.size() && s[idx] == ']') return JsonFailure{"Illegal trailing comma before end of array", comma};
|
||||
}
|
||||
}
|
||||
next = idx + 1;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<JsonFailure> Number(std::size_t start, std::size_t& next) const {
|
||||
const std::size_t len = s.size();
|
||||
std::size_t idx = start;
|
||||
if (s[idx] == '-') {
|
||||
++idx;
|
||||
if (idx >= len) return JsonFailure{"Expecting value", start};
|
||||
}
|
||||
if (s[idx] >= '1' && s[idx] <= '9') {
|
||||
++idx;
|
||||
while (idx < len && IsJsonDigit(s[idx])) ++idx;
|
||||
} else if (s[idx] == '0') {
|
||||
++idx;
|
||||
} else {
|
||||
return JsonFailure{"Expecting value", start};
|
||||
}
|
||||
if (idx + 1 < len && s[idx] == '.' && IsJsonDigit(s[idx + 1])) {
|
||||
idx += 2;
|
||||
while (idx < len && IsJsonDigit(s[idx])) ++idx;
|
||||
}
|
||||
if (idx + 1 < len && (s[idx] == 'e' || s[idx] == 'E')) {
|
||||
const std::size_t exponent = idx;
|
||||
++idx;
|
||||
if (idx + 1 < len && (s[idx] == '-' || s[idx] == '+')) ++idx;
|
||||
while (idx < len && IsJsonDigit(s[idx])) ++idx;
|
||||
if (!IsJsonDigit(s[idx - 1])) idx = exponent;
|
||||
}
|
||||
next = idx;
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<std::string> JsonDecodeError(std::string_view text) {
|
||||
const std::u32string s = Decode(text);
|
||||
JsonScanner scanner(s);
|
||||
std::size_t start = 0;
|
||||
while (start < s.size() && IsJsonSpace(s[start])) ++start;
|
||||
std::size_t end = 0;
|
||||
std::optional<JsonFailure> failure;
|
||||
if (s.starts_with(U'\uFEFF')) {
|
||||
failure = JsonFailure{"Unexpected UTF-8 BOM (decode using utf-8-sig)", 0};
|
||||
} else {
|
||||
failure = scanner.Value(start, end);
|
||||
}
|
||||
if (!failure) {
|
||||
while (end < s.size() && IsJsonSpace(s[end])) ++end;
|
||||
if (end != s.size()) failure = JsonFailure{"Extra data", end};
|
||||
}
|
||||
if (!failure) return std::nullopt;
|
||||
const std::size_t pos = failure->position;
|
||||
const std::u32string_view before = std::u32string_view(s).substr(0, pos);
|
||||
const auto line = std::ranges::count(before, U'\n') + 1;
|
||||
const std::size_t lastNewline = before.rfind(U'\n');
|
||||
const std::size_t column = lastNewline == std::u32string_view::npos ? pos + 1 : pos - lastNewline;
|
||||
return std::format("{}: line {} column {} (char {})", failure->message, line, column, pos);
|
||||
}
|
||||
|
||||
Json LoadJsonFile(const std::filesystem::path& path) {
|
||||
const std::string text = ReadText(path);
|
||||
if (const std::optional<std::string> error = JsonDecodeError(text)) throw Error("JSONDecodeError", *error);
|
||||
return ParseLoose(text);
|
||||
}
|
||||
|
||||
std::string UniversalNewlines(std::string_view text) {
|
||||
if (!text.contains('\r')) return std::string(text);
|
||||
std::string out;
|
||||
|
|
@ -933,6 +1261,29 @@ namespace Crafter::Review::Dyn {
|
|||
return parts;
|
||||
}
|
||||
|
||||
std::string UrlUnsplit(std::string_view scheme, std::string_view netloc, std::string_view path, std::string_view query, std::string_view fragment) {
|
||||
// The schemes that get `//` before an empty netloc.
|
||||
constexpr std::array<std::string_view, 27> UsesNetloc = {
|
||||
"", "ftp", "http", "gopher", "nntp", "telnet", "imap", "wais", "file", "mms", "https", "shttp", "snews", "prospero",
|
||||
"rtsp", "rtsps", "rtspu", "rsync", "svn", "svn+ssh", "sftp", "nfs", "git", "git+ssh", "ws", "wss", "itms-services",
|
||||
};
|
||||
bool hasNetloc = !netloc.empty();
|
||||
if (!hasNetloc && !scheme.empty() && std::ranges::contains(UsesNetloc, scheme) && (path.empty() || path.starts_with('/'))) {
|
||||
hasNetloc = true;
|
||||
}
|
||||
std::string url(path);
|
||||
if (hasNetloc) {
|
||||
if (!url.empty() && !url.starts_with('/')) url.insert(0, "/");
|
||||
url = std::format("//{}{}", netloc, url);
|
||||
} else if (url.starts_with("//")) {
|
||||
url.insert(0, "//");
|
||||
}
|
||||
if (!scheme.empty()) url = std::format("{}:{}", scheme, url);
|
||||
if (!query.empty()) url = std::format("{}?{}", url, query);
|
||||
if (!fragment.empty()) url = std::format("{}#{}", url, fragment);
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string Unquote(std::string_view text) {
|
||||
if (!text.contains('%')) return std::string(text);
|
||||
// Percent sequences become bytes; runs of bytes are decoded as UTF-8
|
||||
|
|
@ -1058,4 +1409,203 @@ namespace Crafter::Review::Dyn {
|
|||
std::string ErrnoText(std::int32_t code) {
|
||||
return std::format("[Errno {}] {}", code, std::strerror(code));
|
||||
}
|
||||
|
||||
Error::Error(std::string_view kind, const std::string& message) : std::runtime_error(message), kind(kind) {}
|
||||
|
||||
const std::string& Error::Kind() const {
|
||||
return kind;
|
||||
}
|
||||
|
||||
bool Truthy(const Json& value) {
|
||||
switch (value.type()) {
|
||||
case Json::value_t::null:
|
||||
return false;
|
||||
case Json::value_t::boolean:
|
||||
return value.get<bool>();
|
||||
case Json::value_t::number_integer:
|
||||
return value.get<std::int64_t>() != 0;
|
||||
case Json::value_t::number_unsigned:
|
||||
return value.get<std::uint64_t>() != 0;
|
||||
case Json::value_t::number_float:
|
||||
// NaN is true: it is not 0.0.
|
||||
return value.get<double>() != 0.0;
|
||||
case Json::value_t::string:
|
||||
return !value.get_ref<const std::string&>().empty();
|
||||
case Json::value_t::array:
|
||||
case Json::value_t::object:
|
||||
return !value.empty();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const Json& Or(const Json& value, const Json& fallback) {
|
||||
return Truthy(value) ? value : fallback;
|
||||
}
|
||||
|
||||
const Json& EmptyDict() {
|
||||
static const Json empty = Json::object();
|
||||
return empty;
|
||||
}
|
||||
|
||||
const Json& EmptyList() {
|
||||
static const Json empty = Json::array();
|
||||
return empty;
|
||||
}
|
||||
|
||||
bool Has(const Json& dict, std::string_view key) {
|
||||
return dict.is_object() && dict.find(key) != dict.end();
|
||||
}
|
||||
|
||||
const Json& Get(const Json& dict, std::string_view key) {
|
||||
static const Json none;
|
||||
if (!dict.is_object()) throw Error("AttributeError", std::format("'{}' object has no attribute 'get'", TypeName(dict)));
|
||||
const auto found = dict.find(key);
|
||||
return found == dict.end() ? none : *found;
|
||||
}
|
||||
|
||||
const Json& At(const Json& dict, std::string_view key) {
|
||||
if (dict.is_object()) {
|
||||
const auto found = dict.find(key);
|
||||
if (found == dict.end()) throw Error("KeyError", Repr(key));
|
||||
return *found;
|
||||
}
|
||||
if (dict.is_array()) throw Error("TypeError", "list indices must be integers or slices, not str");
|
||||
if (dict.is_string()) throw Error("TypeError", "string indices must be integers, not 'str'");
|
||||
throw Error("TypeError", std::format("'{}' object is not subscriptable", TypeName(dict)));
|
||||
}
|
||||
|
||||
std::string_view TypeName(const Json& value) {
|
||||
switch (value.type()) {
|
||||
case Json::value_t::null:
|
||||
return "NoneType";
|
||||
case Json::value_t::boolean:
|
||||
return "bool";
|
||||
case Json::value_t::number_integer:
|
||||
case Json::value_t::number_unsigned:
|
||||
return "int";
|
||||
case Json::value_t::number_float:
|
||||
return "float";
|
||||
case Json::value_t::string:
|
||||
return "str";
|
||||
case Json::value_t::array:
|
||||
return "list";
|
||||
case Json::value_t::object:
|
||||
return "dict";
|
||||
default:
|
||||
return "bytes";
|
||||
}
|
||||
}
|
||||
|
||||
bool IsInt(const Json& value) {
|
||||
return value.is_boolean() || value.is_number_integer();
|
||||
}
|
||||
|
||||
bool IsNumber(const Json& value) {
|
||||
return value.is_boolean() || value.is_number();
|
||||
}
|
||||
|
||||
std::string Str(const Json& value) {
|
||||
if (value.is_string()) return value.get<std::string>();
|
||||
return ReprValue(value);
|
||||
}
|
||||
|
||||
std::string StrOr(const Json& value, std::string_view fallback) {
|
||||
return Truthy(value) ? Str(value) : std::string(fallback);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// An int of a JSON value: a bool is 0 or 1. Unsigned values beyond
|
||||
// the signed range keep their own type in Equal().
|
||||
std::int64_t IntOf(const Json& value) {
|
||||
if (value.is_boolean()) return value.get<bool>() ? 1 : 0;
|
||||
if (value.is_number_unsigned()) return static_cast<std::int64_t>(value.get<std::uint64_t>());
|
||||
return value.get<std::int64_t>();
|
||||
}
|
||||
|
||||
// An integer equals a float exactly, never by rounding the integer to
|
||||
// a double.
|
||||
bool IntEqualsFloat(const Json& integer, double f) {
|
||||
if (!std::isfinite(f) || f != std::floor(f)) return false;
|
||||
if (integer.is_number_unsigned() && integer.get<std::uint64_t>() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
|
||||
if (f < 0.0 || f >= 18446744073709551616.0) return false;
|
||||
return static_cast<std::uint64_t>(f) == integer.get<std::uint64_t>();
|
||||
}
|
||||
if (f < -9223372036854775808.0 || f >= 9223372036854775808.0) return false;
|
||||
return static_cast<std::int64_t>(f) == IntOf(integer);
|
||||
}
|
||||
}
|
||||
|
||||
bool Equal(const Json& a, const Json& b) {
|
||||
if (IsNumber(a) && IsNumber(b)) {
|
||||
if (a.is_number_float() && b.is_number_float()) return a.get<double>() == b.get<double>();
|
||||
if (a.is_number_float()) return IntEqualsFloat(b, a.get<double>());
|
||||
if (b.is_number_float()) return IntEqualsFloat(a, b.get<double>());
|
||||
const bool bigA = a.is_number_unsigned() && a.get<std::uint64_t>() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
|
||||
const bool bigB = b.is_number_unsigned() && b.get<std::uint64_t>() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
|
||||
if (bigA || bigB) return bigA && bigB && a.get<std::uint64_t>() == b.get<std::uint64_t>();
|
||||
return IntOf(a) == IntOf(b);
|
||||
}
|
||||
if (a.type() != b.type()) return false;
|
||||
if (a.is_array()) {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (std::size_t i = 0; i < a.size(); ++i) {
|
||||
if (!Equal(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (a.is_object()) {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (auto it = a.begin(); it != a.end(); ++it) {
|
||||
const auto other = b.find(it.key());
|
||||
if (other == b.end() || !Equal(it.value(), *other)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
bool In(const Json& item, const Json& container) {
|
||||
if (container.is_array()) {
|
||||
return std::ranges::any_of(container, [&](const Json& element) { return Equal(item, element); });
|
||||
}
|
||||
if (container.is_object()) {
|
||||
if (item.is_array() || item.is_object()) throw Error("TypeError", std::format("unhashable type: '{}'", TypeName(item)));
|
||||
return item.is_string() && container.find(item.get_ref<const std::string&>()) != container.end();
|
||||
}
|
||||
if (container.is_string()) {
|
||||
if (!item.is_string()) throw Error("TypeError", std::format("'in <string>' requires string as left operand, not {}", TypeName(item)));
|
||||
return container.get_ref<const std::string&>().contains(item.get_ref<const std::string&>());
|
||||
}
|
||||
throw Error("TypeError", std::format("argument of type '{}' is not a container or iterable", TypeName(container)));
|
||||
}
|
||||
|
||||
Json Add(const Json& a, const Json& b) {
|
||||
if (!IsNumber(a) || !IsNumber(b)) {
|
||||
throw Error("TypeError", std::format("unsupported operand type(s) for +: '{}' and '{}'", TypeName(a), TypeName(b)));
|
||||
}
|
||||
if (a.is_number_float() || b.is_number_float()) {
|
||||
auto asDouble = [](const Json& value) { return value.is_number_float() ? value.get<double>() : static_cast<double>(IntOf(value)); };
|
||||
return Json(asDouble(a) + asDouble(b));
|
||||
}
|
||||
return Json(IntOf(a) + IntOf(b));
|
||||
}
|
||||
|
||||
double Sum(std::span<const double> values) {
|
||||
if (values.empty()) return 0.0;
|
||||
// sum() starts from the int 0; 0 + x is x (and 0.0 for -0.0).
|
||||
double total = 0.0 + values.front();
|
||||
double compensation = 0.0;
|
||||
for (const double x : values.subspan(1)) {
|
||||
const double t = total + x;
|
||||
if (std::fabs(total) >= std::fabs(x)) {
|
||||
compensation += (total - t) + x;
|
||||
} else {
|
||||
compensation += (x - t) + total;
|
||||
}
|
||||
total = t;
|
||||
}
|
||||
if (compensation != 0.0 && std::isfinite(compensation)) total += compensation;
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue