tiny.preserves.Conversions
Defined in patterns_mod.
Returns a namespace of pattern conversions for Value(D).
Source
Source: lib/preserves/src/patterns.zig:37
zig
/// Returns a namespace of pattern conversions for `Value(D)`. Code whose values hold embedded/// values of one type calls it once at compile time, for every pattern conversion over those/// values. The type `D` has to provide `eql`, `order`, `deinit` and `clone`, or the call is a/// compile error. `conversions` and `any_conversions` are its two instances.pub fn Conversions(comptime D: type) type { domain_mod.assertIsDomain(D); const V = Value(D); const H = constructors_mod.Constructors(D); return struct { const Self = @This(); /// Returns `value` unchanged. The call allocates nothing. pub fn preserve(alloc: Allocator, value: V) V { _ = alloc; return value; } /// Returns the in-memory pattern that the wire value `pattern` spells, allocated with /// `alloc`. Code receiving a pattern in wire form calls it for an in-memory pattern. The /// symbol `_` and the record `<_>` become the discard pattern. `<bind P>` becomes a capture /// of `P`, and `<lit v>` becomes `v` itself. `<group <rec L> {…}>` becomes a record labeled /// `L`, and `<group <arr> {…}>` a sequence, each item taken in key order from the /// dictionary. `<group <dict> {…}>` becomes a dictionary with the same keys. Other records, /// sequences and dictionaries keep their shape, and their parts are converted in turn. The /// result shares atoms and `<lit>` contents with `pattern`, so `pattern` has to outlive it. /// When an allocation fails, the call returns the unconverted input at that level and keeps /// what it already allocated. The package's tests build its result in an arena and free the /// arena whole. pub fn preservePattern(alloc: Allocator, pattern: V) V { switch (pattern) { .discard => return pattern, .capture => |inner| { const new = alloc.create(V) catch return pattern; new.* = Self.preservePattern(alloc, inner.*); return .{ .capture = new }; }, .bind => |b| { const new_pat = alloc.create(V) catch return pattern; new_pat.* = Self.preservePattern(alloc, b.pattern.*); return .{ .bind = .{ .name = b.name, .pattern = new_pat } }; }, .rest_pattern => |rp| { const new_prefix = alloc.alloc(V, rp.prefix.len) catch return pattern; for (rp.prefix, 0..) |item, i| { new_prefix[i] = Self.preservePattern(alloc, item); } const new_rest = alloc.create(V) catch return pattern; new_rest.* = Self.preservePattern(alloc, rp.rest.*); return .{ .rest_pattern = .{ .prefix = new_prefix, .rest = new_rest } }; }, .record => |r| return Self.preservePatternRecord(alloc, pattern, r), .symbol => |s| { if (std.mem.eql(u8, s, symbols_mod.SYM_DISCARD.name)) return .{ .discard = {} }; return pattern; }, .dictionary => |d| { const new_entries = alloc.alloc(V.DictionaryEntry, d.len) catch return pattern; for (d, 0..) |entry, i| { new_entries[i] = .{ .key = Self.preservePattern(alloc, entry.key), .value = Self.preservePattern(alloc, entry.value), }; } return .{ .dictionary = new_entries }; }, .sequence => |s| { const new_items = alloc.alloc(V, s.len) catch return pattern; for (s, 0..) |item, i| { new_items[i] = Self.preservePattern(alloc, item); } return .{ .sequence = new_items }; }, else => return pattern, } } fn preservePatternRecord(alloc: Allocator, fallback: V, r: V.Record) V { if (r.label.* == .symbol) { const lname = r.label.*.symbol; if (std.mem.eql(u8, lname, symbols_mod.SYM_DISCARD.name) and r.fields.len == 0) { return .{ .discard = {} }; } if (std.mem.eql(u8, lname, symbols_mod.SYM_BIND_PAT.name) and r.fields.len == 1) { const inner = alloc.create(V) catch return fallback; inner.* = Self.preservePattern(alloc, r.fields[0]); return .{ .capture = inner }; } if (std.mem.eql(u8, lname, symbols_mod.SYM_LIT.name) and r.fields.len == 1) { return r.fields[0]; } if (std.mem.eql(u8, lname, symbols_mod.SYM_GROUP.name) and r.fields.len == 2) { if (Self.preserveGroup(alloc, r.fields[0], r.fields[1], fallback)) |g| return g; } } const new_label = alloc.create(V) catch return fallback; new_label.* = Self.preserve(alloc, r.label.*); const new_fields = alloc.alloc(V, r.fields.len) catch return fallback; for (r.fields, 0..) |field, i| { new_fields[i] = Self.preservePattern(alloc, field); } return .{ .record = .{ .label = new_label, .fields = new_fields } }; } fn preserveGroup(alloc: Allocator, group_type: V, entries_val: V, fallback: V) ?V { const gt = switch (group_type) { .record => |gtr| gtr, else => return null, }; if (gt.label.* != .symbol) return null; const gs = gt.label.*.symbol; if (std.mem.eql(u8, gs, symbols_mod.SYM_REC.name)) { const rec_label = if (gt.fields.len > 0) gt.fields[0] else V{ .symbol = "" }; const fields = Self.entriesToIndexed(alloc, entries_val) catch return fallback; defer alloc.free(fields); const new_fields = alloc.alloc(V, fields.len) catch return fallback; for (fields, 0..) |f, i| { new_fields[i] = Self.preservePattern(alloc, f); } const new_label = alloc.create(V) catch return fallback; new_label.* = rec_label; return V{ .record = .{ .label = new_label, .fields = new_fields } }; } if (std.mem.eql(u8, gs, symbols_mod.SYM_ARR.name)) { const fields = Self.entriesToIndexed(alloc, entries_val) catch return fallback; defer alloc.free(fields); const new_items = alloc.alloc(V, fields.len) catch return fallback; for (fields, 0..) |f, i| { new_items[i] = Self.preservePattern(alloc, f); } return V{ .sequence = new_items }; } if (std.mem.eql(u8, gs, symbols_mod.SYM_DICT.name)) { switch (entries_val) { .dictionary => |d| { const new_entries = alloc.alloc(V.DictionaryEntry, d.len) catch return fallback; for (d, 0..) |entry, i| { new_entries[i] = .{ .key = entry.key, .value = Self.preservePattern(alloc, entry.value), }; } return V{ .dictionary = new_entries }; }, else => {}, } } return null; } /// Returns the wire form of `pattern`, allocated with `alloc`. The text formatter calls it /// for the wire spelling of captures and binds, and code sending a pattern calls it, for a /// plain value the codecs can write. The discard pattern becomes `<_>`, and a capture or /// bind becomes `<bind P>` around its converted pattern. An atom or a set becomes /// `<lit v>`, and an embedded value stays as it is. A record becomes /// `<group <rec L> {0: …, 1: …}>`, and a sequence `<group <arr> {…}>`, keyed by position. A /// dictionary becomes `<group <dict> {…}>` with the same keys. A rest pattern becomes an /// array group of its prefix, then `<lit .>`, then its rest. The result shares atoms, /// labels and dictionary keys with `pattern`, so `pattern` has to outlive it. A bind's name /// does not appear in the result. The only error is running out of memory, and the call /// does not free what it built before the failure. pub fn patternToPreserves(alloc: Allocator, pattern: V) !V { return switch (pattern) { .discard => H.record(alloc, V{ .symbol = symbols_mod.SYM_DISCARD.name }, &.{}), .capture => |inner| blk: { const converted = try Self.patternToPreserves(alloc, inner.*); break :blk H.record(alloc, V{ .symbol = symbols_mod.SYM_BIND_PAT.name }, &.{converted}); }, .bind => |b| blk: { const converted = try Self.patternToPreserves(alloc, b.pattern.*); break :blk H.record(alloc, V{ .symbol = symbols_mod.SYM_BIND_PAT.name }, &.{converted}); }, .boolean, .double, .signed_integer, .string, .byte_string, .symbol, => H.record(alloc, V{ .symbol = symbols_mod.SYM_LIT.name }, &.{pattern}), .embedded => pattern, .record => |r| blk: { const group_type = try H.record(alloc, V{ .symbol = symbols_mod.SYM_REC.name }, &.{r.label.*}); const entries = try alloc.alloc(V.DictionaryEntry, r.fields.len); for (r.fields, 0..) |field, i| { entries[i] = .{ .key = V.initI128(@as(i128, @intCast(i))), .value = try Self.patternToPreserves(alloc, field), }; } const dict = V{ .dictionary = entries }; break :blk H.record(alloc, V{ .symbol = symbols_mod.SYM_GROUP.name }, &.{ group_type, dict }); }, .sequence => |items| blk: { const group_type = try H.record(alloc, V{ .symbol = symbols_mod.SYM_ARR.name }, &.{}); const entries = try alloc.alloc(V.DictionaryEntry, items.len); for (items, 0..) |item, i| { entries[i] = .{ .key = V.initI128(@as(i128, @intCast(i))), .value = try Self.patternToPreserves(alloc, item), }; } const dict = V{ .dictionary = entries }; break :blk H.record(alloc, V{ .symbol = symbols_mod.SYM_GROUP.name }, &.{ group_type, dict }); }, .dictionary => |d| blk: { const group_type = try H.record(alloc, V{ .symbol = symbols_mod.SYM_DICT.name }, &.{}); const entries = try alloc.alloc(V.DictionaryEntry, d.len); for (d, 0..) |entry, i| { entries[i] = .{ .key = entry.key, .value = try Self.patternToPreserves(alloc, entry.value), }; } const dict = V{ .dictionary = entries }; break :blk H.record(alloc, V{ .symbol = symbols_mod.SYM_GROUP.name }, &.{ group_type, dict }); }, .rest_pattern => |rp| blk: { const items = try alloc.alloc(V, rp.prefix.len + 2); defer alloc.free(items); for (rp.prefix, 0..) |item, i| { items[i] = item; } items[rp.prefix.len] = V{ .symbol = "." }; items[rp.prefix.len + 1] = rp.rest.*; break :blk try Self.patternToPreserves(alloc, V{ .sequence = items }); }, .set => H.record(alloc, V{ .symbol = symbols_mod.SYM_LIT.name }, &.{pattern}), }; } /// Returns the in-memory pattern that the wire value `value` spells, allocated with /// `alloc`. Code receiving a pattern in wire form calls it for an in-memory pattern, with /// allocation failure reported. The call reads the same spellings as `preservePattern`: `_` /// and `<_>`, `<bind P>`, `<lit v>` and the three `<group>` forms. A `<lit v>` gets a copy /// of `v`'s compound storage. Other records, sequences, dictionaries and in-memory patterns /// keep their shape, and their parts are converted in turn. Strings, symbols, byte strings /// and bind names in the result point into `value`, and sets, integers and embedded values /// come back as they are, sharing storage with `value`. The package's test frees a result /// with `freeValueDeep`, which frees shared sets and integers too. The call returns /// `error.OutOfMemory` when an allocation fails. On failure, most arms free what they /// built, and the `<group>` arms and the `<bind P>` arm leak it. pub fn preservesToPattern(alloc: Allocator, value: V) Allocator.Error!V { switch (value) { .symbol => |s| { if (std.mem.eql(u8, s, symbols_mod.SYM_DISCARD.name)) { return .{ .discard = {} }; } }, .record => |r| { switch (r.label.*) { .symbol => |s| { if (std.mem.eql(u8, s, symbols_mod.SYM_DISCARD.name) and r.fields.len == 0) { return .{ .discard = {} }; } if (std.mem.eql(u8, s, symbols_mod.SYM_BIND_PAT.name) and r.fields.len == 1) { const inner = try Self.preservesToPattern(alloc, r.fields[0]); return H.capture(alloc, inner); } if (std.mem.eql(u8, s, symbols_mod.SYM_LIT.name) and r.fields.len == 1) { return try Self.cloneValueOwned(alloc, r.fields[0]); } if (std.mem.eql(u8, s, symbols_mod.SYM_GROUP.name) and r.fields.len == 2) { if (try Self.preservesToGroup(alloc, r.fields[0], r.fields[1])) |g| return g; } }, else => {}, } return try Self.plainRecordToPattern(alloc, r); }, .sequence => |items| { const converted = try alloc.alloc(V, items.len); var filled: usize = 0; errdefer { for (converted[0..filled]) |item| { ownership.freeValueDeep(D, alloc, item); } alloc.free(converted); } while (filled < items.len) : (filled += 1) { converted[filled] = try Self.preservesToPattern(alloc, items[filled]); } return .{ .sequence = converted }; }, .dictionary => |entries| { const converted = try alloc.alloc(V.DictionaryEntry, entries.len); var filled: usize = 0; errdefer { for (converted[0..filled]) |entry| { ownership.freeValueDeep(D, alloc, entry.key); ownership.freeValueDeep(D, alloc, entry.value); } alloc.free(converted); } while (filled < entries.len) : (filled += 1) { const key = try Self.cloneValueOwned(alloc, entries[filled].key); errdefer ownership.freeValueDeep(D, alloc, key); converted[filled] = .{ .key = key, .value = try Self.preservesToPattern(alloc, entries[filled].value), }; } return .{ .dictionary = converted }; }, .capture => |inner| { const converted = try Self.preservesToPattern(alloc, inner.*); return H.capture(alloc, converted); }, .bind => |binding| { const pattern = try alloc.create(V); errdefer alloc.destroy(pattern); pattern.* = try Self.preservesToPattern(alloc, binding.pattern.*); return .{ .bind = .{ .name = binding.name, .pattern = pattern, } }; }, .rest_pattern => |rest| { const prefix = try alloc.alloc(V, rest.prefix.len); var filled: usize = 0; errdefer { for (prefix[0..filled]) |item| { ownership.freeValueDeep(D, alloc, item); } alloc.free(prefix); } while (filled < rest.prefix.len) : (filled += 1) { prefix[filled] = try Self.preservesToPattern(alloc, rest.prefix[filled]); } const rest_ptr = try alloc.create(V); errdefer alloc.destroy(rest_ptr); rest_ptr.* = try Self.preservesToPattern(alloc, rest.rest.*); return .{ .rest_pattern = .{ .prefix = prefix, .rest = rest_ptr } }; }, else => {}, } return value; } fn plainRecordToPattern(alloc: Allocator, r: V.Record) Allocator.Error!V { const label = try alloc.create(V); errdefer alloc.destroy(label); label.* = try Self.cloneValueOwned(alloc, r.label.*); errdefer ownership.freeValueDeep(D, alloc, label.*); const fields = try alloc.alloc(V, r.fields.len); var filled: usize = 0; errdefer { for (fields[0..filled]) |field| { ownership.freeValueDeep(D, alloc, field); } alloc.free(fields); } while (filled < r.fields.len) : (filled += 1) { fields[filled] = try Self.preservesToPattern(alloc, r.fields[filled]); } return .{ .record = .{ .label = label, .fields = fields, } }; } fn cloneValueOwned(alloc: Allocator, value: V) Allocator.Error!V { return switch (value) { .boolean, .double, .string, .byte_string, .symbol, .discard => value, .signed_integer => |si| .{ .signed_integer = try si.clone(alloc) }, .record => |record| blk: { const label = try alloc.create(V); errdefer alloc.destroy(label); label.* = try Self.cloneValueOwned(alloc, record.label.*); errdefer ownership.freeValueDeep(D, alloc, label.*); break :blk .{ .record = .{ .label = label, .fields = try Self.cloneValueSliceOwned(alloc, record.fields), } }; }, .sequence => |items| .{ .sequence = try Self.cloneValueSliceOwned(alloc, items) }, .set => |items| .{ .set = try Self.cloneValueSliceOwned(alloc, items) }, .dictionary => |entries| .{ .dictionary = try Self.cloneDictionaryOwned(alloc, entries) }, .embedded => |embedded| .{ .embedded = try embedded.clone(alloc) }, .capture => |inner| blk: { const cloned = try alloc.create(V); errdefer alloc.destroy(cloned); cloned.* = try Self.cloneValueOwned(alloc, inner.*); break :blk .{ .capture = cloned }; }, .bind => |binding| blk: { const cloned = try alloc.create(V); errdefer alloc.destroy(cloned); cloned.* = try Self.cloneValueOwned(alloc, binding.pattern.*); break :blk .{ .bind = .{ .name = binding.name, .pattern = cloned, } }; }, .rest_pattern => |rest| blk: { const prefix = try Self.cloneValueSliceOwned(alloc, rest.prefix); errdefer { for (prefix) |item| ownership.freeValueDeep(D, alloc, item); alloc.free(prefix); } const rest_ptr = try alloc.create(V); errdefer alloc.destroy(rest_ptr); rest_ptr.* = try Self.cloneValueOwned(alloc, rest.rest.*); break :blk .{ .rest_pattern = .{ .prefix = prefix, .rest = rest_ptr } }; }, }; } fn cloneValueSliceOwned(alloc: Allocator, items: []const V) Allocator.Error![]V { const cloned = try alloc.alloc(V, items.len); var filled: usize = 0; errdefer { for (cloned[0..filled]) |item| { ownership.freeValueDeep(D, alloc, item); } alloc.free(cloned); } while (filled < items.len) : (filled += 1) { cloned[filled] = try Self.cloneValueOwned(alloc, items[filled]); } return cloned; } fn cloneDictionaryOwned(alloc: Allocator, entries: []const V.DictionaryEntry) Allocator.Error![]V.DictionaryEntry { const cloned = try alloc.alloc(V.DictionaryEntry, entries.len); var filled: usize = 0; errdefer { for (cloned[0..filled]) |entry| { ownership.freeValueDeep(D, alloc, entry.key); ownership.freeValueDeep(D, alloc, entry.value); } alloc.free(cloned); } while (filled < entries.len) : (filled += 1) { const key = try Self.cloneValueOwned(alloc, entries[filled].key); errdefer ownership.freeValueDeep(D, alloc, key); cloned[filled] = .{ .key = key, .value = try Self.cloneValueOwned(alloc, entries[filled].value), }; } return cloned; } fn preservesToGroup(alloc: Allocator, group_type: V, entries_val: V) Allocator.Error!?V { const gt = switch (group_type) { .record => |gtr| gtr, else => return null, }; const gs = switch (gt.label.*) { .symbol => |s| s, else => return null, }; if (std.mem.eql(u8, gs, symbols_mod.SYM_REC.name)) { const rec_label = if (gt.fields.len > 0) gt.fields[0] else V{ .symbol = "" }; const fields = try Self.entriesToIndexed(alloc, entries_val); defer alloc.free(fields); const converted = try alloc.alloc(V, fields.len); defer alloc.free(converted); for (fields, 0..) |f, i| { converted[i] = try Self.preservesToPattern(alloc, f); } return try H.record(alloc, rec_label, converted); } if (std.mem.eql(u8, gs, symbols_mod.SYM_ARR.name)) { const fields = try Self.entriesToIndexed(alloc, entries_val); defer alloc.free(fields); const converted = try alloc.alloc(V, fields.len); for (fields, 0..) |f, i| { converted[i] = try Self.preservesToPattern(alloc, f); } return V{ .sequence = converted }; } if (std.mem.eql(u8, gs, symbols_mod.SYM_DICT.name)) { switch (entries_val) { .dictionary => |d| { const converted = try alloc.alloc(V.DictionaryEntry, d.len); for (d, 0..) |entry, i| { converted[i] = .{ .key = entry.key, .value = try Self.preservesToPattern(alloc, entry.value), }; } return V{ .dictionary = converted }; }, else => {}, } } return null; } fn entriesToIndexed(alloc: Allocator, entries_val: V) Allocator.Error![]V { switch (entries_val) { .dictionary => |d| { const sorted = try alloc.dupe(V.DictionaryEntry, d); defer alloc.free(sorted); const Cmp = struct { fn lt(_: void, a: V.DictionaryEntry, b: V.DictionaryEntry) bool { return a.key.compare(b.key) == .lt; } }; std.mem.sort(V.DictionaryEntry, sorted, {}, Cmp.lt); const result = try alloc.alloc(V, sorted.len); for (sorted, 0..) |entry, i| { result[i] = entry.value; } return result; }, else => return try alloc.alloc(V, 0), } } /// Returns the observer, the second field of an `<Observe pattern observer>` record. The /// call returns null unless `value` is a record labeled `Observe` with two fields. The /// result shares storage with `value`, and the call allocates nothing. pub fn observeObserver(value: V) ?V { if (!predicates_mod.isRecord(D, value, symbols_mod.SYM_OBSERVE.name, 2)) return null; return value.record.fields[1]; } /// Returns the pattern, the first field of an `<Observe pattern observer>` record. The call /// returns null unless `value` is a record labeled `Observe` with two fields. The result /// shares storage with `value`, and the call allocates nothing. pub fn observePattern(value: V) ?V { if (!predicates_mod.isRecord(D, value, symbols_mod.SYM_OBSERVE.name, 2)) return null; return value.record.fields[0]; } /// Calls `callback(context, e)` for each embedded value `e` in `value`, depth first. Code /// holding references through embedded values calls it, for one visit to each, such as a /// count or a retain. The call visits record labels and fields, sequence and set items, /// dictionary keys and values, and the parts of in-memory patterns. The call allocates /// nothing and returns nothing. pub fn foreachEmbedded(value: V, context: anytype, callback: anytype) void { switch (value) { .embedded => |e| callback(context, e), .record => |r| { Self.foreachEmbedded(r.label.*, context, callback); for (r.fields) |field| Self.foreachEmbedded(field, context, callback); }, .sequence => |s| for (s) |item| Self.foreachEmbedded(item, context, callback), .set => |s| for (s) |item| Self.foreachEmbedded(item, context, callback), .dictionary => |d| for (d) |entry| { Self.foreachEmbedded(entry.key, context, callback); Self.foreachEmbedded(entry.value, context, callback); }, .capture => |p| Self.foreachEmbedded(p.*, context, callback), .bind => |b| Self.foreachEmbedded(b.pattern.*, context, callback), .rest_pattern => |rp| { for (rp.prefix) |item| Self.foreachEmbedded(item, context, callback); Self.foreachEmbedded(rp.rest.*, context, callback); }, else => {}, } } /// Returns a copy of `value` in which each embedded value `e` is replaced by /// `map_fn(context, alloc, e)`. Code that moves a value to another type of embedded value, /// or swaps each embedded value for another value, calls it for the rewritten copy. The /// call copies records, sequences, sets, dictionaries and in-memory patterns with `alloc`, /// and returns atoms as they are. The call returns any error `map_fn` returns, and /// `error.OutOfMemory`. On failure the call does not free what it built before. pub fn mapEmbedded( alloc: Allocator, value: V, context: anytype, map_fn: anytype, ) !V { switch (value) { .embedded => |e| return map_fn(context, alloc, e), .record => |r| { const new_label = try alloc.create(V); new_label.* = try Self.mapEmbedded(alloc, r.label.*, context, map_fn); const new_fields = try alloc.alloc(V, r.fields.len); for (r.fields, 0..) |field, i| { new_fields[i] = try Self.mapEmbedded(alloc, field, context, map_fn); } return V{ .record = .{ .label = new_label, .fields = new_fields } }; }, .sequence => |s| { const new_items = try alloc.alloc(V, s.len); for (s, 0..) |item, i| { new_items[i] = try Self.mapEmbedded(alloc, item, context, map_fn); } return V{ .sequence = new_items }; }, .set => |s| { const new_items = try alloc.alloc(V, s.len); for (s, 0..) |item, i| { new_items[i] = try Self.mapEmbedded(alloc, item, context, map_fn); } return V{ .set = new_items }; }, .dictionary => |d| { const new_entries = try alloc.alloc(V.DictionaryEntry, d.len); for (d, 0..) |entry, i| { new_entries[i] = .{ .key = try Self.mapEmbedded(alloc, entry.key, context, map_fn), .value = try Self.mapEmbedded(alloc, entry.value, context, map_fn), }; } return V{ .dictionary = new_entries }; }, .capture => |p| { const new_ptr = try alloc.create(V); new_ptr.* = try Self.mapEmbedded(alloc, p.*, context, map_fn); return V{ .capture = new_ptr }; }, .bind => |b| { const new_pat = try alloc.create(V); new_pat.* = try Self.mapEmbedded(alloc, b.pattern.*, context, map_fn); return V{ .bind = .{ .name = b.name, .pattern = new_pat } }; }, .rest_pattern => |rp| { const new_prefix = try alloc.alloc(V, rp.prefix.len); for (rp.prefix, 0..) |item, i| { new_prefix[i] = try Self.mapEmbedded(alloc, item, context, map_fn); } const new_rest = try alloc.create(V); new_rest.* = try Self.mapEmbedded(alloc, rp.rest.*, context, map_fn); return V{ .rest_pattern = .{ .prefix = new_prefix, .rest = new_rest } }; }, else => return value, } } };}Source: lib/preserves/src/root.zig:219
zig
pub const Conversions = patterns_mod.Conversions;Audit
| Definitions | 1 |
|---|---|
| Public names | 2 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |