tiny.preserves.text_writer
Defined in tiny.preserves.
Writes values as text, whatever the type of their embedded values.
API (3)
Actions
Public operations.
encode: Returns the text ofvalueas new bytes allocated withallocator.writeValue: Appends the text ofvaluetoout, growingoutwithallocator.
Types and contracts
Public types and contracts.
EncodeError: The errorsencodeandwriteValuereturn.
Source
Source: lib/preserves/src/root.zig:123
zig
pub const text_writer = text.writer;Source: lib/preserves/src/text/writer.zig
zig
//! Writes values as text, whatever the type of their embedded values. A caller writing a value for//! people to read, or for a program to read back, needs a spelling each text reader accepts. A//! double that is infinite or NaN has no decimal spelling. A symbol can look like a number or hold//! characters that end a bare token.//!//! Special doubles are written as their bits, `#xd"…"`, and every other double carries a `.` or//! exponent so it reads back as a double. A symbol is written bare only when every character is a//! letter, digit or one of `-~!$%^&*?_=+/.|` and it does not look like a number, and the symbol is//! quoted with `'` otherwise. Sets and dictionaries are written in the order they are stored, so//! equal values stored in different orders give different text. The writer refuses discards,//! captures, binds and rest patterns, repeated set elements or dictionary keys, and embedded values//! whose type lacks `encodeText`, as `NoEmbedded` and `AnyEmbedded` both do.const std = @import("std");const Allocator = std.mem.Allocator;const ArrayList = std.ArrayListUnmanaged(u8);const preserves = @import("../root.zig");const value_mod = preserves.value;const integer_mod = preserves.integer_mod;/// The errors `encode` and `writeValue` return. Code that calls `encode` or `writeValue` switches/// on these errors. `EmbeddedNotSupported`: the value holds an embedded value whose type lacks/// `encodeText`. `DuplicateSetElement` and `DuplicateDictionaryKey`: a set with two equal elements,/// or a dictionary with two equal keys. `PatternFormNotEncodable`: the value holds a discard,/// capture, bind or rest pattern. `OutOfMemory`: an allocation failed.pub const EncodeError = Allocator.Error || error{ EmbeddedNotSupported, DuplicateSetElement, DuplicateDictionaryKey, PatternFormNotEncodable,};/// Returns the text of `value` as new bytes allocated with `allocator`. Code that saves or prints a/// value calls it for new text the caller owns. The caller owns the bytes and frees them with/// `allocator`. On any error the call frees what it wrote.pub fn encode(comptime D: type, allocator: Allocator, value: value_mod.Value(D)) EncodeError![]u8 { var buf: ArrayList = .empty; errdefer buf.deinit(allocator); try writeValue(D, allocator, &buf, value); return buf.toOwnedSlice(allocator);}/// Appends the text of `value` to `out`, growing `out` with `allocator`. `toText` calls it for each/// atom, and code that builds one text from more than one value calls it for each. On error, the/// text appended before the failure stays in `out`. Booleans are `#t` and `#f`, and integers are/// decimal at any width. Strings are quoted with `"`, with backslash escapes for quotes,/// backslashes and control characters. Byte strings are written as unpadded base64 in `#[…]`./// Record fields and sequence and set items are separated by spaces, and dictionary entries by/// `, `. A quoted symbol escapes a `'` as `\'`, which `parse` accepts. A bare symbol can contain/// `_`, so a symbol such as `1_000` is written bare and `parse` reads it as a symbol.pub fn writeValue( comptime D: type, allocator: Allocator, out: *ArrayList, value: value_mod.Value(D),) EncodeError!void { switch (value) { .boolean => |b| try out.appendSlice(allocator, if (b) "#t" else "#f"), .double => |v| try writeDouble(allocator, out, v), .signed_integer => |si| try writeSignedInteger(allocator, out, si), .string => |s| try writeQuotedString(allocator, out, s), .byte_string => |s| try writeByteString(allocator, out, s), .symbol => |s| try writeSymbol(allocator, out, s), .record => |r| { try out.append(allocator, '<'); try writeValue(D, allocator, out, r.label.*); for (r.fields) |f| { try out.append(allocator, ' '); try writeValue(D, allocator, out, f); } try out.append(allocator, '>'); }, .sequence => |items| { try out.append(allocator, '['); for (items, 0..) |item, i| { if (i > 0) try out.append(allocator, ' '); try writeValue(D, allocator, out, item); } try out.append(allocator, ']'); }, .set => |items| { if (!value_mod.Value(D).setElementsDistinct(items)) { return error.DuplicateSetElement; } try out.appendSlice(allocator, "#{"); for (items, 0..) |item, i| { if (i > 0) try out.append(allocator, ' '); try writeValue(D, allocator, out, item); } try out.append(allocator, '}'); }, .dictionary => |entries| { if (!value_mod.Value(D).dictionaryKeysDistinct(entries)) { return error.DuplicateDictionaryKey; } try out.append(allocator, '{'); for (entries, 0..) |e, i| { if (i > 0) try out.appendSlice(allocator, ", "); try writeValue(D, allocator, out, e.key); try out.appendSlice(allocator, ": "); try writeValue(D, allocator, out, e.value); } try out.append(allocator, '}'); }, .embedded => |d| { if (!@hasDecl(D, "encodeText")) return error.EmbeddedNotSupported; try out.appendSlice(allocator, "#:"); try D.encodeText(d, allocator, out); }, .discard, .capture, .bind, .rest_pattern => return error.PatternFormNotEncodable, }}fn writeDouble(allocator: Allocator, out: *ArrayList, v: f64) EncodeError!void { if (std.math.isNan(v) or std.math.isInf(v)) { const bits: u64 = @bitCast(v); try out.appendSlice(allocator, "#xd\""); var hex: [16]u8 = undefined; _ = std.fmt.bufPrint(&hex, "{x:0>16}", .{bits}) catch unreachable; try out.appendSlice(allocator, &hex); try out.append(allocator, '"'); return; } var tmp: [384]u8 = undefined; const s = std.fmt.bufPrint(&tmp, "{d}", .{v}) catch unreachable; try out.appendSlice(allocator, s); if (std.mem.indexOfScalar(u8, s, '.') == null and std.mem.indexOfAny(u8, s, "eE") == null) { try out.appendSlice(allocator, ".0"); }}fn writeSignedInteger( allocator: Allocator, out: *ArrayList, si: integer_mod.SignedInteger,) EncodeError!void { switch (si.repr) { .i128 => |v| { var tmp: [48]u8 = undefined; const s = std.fmt.bufPrint(&tmp, "{d}", .{v}) catch unreachable; try out.appendSlice(allocator, s); }, .u128 => |v| { var tmp: [48]u8 = undefined; const s = std.fmt.bufPrint(&tmp, "{d}", .{v}) catch unreachable; try out.appendSlice(allocator, s); }, .big => |bytes| try writeBigDecimal(allocator, out, bytes), }}fn writeBigDecimal(allocator: Allocator, out: *ArrayList, bytes: []const u8) EncodeError!void { if (bytes.len == 0) { try out.append(allocator, '0'); return; } const is_negative = (bytes[0] & 0x80) != 0; var mag = try allocator.alloc(u8, bytes.len); defer allocator.free(mag); if (is_negative) { var carry: u16 = 1; var i: usize = bytes.len; while (i > 0) { i -= 1; const inv: u16 = @as(u16, ~bytes[i]) & 0xff; const sum = inv + carry; mag[i] = @intCast(sum & 0xff); carry = sum >> 8; } } else { @memcpy(mag, bytes); } var digits: std.ArrayListUnmanaged(u8) = .empty; defer digits.deinit(allocator); const remaining = try allocator.dupe(u8, mag); defer allocator.free(remaining); while (true) { var all_zero = true; var rem: u16 = 0; for (remaining) |*b| { const cur = (rem << 8) | @as(u16, b.*); const q: u8 = @intCast(cur / 10); rem = cur % 10; b.* = q; if (q != 0) all_zero = false; } try digits.append(allocator, @intCast(rem + '0')); if (all_zero) break; } if (is_negative) try out.append(allocator, '-'); var j: usize = digits.items.len; while (j > 0) { j -= 1; try out.append(allocator, digits.items[j]); }}fn writeQuotedString(allocator: Allocator, out: *ArrayList, s: []const u8) EncodeError!void { try out.append(allocator, '"'); for (s) |ch| { switch (ch) { '"' => try out.appendSlice(allocator, "\\\""), '\\' => try out.appendSlice(allocator, "\\\\"), '\n' => try out.appendSlice(allocator, "\\n"), '\r' => try out.appendSlice(allocator, "\\r"), '\t' => try out.appendSlice(allocator, "\\t"), 0x08 => try out.appendSlice(allocator, "\\b"), 0x0c => try out.appendSlice(allocator, "\\f"), else => { if (ch < 0x20) { try out.appendSlice(allocator, "\\u"); var hex: [4]u8 = undefined; _ = std.fmt.bufPrint(&hex, "{x:0>4}", .{ch}) catch unreachable; try out.appendSlice(allocator, &hex); } else { try out.append(allocator, ch); } }, } } try out.append(allocator, '"');}fn writeByteString(allocator: Allocator, out: *ArrayList, bytes: []const u8) EncodeError!void { try out.append(allocator, '#'); try out.append(allocator, '['); const enc = std.base64.standard_no_pad.Encoder; const needed = enc.calcSize(bytes.len); const dst = try allocator.alloc(u8, needed); defer allocator.free(dst); _ = enc.encode(dst, bytes); try out.appendSlice(allocator, dst); try out.append(allocator, ']');}fn writeSymbol(allocator: Allocator, out: *ArrayList, s: []const u8) EncodeError!void { if (s.len > 0 and isBareSymbol(s) and !looksLikeNumber(s)) { try out.appendSlice(allocator, s); } else { try out.append(allocator, '\''); for (s) |ch| { switch (ch) { '\'' => try out.appendSlice(allocator, "\\'"), '\\' => try out.appendSlice(allocator, "\\\\"), '\n' => try out.appendSlice(allocator, "\\n"), '\r' => try out.appendSlice(allocator, "\\r"), '\t' => try out.appendSlice(allocator, "\\t"), else => { if (ch < 0x20) { try out.appendSlice(allocator, "\\u"); var hex: [4]u8 = undefined; _ = std.fmt.bufPrint(&hex, "{x:0>4}", .{ch}) catch unreachable; try out.appendSlice(allocator, &hex); } else { try out.append(allocator, ch); } }, } } try out.append(allocator, '\''); }}fn isBareSymbol(s: []const u8) bool { for (s) |c| { if (!isBareSymbolChar(c)) return false; } return true;}fn isBareSymbolChar(c: u8) bool { return switch (c) { 'a'...'z', 'A'...'Z', '0'...'9' => true, '-', '~', '!', '$', '%', '^', '&', '*', '?', '_', '=', '+', '/', '.', '|' => true, else => false, };}fn looksLikeNumber(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 !isDigit(s[i])) return false; const int_start = i; while (i < s.len and isDigit(s[i])) i += 1; if (i == int_start) return false; if (i == s.len) return true; if (s[i] == '.') { i += 1; const frac_start = i; while (i < s.len and isDigit(s[i])) i += 1; if (i == frac_start) return 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 isDigit(s[i])) i += 1; if (i == exp_start) return false; } return i == s.len;}fn isDigit(c: u8) bool { return c >= '0' and c <= '9';}test "text encode primitives" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; const V = value_mod.Value(NE); const t = try encode(NE, allocator, V.initBoolean(true)); defer allocator.free(t); try std.testing.expectEqualStrings("#t", t); const n = try encode(NE, allocator, V.initI128(-42)); defer allocator.free(n); try std.testing.expectEqualStrings("-42", n); const s = try encode(NE, allocator, V{ .string = "hi" }); defer allocator.free(s); try std.testing.expectEqualStrings("\"hi\"", s); const sym = try encode(NE, allocator, V{ .symbol = "hello" }); defer allocator.free(sym); try std.testing.expectEqualStrings("hello", sym);}test "symbol that looks like a number is quoted" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; const V = value_mod.Value(NE); const out = try encode(NE, allocator, V{ .symbol = "123" }); defer allocator.free(out); try std.testing.expectEqualStrings("'123'", out);}test "u128 and big integer decimal" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; const V = value_mod.Value(NE); const above: u128 = @as(u128, @intCast(std.math.maxInt(i128))) + 1; const big = V.initU128(above); const out = try encode(NE, allocator, big); defer allocator.free(out); try std.testing.expectEqualStrings("170141183460469231731687303715884105728", out);}test "text encode rejects duplicate set elements and dictionary keys" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; const V = value_mod.Value(NE); var set_items = [_]V{ V.initI128(1), V.initI128(1) }; var entries = [_]V.DictionaryEntry{ .{ .key = V.initI128(1), .value = V.initBoolean(true) }, .{ .key = V.initI128(1), .value = V.initBoolean(false) }, }; try std.testing.expectError( error.DuplicateSetElement, encode(NE, allocator, V.initSet(&set_items)), ); try std.testing.expectError( error.DuplicateDictionaryKey, encode(NE, allocator, V.initDictionary(&entries)), );}Also reachable as
Audit
| Definitions | 3 |
|---|---|
| Public names | 6 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |