Skip to documentation
SLOP

tiny.preserves.text_reader

Reference tiny.preserves text_reader

Defined in tiny.preserves.

Reads values from text, whatever the type of their embedded values.

API (6)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.preservestext reader
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callsprivate sourcelib.preserves.src.text.readerreadBareTokentext_readerlooksLikeFloat
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.preserves.src.text.readerreadBareTokentext_readerlooksLikeInteger
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.preserves.src.text.readerreadBareTokenprivate sourcelib.preserves.src.text.readerparseDecimalIntoBigtext_readerparseDecimalIntoSignedInteger
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallstest sourcelib.preserves.src.text.testtest: generic text reader leaves the ...private sourcelib.preserves.src.text.readerreadValueAttext_readerreadValue
Static calls · unresolved targets: 0 · external targets: 1.

Source: lib/preserves/src/root.zig:124

zig
pub const text_reader = text.reader;

Source: lib/preserves/src/text/reader.zig

zig
//! Reads values from text, whatever the type of their embedded values. A caller needs a text reader//! for its own value type, whatever type its embedded values have. The caller also needs a reader//! that checks every string and symbol as UTF-8 and reads JSON-style surrogate-pair escapes. The//! type of the embedded values decides how an embedded value is spelled in text, and the reader//! knows only what that type tells it. Annotations can hold any value, including embedded values//! the caller's type does not read.//!//! `#:` reads an embedded value through its type's `decodeText`, and fails with//! `EmbeddedNotSupported` when the type lacks one, as `NoEmbedded` and `AnyEmbedded` both do. The//! reader reads each annotation with its own private type for embedded values, checks it like any//! value, then frees it, so `@a v` returns `v`. Sets and dictionaries keep the order the text lists//! them in, after the repeated-item check. The reader copies every string and symbol it keeps, so//! the value owns all its memory and borrows nothing from the text.const std = @import("std");const Allocator = std.mem.Allocator;const preserves = @import("../root.zig");const nesting = @import("nesting.zig");const value_mod = preserves.value;const integer_mod = preserves.integer_mod;/// The errors `decode` and `readValue` return. Code that calls `decode` or `readValue` switches on/// these errors. `UnexpectedEof`: the text ends before a value, inside a string, escape or byte/// string, or before a closing bracket. `UnexpectedTrailingBytes`: `decode` finds anything other/// than whitespace and commas after the value. `BadSyntax`: a delimiter at the start of a value, an/// unknown `#` form, a dictionary key followed by something other than `:`, or a `#xd"…"` double/// not closed after 16 digits. `BadEscape`: an unknown backslash escape, or a `\u` escape that/// names a lone surrogate. `BadHex`: a character other than a hexadecimal digit in `\u`, `\x`,/// `#x"…"` or `#xd"…"`. `BadBase64`: a `#[…]` byte string with invalid base64. `BadNumber`: a token/// shaped like a double that the standard parser rejects. `InvalidUtf8`: a string, quoted symbol or/// bare symbol with invalid UTF-8. `EmbeddedNotSupported`: a `#:` value outside an annotation when/// the type of the embedded values lacks `decodeText`. `NonCanonicalInteger` is listed, and the/// reader never returns it. `DuplicateSetElement` and `DuplicateDictionaryKey`: a set with two/// equal elements, or a dictionary with two equal keys. `NestingLimitExceeded`: values nested more/// than 256 levels deep.pub const DecodeError = Allocator.Error || error{    UnexpectedEof,    UnexpectedTrailingBytes,    BadSyntax,    BadEscape,    BadHex,    BadBase64,    BadNumber,    InvalidUtf8,    EmbeddedNotSupported,    NonCanonicalInteger,    DuplicateSetElement,    DuplicateDictionaryKey,    NestingLimitExceeded,};const AnnotationEmbedded = struct {    value: *value_mod.Value(AnnotationEmbedded),    pub fn eql(a: AnnotationEmbedded, b: AnnotationEmbedded) bool {        return a.value.*.eql(b.value.*);    }    pub fn hash(self: AnnotationEmbedded) u64 {        return self.value.*.hash();    }    pub fn order(a: AnnotationEmbedded, b: AnnotationEmbedded) std.math.Order {        return a.value.*.compare(b.value.*);    }    pub fn deinit(self: *AnnotationEmbedded, allocator: Allocator) void {        self.value.deinit(allocator);        allocator.destroy(self.value);    }    pub fn clone(        self: AnnotationEmbedded,        allocator: Allocator,    ) Allocator.Error!AnnotationEmbedded {        const V = value_mod.Value(AnnotationEmbedded);        const cloned = try allocator.create(V);        errdefer allocator.destroy(cloned);        cloned.* = try preserves.ownership.cloneValueDeep(            AnnotationEmbedded,            allocator,            self.value.*,        );        return .{ .value = cloned };    }};fn decodeAnnotationEmbedded(    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!AnnotationEmbedded {    const V = value_mod.Value(AnnotationEmbedded);    var value = try readValueAt(AnnotationEmbedded, allocator, text, index, level);    errdefer value.deinit(allocator);    const owned = try allocator.create(V);    owned.* = value;    return .{ .value = owned };}/// Returns the one value `text` holds, allocated with `allocator`. Code that reads a whole data/// file calls it, so each file yields one owned value. The value's embedded values have the type/// `D`. Whitespace and commas separate values. `decode` reads no comments, so `# ` fails as an/// unknown `#` form. A bare token that is all digits, with an optional sign, becomes an integer of/// any size, a token in decimal or exponent form becomes a double, and any other token becomes a/// symbol.////// The value owns all its memory, so `text` may be freed at once, and the caller frees the value/// with `deinit`. On any error the call frees everything it allocated. Its repeated-item checks/// compare each element or key with every earlier one, so their cost grows with the square of the/// count.pub fn decode(comptime D: type, allocator: Allocator, text: []const u8) DecodeError!value_mod.Value(D) {    var index: usize = 0;    var v = try readValueAt(D, allocator, text, &index, nesting.root);    errdefer v.deinit(allocator);    skipTrailing(text, &index);    if (index != text.len) return error.UnexpectedTrailingBytes;    return v;}/// Reads one value from `text` at `index.*` and advances `index` past it. Code reading more than/// one value from one text calls it once per value, for each value and the position after it. The/// call leaves any text after the value unread and reports no trailing content. On/// `NestingLimitExceeded`, `index` stops at the opening bracket it rejected. Its value, errors and/// ownership match `decode`.pub fn readValue(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,) DecodeError!value_mod.Value(D) {    return readValueAt(D, allocator, text, index, nesting.root);}fn readValueAt(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!value_mod.Value(D) {    const value_level = try skipAnnotations(allocator, text, index, level);    skipWhitespaceAndCommas(text, index);    if (index.* >= text.len) return error.UnexpectedEof;    const c = text[index.*];    return switch (c) {        '"' => blk: {            index.* += 1;            const bytes = try readStringLiteral(allocator, text, index, '"');            if (!std.unicode.utf8ValidateSlice(bytes)) {                allocator.free(bytes);                break :blk error.InvalidUtf8;            }            break :blk .{ .string = bytes };        },        '\'' => blk: {            index.* += 1;            const bytes = try readStringLiteral(allocator, text, index, '\'');            if (!std.unicode.utf8ValidateSlice(bytes)) {                allocator.free(bytes);                break :blk error.InvalidUtf8;            }            break :blk .{ .symbol = bytes };        },        '<' => blk: {            const nested = try nesting.descend(value_level);            index.* += 1;            break :blk try readRecord(                D,                allocator,                text,                index,                nested,            );        },        '[' => blk: {            const nested = try nesting.descend(value_level);            index.* += 1;            break :blk try readSequence(                D,                allocator,                text,                index,                nested,            );        },        '{' => blk: {            const nested = try nesting.descend(value_level);            index.* += 1;            break :blk try readDictionary(                D,                allocator,                text,                index,                nested,            );        },        '#' => try readHashForm(D, allocator, text, index, value_level),        else => try readBareToken(D, allocator, text, index),    };}fn readHashForm(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!value_mod.Value(D) {    if (index.* + 1 >= text.len) return error.UnexpectedEof;    const next = text[index.* + 1];    switch (next) {        't' => {            index.* += 2;            return value_mod.Value(D).initBoolean(true);        },        'f' => {            index.* += 2;            return value_mod.Value(D).initBoolean(false);        },        '{' => {            const nested = try nesting.descend(level);            index.* += 2;            return try readSet(                D,                allocator,                text,                index,                nested,            );        },        '"' => {            index.* += 2;            return try readLiteralByteString(D, allocator, text, index);        },        '[' => {            index.* += 2;            return try readBase64ByteString(D, allocator, text, index);        },        ':' => {            const embedded_level = try nesting.descend(level);            index.* += 2;            if (D == AnnotationEmbedded) {                const d = try decodeAnnotationEmbedded(                    allocator,                    text,                    index,                    embedded_level,                );                return value_mod.Value(D).initEmbedded(d);            }            if (!@hasDecl(D, "decodeText")) return error.EmbeddedNotSupported;            const d = try D.decodeText(allocator, text, index);            return value_mod.Value(D).initEmbedded(d);        },        'x' => {            if (index.* + 2 >= text.len) return error.UnexpectedEof;            const follow = text[index.* + 2];            if (follow == '"') {                index.* += 3;                return try readHexByteString(D, allocator, text, index);            } else if (follow == 'd') {                if (index.* + 3 >= text.len or text[index.* + 3] != '"') return error.BadSyntax;                index.* += 4;                return try readHexDouble(D, text, index);            } else return error.BadSyntax;        },        else => return error.BadSyntax,    }}fn readRecord(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    skipWhitespaceAndCommas(text, index);    var label_value = try readValueAt(D, allocator, text, index, level);    errdefer label_value.deinit(allocator);    var fields: std.ArrayListUnmanaged(V) = .empty;    errdefer {        for (fields.items) |*f| f.deinit(allocator);        fields.deinit(allocator);    }    while (true) {        skipWhitespaceAndCommas(text, index);        if (index.* >= text.len) return error.UnexpectedEof;        if (text[index.*] == '>') {            index.* += 1;            break;        }        var f = try readValueAt(D, allocator, text, index, level);        errdefer f.deinit(allocator);        try fields.append(allocator, f);    }    const owned = try fields.toOwnedSlice(allocator);    errdefer {        for (owned) |*field| field.deinit(allocator);        allocator.free(owned);    }    return try V.initRecord(allocator, label_value, owned);}fn readSequence(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    var items: std.ArrayListUnmanaged(V) = .empty;    errdefer {        for (items.items) |*it| it.deinit(allocator);        items.deinit(allocator);    }    while (true) {        skipWhitespaceAndCommas(text, index);        if (index.* >= text.len) return error.UnexpectedEof;        if (text[index.*] == ']') {            index.* += 1;            break;        }        var it = try readValueAt(D, allocator, text, index, level);        errdefer it.deinit(allocator);        try items.append(allocator, it);    }    return .{ .sequence = try items.toOwnedSlice(allocator) };}fn readSet(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    var items: std.ArrayListUnmanaged(V) = .empty;    errdefer {        for (items.items) |*it| it.deinit(allocator);        items.deinit(allocator);    }    while (true) {        skipWhitespaceAndCommas(text, index);        if (index.* >= text.len) return error.UnexpectedEof;        if (text[index.*] == '}') {            index.* += 1;            break;        }        var it = try readValueAt(D, allocator, text, index, level);        errdefer it.deinit(allocator);        if (V.setContainsElement(items.items, it)) return error.DuplicateSetElement;        try items.append(allocator, it);    }    return .{ .set = try items.toOwnedSlice(allocator) };}fn readDictionary(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    var entries: std.ArrayListUnmanaged(V.DictionaryEntry) = .empty;    errdefer {        for (entries.items) |*e| {            e.key.deinit(allocator);            e.value.deinit(allocator);        }        entries.deinit(allocator);    }    while (true) {        skipWhitespaceAndCommas(text, index);        if (index.* >= text.len) return error.UnexpectedEof;        if (text[index.*] == '}') {            index.* += 1;            break;        }        var k = try readValueAt(D, allocator, text, index, level);        errdefer k.deinit(allocator);        skipWhitespaceAndCommas(text, index);        if (index.* >= text.len or text[index.*] != ':') return error.BadSyntax;        index.* += 1;        skipWhitespaceAndCommas(text, index);        var v = try readValueAt(D, allocator, text, index, level);        errdefer v.deinit(allocator);        if (V.dictionaryContainsKey(entries.items, k)) {            return error.DuplicateDictionaryKey;        }        try entries.append(allocator, .{ .key = k, .value = v });    }    return .{ .dictionary = try entries.toOwnedSlice(allocator) };}fn readStringLiteral(    allocator: Allocator,    text: []const u8,    index: *usize,    terminator: u8,) DecodeError![]u8 {    var buf: std.ArrayListUnmanaged(u8) = .empty;    errdefer buf.deinit(allocator);    while (true) {        if (index.* >= text.len) return error.UnexpectedEof;        const c = text[index.*];        index.* += 1;        if (c == terminator) break;        if (c == '\\') {            if (index.* >= text.len) return error.UnexpectedEof;            const esc = text[index.*];            index.* += 1;            switch (esc) {                '\\' => try buf.append(allocator, '\\'),                '/' => try buf.append(allocator, '/'),                'b' => try buf.append(allocator, 0x08),                'f' => try buf.append(allocator, 0x0c),                'n' => try buf.append(allocator, 0x0a),                'r' => try buf.append(allocator, 0x0d),                't' => try buf.append(allocator, 0x09),                'u' => try readUnicodeEscape(allocator, &buf, text, index),                else => {                    if (esc == terminator) {                        try buf.append(allocator, esc);                    } else return error.BadEscape;                },            }        } else {            try buf.append(allocator, c);        }    }    return try buf.toOwnedSlice(allocator);}fn readUnicodeEscape(    allocator: Allocator,    buf: *std.ArrayListUnmanaged(u8),    text: []const u8,    index: *usize,) DecodeError!void {    const n1 = try readHex4(text, index);    var codepoint: u32 = n1;    if (n1 >= 0xD800 and n1 <= 0xDBFF) {        if (index.* + 2 > text.len or text[index.*] != '\\' or text[index.* + 1] != 'u') {            return error.BadEscape;        }        index.* += 2;        const n2 = try readHex4(text, index);        if (n2 < 0xDC00 or n2 > 0xDFFF) return error.BadEscape;        codepoint = ((n1 - 0xD800) << 10) + (n2 - 0xDC00) + 0x10000;    } else if (n1 >= 0xDC00 and n1 <= 0xDFFF) {        return error.BadEscape;    }    var utf8: [4]u8 = undefined;    const written = std.unicode.utf8Encode(@intCast(codepoint), &utf8) catch return error.BadEscape;    try buf.appendSlice(allocator, utf8[0..written]);}fn readHex4(text: []const u8, index: *usize) DecodeError!u32 {    if (index.* + 4 > text.len) return error.UnexpectedEof;    var v: u32 = 0;    var i: usize = 0;    while (i < 4) : (i += 1) {        const d = hexDigit(text[index.*]) orelse return error.BadHex;        v = (v << 4) | d;        index.* += 1;    }    return v;}fn readHexByteString(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,) DecodeError!value_mod.Value(D) {    var buf: std.ArrayListUnmanaged(u8) = .empty;    errdefer buf.deinit(allocator);    while (true) {        skipWhitespaceOnly(text, index);        if (index.* >= text.len) return error.UnexpectedEof;        const c = text[index.*];        if (c == '"') {            index.* += 1;            break;        }        index.* += 1;        if (index.* >= text.len) return error.UnexpectedEof;        const c2 = text[index.*];        index.* += 1;        const h1 = hexDigit(c) orelse return error.BadHex;        const h2 = hexDigit(c2) orelse return error.BadHex;        try buf.append(allocator, @intCast((h1 << 4) | h2));    }    return .{ .byte_string = try buf.toOwnedSlice(allocator) };}fn readHexDouble(    comptime D: type,    text: []const u8,    index: *usize,) DecodeError!value_mod.Value(D) {    var bytes: [8]u8 = undefined;    var filled: usize = 0;    while (filled < 8) {        skipWhitespaceOnly(text, index);        if (index.* + 1 >= text.len) return error.UnexpectedEof;        const c1 = text[index.*];        const c2 = text[index.* + 1];        const h1 = hexDigit(c1) orelse return error.BadHex;        const h2 = hexDigit(c2) orelse return error.BadHex;        bytes[filled] = @intCast((h1 << 4) | h2);        filled += 1;        index.* += 2;    }    skipWhitespaceOnly(text, index);    if (index.* >= text.len or text[index.*] != '"') return error.BadSyntax;    index.* += 1;    const bits = std.mem.readInt(u64, &bytes, .big);    return value_mod.Value(D).initDouble(@bitCast(bits));}fn readLiteralByteString(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,) DecodeError!value_mod.Value(D) {    var buf: std.ArrayListUnmanaged(u8) = .empty;    errdefer buf.deinit(allocator);    while (true) {        if (index.* >= text.len) return error.UnexpectedEof;        const c = text[index.*];        index.* += 1;        if (c == '"') break;        if (c == '\\') {            if (index.* >= text.len) return error.UnexpectedEof;            const esc = text[index.*];            index.* += 1;            switch (esc) {                '\\' => try buf.append(allocator, '\\'),                '"' => try buf.append(allocator, '"'),                '/' => try buf.append(allocator, '/'),                'b' => try buf.append(allocator, 0x08),                'f' => try buf.append(allocator, 0x0c),                'n' => try buf.append(allocator, 0x0a),                'r' => try buf.append(allocator, 0x0d),                't' => try buf.append(allocator, 0x09),                'x' => {                    if (index.* + 2 > text.len) return error.UnexpectedEof;                    const h1 = hexDigit(text[index.*]) orelse return error.BadHex;                    const h2 = hexDigit(text[index.* + 1]) orelse return error.BadHex;                    index.* += 2;                    try buf.append(allocator, @intCast((h1 << 4) | h2));                },                else => return error.BadEscape,            }        } else {            try buf.append(allocator, c);        }    }    return .{ .byte_string = try buf.toOwnedSlice(allocator) };}fn readBase64ByteString(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,) DecodeError!value_mod.Value(D) {    var enc_buf: std.ArrayListUnmanaged(u8) = .empty;    defer enc_buf.deinit(allocator);    while (true) {        skipWhitespaceOnly(text, index);        if (index.* >= text.len) return error.UnexpectedEof;        var c = text[index.*];        if (c == ']') {            index.* += 1;            break;        }        index.* += 1;        if (c == '=') continue;        if (c == '-') c = '+';        if (c == '_') c = '/';        try enc_buf.append(allocator, c);    }    const decoder = std.base64.standard_no_pad.Decoder;    const expected = decoder.calcSizeForSlice(enc_buf.items) catch return error.BadBase64;    const dst = try allocator.alloc(u8, expected);    errdefer allocator.free(dst);    decoder.decode(dst, enc_buf.items) catch {        return error.BadBase64;    };    return .{ .byte_string = dst };}fn readBareToken(    comptime D: type,    allocator: Allocator,    text: []const u8,    index: *usize,) DecodeError!value_mod.Value(D) {    const start = index.*;    while (index.* < text.len and !isDelimiter(text[index.*])) {        index.* += 1;    }    const tok = text[start..index.*];    if (tok.len == 0) return error.BadSyntax;    if (looksLikeFloat(tok)) {        const f = std.fmt.parseFloat(f64, tok) catch return error.BadNumber;        return value_mod.Value(D).initDouble(f);    }    if (looksLikeInteger(tok)) {        const si = try parseDecimalIntoSignedInteger(allocator, tok);        return value_mod.Value(D).initSignedInteger(si);    }    if (!std.unicode.utf8ValidateSlice(tok)) return error.InvalidUtf8;    const owned = try allocator.dupe(u8, tok);    return .{ .symbol = owned };}fn isDelimiter(c: u8) bool {    return switch (c) {        ' ', '\t', '\r', '\n', ',', '(', ')', '{', '}', '[', ']', '<', '>', '"', '\'', ';', '@', '#', ':' => true,        else => false,    };}fn skipWhitespaceOnly(text: []const u8, index: *usize) void {    while (index.* < text.len) {        switch (text[index.*]) {            ' ', '\t', '\r', '\n' => index.* += 1,            else => return,        }    }}fn skipWhitespaceAndCommas(text: []const u8, index: *usize) void {    while (index.* < text.len) {        switch (text[index.*]) {            ' ', '\t', '\r', '\n', ',' => index.* += 1,            else => return,        }    }}fn skipTrailing(text: []const u8, index: *usize) void {    skipWhitespaceAndCommas(text, index);}fn skipAnnotations(    allocator: Allocator,    text: []const u8,    index: *usize,    level: nesting.Level,) DecodeError!nesting.Level {    var value_level = level;    while (true) {        skipWhitespaceAndCommas(text, index);        if (index.* >= text.len or text[index.*] != '@') return value_level;        const nested = try nesting.descend(value_level);        index.* += 1;        value_level = nested;        var annotation = try readValueAt(            AnnotationEmbedded,            allocator,            text,            index,            value_level,        );        annotation.deinit(allocator);    }}fn hexDigit(c: u8) ?u32 {    return switch (c) {        '0'...'9' => @as(u32, c - '0'),        'a'...'f' => @as(u32, c - 'a' + 10),        'A'...'F' => @as(u32, c - 'A' + 10),        else => null,    };}/// The predicate accepts an input consisting of an optional `+` or `-` followed by one or more/// ASCII decimal digits. It rejects an empty input, a sign alone, and underscores. The public/// `parse` and `decode` readers call this predicate before decimal integer conversion. The text/// writer quotes a symbol that the predicate accepts, because a matching symbol takes the quoted/// branch when `looksLikeNumber` recognizes it.pub fn looksLikeInteger(s: []const u8) bool {    if (s.len == 0) return false;    var i: usize = 0;    if (s[i] == '-' or s[i] == '+') i += 1;    if (i >= s.len) return false;    const digit_start = i;    while (i < s.len) : (i += 1) {        if (s[i] < '0' or s[i] > '9') return false;    }    return i > digit_start;}/// Accepts an optional `+` or `-`, one or more ASCII decimal digits, and a fraction or exponent or/// both, so `.5` fails. A dot requires at least one following digit, so `5.` fails. An `e` or `E`/// exponent accepts an optional sign and requires at least one digit. The predicate consumes the/// whole token, so `1_2e3` fails.////// The public `parse` reader calls this predicate before trying `std.fmt.parseFloat`, then falls/// through to a symbol if conversion fails. The public `decode` reader calls it before float/// conversion and reports `BadNumber` if conversion fails.////// The text writer quotes a symbol that this predicate accepts, because a matching symbol takes the/// quoted branch when `looksLikeNumber` recognizes it.pub fn looksLikeFloat(s: []const u8) bool {    if (s.len == 0) return false;    var i: usize = 0;    if (s[i] == '-' or s[i] == '+') i += 1;    if (i >= s.len or s[i] < '0' or s[i] > '9') return false;    while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1;    var has_decimal_part = false;    if (i < s.len and s[i] == '.') {        i += 1;        const frac_start = i;        while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1;        if (i > frac_start) has_decimal_part = true else return false;    }    var has_exp = false;    if (i < s.len and (s[i] == 'e' or s[i] == 'E')) {        i += 1;        if (i < s.len and (s[i] == '-' or s[i] == '+')) i += 1;        const exp_start = i;        while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1;        if (i > exp_start) has_exp = true else return false;    }    return i == s.len and (has_decimal_part or has_exp);}/// Returns the integer the decimal string `s` spells, with an optional leading `+` or `-`. `parse`/// and `decode` call it only for tokens accepted by `looksLikeInteger`, which permits an optional/// sign followed by digits. A value that fits 128 bits signed or unsigned is stored inline, and a/// wider one as big-endian two's-complement bytes allocated with `allocator`. Inside the 128-bit/// range, underscores between digits are skipped, so `1_000` returns 1000. The call returns/// `error.BadNumber` for an empty string, a sign alone, or a wide value holding a character other/// than a digit.pub fn parseDecimalIntoSignedInteger(allocator: Allocator, s: []const u8) DecodeError!integer_mod.SignedInteger {    if (s.len == 0) return error.BadNumber;    if (std.fmt.parseInt(i128, s, 10)) |v| {        return integer_mod.SignedInteger.fromI128(v);    } else |_| {}    if (std.fmt.parseInt(u128, s, 10)) |v| {        return integer_mod.SignedInteger.fromU128(v);    } else |_| {}    return try parseDecimalIntoBig(allocator, s);}fn parseDecimalIntoBig(allocator: Allocator, s: []const u8) DecodeError!integer_mod.SignedInteger {    var i: usize = 0;    var is_negative = false;    if (s[i] == '-') {        is_negative = true;        i += 1;    } else if (s[i] == '+') i += 1;    if (i >= s.len) return error.BadNumber;    var mag: std.ArrayListUnmanaged(u8) = .empty;    defer mag.deinit(allocator);    try mag.append(allocator, 0);    while (i < s.len) : (i += 1) {        const d = s[i];        if (d < '0' or d > '9') return error.BadNumber;        const digit: u8 = @intCast(d - '0');        var carry: u16 = digit;        var j: usize = mag.items.len;        while (j > 0) {            j -= 1;            const product = @as(u16, mag.items[j]) * 10 + carry;            mag.items[j] = @intCast(product & 0xff);            carry = product >> 8;        }        while (carry > 0) {            try mag.insert(allocator, 0, @intCast(carry & 0xff));            carry >>= 8;        }    }    var start: usize = 0;    while (start < mag.items.len - 1 and mag.items[start] == 0) start += 1;    const stripped = mag.items[start..];    if (is_negative) {        const buf = try allocator.alloc(u8, stripped.len + 1);        defer allocator.free(buf);        buf[0] = 0x00;        @memcpy(buf[1..], stripped);        var k: usize = buf.len;        var carry: u16 = 1;        while (k > 0) {            k -= 1;            const inv: u16 = @as(u16, ~buf[k]) & 0xff;            const sum = inv + carry;            buf[k] = @intCast(sum & 0xff);            carry = sum >> 8;        }        const canonical = try canonicalizeBytes(allocator, buf);        defer allocator.free(canonical);        return try integer_mod.SignedInteger.fromCanonicalBytes(allocator, canonical);    } else {        if (stripped.len > 0 and (stripped[0] & 0x80) != 0) {            const buf = try allocator.alloc(u8, stripped.len + 1);            defer allocator.free(buf);            buf[0] = 0x00;            @memcpy(buf[1..], stripped);            return try integer_mod.SignedInteger.fromCanonicalBytes(allocator, buf);        }        return try integer_mod.SignedInteger.fromCanonicalBytes(allocator, stripped);    }}fn canonicalizeBytes(allocator: Allocator, bytes: []const u8) DecodeError![]const u8 {    var start: usize = 0;    while (start + 1 < bytes.len) {        const first = bytes[start];        const second = bytes[start + 1];        if (first == 0xff and (second & 0x80) != 0) {            start += 1;            continue;        }        if (first == 0x00 and (second & 0x80) == 0) {            start += 1;            continue;        }        break;    }    return try allocator.dupe(u8, bytes[start..]);}fn expectDuplicateWithAllocator(    allocator: Allocator,    text: []const u8,    expected: DecodeError,) !void {    const NE = preserves.domain.NoEmbedded;    if (decode(NE, allocator, text)) |decoded| {        var value = decoded;        value.deinit(allocator);        return error.ExpectedDuplicate;    } else |err| {        if (err == error.OutOfMemory) return err;        if (err == expected) return;        return err;    }}fn checkDuplicateAllocationFailures(allocator: Allocator) !void {    try expectDuplicateWithAllocator(        allocator,        "#{<\"label\" 'quoted'> <\"label\" 'quoted'>}",        error.DuplicateSetElement,    );    try expectDuplicateWithAllocator(        allocator,        "{\"key\": <\"label\" 'first'>, \"key\": <\"label\" 'second'>}",        error.DuplicateDictionaryKey,    );    try expectDuplicateWithAllocator(        allocator,        "@#:#{[\"owned\"] [\"owned\"]} 3",        error.DuplicateSetElement,    );}test "parse integer" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    var v = try decode(NE, allocator, "42");    defer v.deinit(allocator);    try std.testing.expectEqual(@as(i128, 42), try v.signed_integer.toI128());}test "parse negative big integer" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    var v = try decode(NE, allocator, "-170141183460469231731687303715884105729");    defer v.deinit(allocator);    try std.testing.expectEqual(integer_mod.Tier.big, @as(integer_mod.Tier, v.signed_integer.repr));}test "parse sequence with commas" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    var v = try decode(NE, allocator, "[1, 2, 3]");    defer v.deinit(allocator);    try std.testing.expectEqual(@as(usize, 3), v.sequence.len);}test "parse sequence preserves duplicate values" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    var v = try decode(NE, allocator, "[\"same\", \"same\"]");    defer v.deinit(allocator);    try std.testing.expectEqual(@as(usize, 2), v.sequence.len);    try std.testing.expect(v.sequence[0].eql(v.sequence[1]));}test "parse quoted symbol" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    var v = try decode(NE, allocator, "'hello world'");    defer v.deinit(allocator);    try std.testing.expectEqualStrings("hello world", v.symbol);}test "parse rejects duplicate set elements and releases owned values" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    try std.testing.expectError(        error.DuplicateSetElement,        decode(NE, allocator, "#{[\"owned\"] [\"owned\"]}"),    );}test "parse rejects duplicate dictionary keys and releases owned entries" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    try std.testing.expectError(        error.DuplicateDictionaryKey,        decode(NE, allocator, "{\"key\": [\"first\"], \"key\": [\"second\"]}"),    );}test "parse duplicate errors release every allocation failure path" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkDuplicateAllocationFailures,        .{},    );}test "parse skips structurally nested annotations" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    const cases = [_][]const u8{        "@[[1] 2] 3",        "@{a: {b: c}} 3",        "@<r <s>> 3",        "@#{[1] {a: 2}} 3",        "@{a: @b c} 3",        "@\"closing ] } >\" 3",        "@#xd\"3ff0000000000000\" 3",        "@#[AQI=] 3",    };    for (cases) |text| {        var value = try decode(NE, allocator, text);        defer value.deinit(allocator);        try std.testing.expectEqual(@as(i128, 3), try value.signed_integer.toI128());    }}test "parse validates discarded annotations" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    try std.testing.expectError(        error.DuplicateSetElement,        decode(NE, allocator, "@#{1 1} 2"),    );    try std.testing.expectError(        error.DuplicateDictionaryKey,        decode(NE, allocator, "@{a: 1 a: 2} 3"),    );    try std.testing.expectError(error.BadSyntax, decode(NE, allocator, "@{a} 3"));    try std.testing.expectError(error.BadEscape, decode(NE, allocator, "@\"\\q\" 3"));    try std.testing.expectError(error.BadHex, decode(NE, allocator, "@#x\"zz\" 3"));    try std.testing.expectError(error.BadBase64, decode(NE, allocator, "@#[%%%] 3"));}test "discarded annotations do not enter the application embedded domain" {    const allocator = std.testing.allocator;    const NE = preserves.domain.NoEmbedded;    var value = try decode(NE, allocator, "@#:foo 3");    defer value.deinit(allocator);    try std.testing.expectEqual(@as(i128, 3), try value.signed_integer.toI128());    try std.testing.expectError(        error.DuplicateSetElement,        decode(NE, allocator, "@#:#{1 1} 3"),    );}

Also reachable as

text.reader.

Audit

Definitions6
Public names12
Members0
Version26.7.0
Revisiondaab053ee433