lib/pretty/core/src/compact/table.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const format = @import("format.zig");
  4 const types = @import("types.zig");
  5 
  6 const Allocator = std.mem.Allocator;
  7 pub const Align = types.Align;
  8 pub const Column = types.Column;
  9 
 10 pub const Limits = struct {
 11     columns: usize,
 12     rows: usize,
 13     maximum_line_bytes: usize,
 14     footer_bytes: usize,
 15     output_bytes: usize,
 16 
 17     pub fn inspect(
 18         columns: []const Column,
 19         rows: []const []const []const u8,
 20         total: ?usize,
 21     ) error{CapacityOverflow}!Limits {
 22         const maximum_line_bytes = try format.maximumLineBytes(columns, rows);
 23         const body_lines = std.math.add(usize, rows.len, 2) catch
 24             return error.CapacityOverflow;
 25         const body_bytes = std.math.mul(
 26             usize,
 27             maximum_line_bytes,
 28             body_lines,
 29         ) catch return error.CapacityOverflow;
 30         const footer_bytes = try format.footerLength(rows.len, total);
 31         const output_bytes = std.math.add(usize, body_bytes, footer_bytes) catch
 32             return error.CapacityOverflow;
 33         std.debug.assert(maximum_line_bytes > 0);
 34         std.debug.assert(output_bytes >= body_bytes);
 35         return .{
 36             .columns = columns.len,
 37             .rows = rows.len,
 38             .maximum_line_bytes = maximum_line_bytes,
 39             .footer_bytes = footer_bytes,
 40             .output_bytes = output_bytes,
 41         };
 42     }
 43 };
 44 
 45 pub const Capacity = struct {
 46     limits: Limits,
 47     width_bytes: usize,
 48     output_bytes: usize,
 49     working_bytes: usize,
 50 
 51     pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
 52         const width_bytes = std.math.mul(
 53             usize,
 54             limits.columns,
 55             @sizeOf(usize),
 56         ) catch return error.CapacityOverflow;
 57         const working_bytes = std.math.add(
 58             usize,
 59             width_bytes,
 60             limits.output_bytes,
 61         ) catch return error.CapacityOverflow;
 62         std.debug.assert(working_bytes >= width_bytes);
 63         std.debug.assert(working_bytes >= limits.output_bytes);
 64         return .{
 65             .limits = limits,
 66             .width_bytes = width_bytes,
 67             .output_bytes = limits.output_bytes,
 68             .working_bytes = working_bytes,
 69         };
 70     }
 71 };
 72 
 73 pub const RenderError = error{
 74     CapacityModelViolation,
 75     InputChanged,
 76 };
 77 
 78 const TableLimits = Limits;
 79 const TableCapacity = Capacity;
 80 
 81 /// Renders a compact table into storage derived from an exact input inspection.
 82 /// The returned slice is borrowed until the next render or `deinit`.
 83 pub const Table = struct {
 84     pub const claim: alloc_phase.capacity.Declaration = .{
 85         .source = .{
 86             .id = "pretty.compact_table",
 87             .kind = .phase_static,
 88             .limit_source = .caller,
 89             .storage = .{
 90                 .covered = &.{
 91                     .{
 92                         .id = "exact_per_column_width_slice",
 93                         .lifetime = .steady,
 94                         .detail = "exact per-column width slice",
 95                     },
 96                     .{
 97                         .id = "derived_fixed_worst_case_rendered_output_byte_slice",
 98                         .lifetime = .steady,
 99                         .detail = "derived fixed worst-case rendered-output byte slice",
100                     },
101                 },
102                 .excluded = &.{
103                     "caller-owned column, row, header, and cell storage",
104                     "the convenience compact.table wrapper result duplicate and its caller-owned lifetime",
105                     "destination writes performed after Table.render returns its borrowed output",
106                     "glom-owned result preparation, references, snippets, and non-table presentation modes",
107                 },
108             },
109             .capacity = .{
110                 .inputs = &.{
111                     alloc_phase.capacity.bindInput(TableLimits, "columns", "columns"),
112                     alloc_phase.capacity.bindInput(TableLimits, "maximum_line_bytes", "maximum_line_bytes"),
113                     alloc_phase.capacity.bindInput(TableLimits, "rows", "rows"),
114                     alloc_phase.capacity.bindInput(TableLimits, "footer_bytes", "footer_bytes"),
115                 },
116                 .type_selectors = &.{
117                     alloc_phase.capacity.bindType(usize, "usize"),
118                 },
119                 .nodes = &.{
120                     .{ .input = 0 },
121                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
122                     .{ .input = 1 },
123                     .{ .input = 2 },
124                     .{ .constant = 2 },
125                     .{ .add = .{ .left = 3, .right = 4 } },
126                     .{ .product = .{ .left = 2, .right = 5 } },
127                     .{ .input = 3 },
128                     .{ .add = .{ .left = 6, .right = 7 } },
129                     .{ .add = .{ .left = 1, .right = 8 } },
130                 },
131                 .assertions = &.{.{
132                     .scope = .closure_total,
133                     .measure = .retained,
134                     .relation = .exact,
135                     .expression = 9,
136                 }},
137             },
138             .overload = .{
139                 .kind = .reject_before_seal,
140                 .detail = "capacity arithmetic overflow and initialization OOM fail before activation; render rejects changed derived capacity facts before measuring widths or writing output; content changes that preserve admitted capacity facts remain supported",
141             },
142             .risks = .{
143                 .transitive = .{
144                     .status = .open,
145                     .detail = "the source-reviewed fixed-writer helper chain carries no allocator capability, but no machine-checked call-graph certificate excludes future policy allocator reacquisition",
146                 },
147                 .foreign = .{
148                     .status = .excluded,
149                     .detail = "the owner uses process-local caller-allocated slices and a fixed in-memory writer; destination and operating-system writes are outside the owner",
150                 },
151             },
152             .obligations = &.{
153                 .{ .key = "pretty_compact_table_capacity_capacity_model", .role = .capacity_model },
154                 .{ .key = "pretty_compact_table_capacity_overload", .role = .overload },
155                 .{ .key = "pretty_compact_table_oom_retry", .role = .overload },
156                 .{ .key = "pretty_compact_table_sealed_transitive_risk", .role = .transitive_risk },
157                 .{ .key = "pretty_compact_table_sealed_foreign_risk", .role = .foreign_risk },
158                 .{ .key = "pretty_compact_table_limit_drift", .role = .custom },
159                 .{ .key = "pretty_compact_table_semantics", .role = .transitive_risk },
160                 .{ .key = "pretty_compact_table_overflow", .role = .overload },
161             },
162         },
163         .bindings = .{
164             .owner = @This(),
165             .seal = .{
166                 .family = alloc_phase.capacity.selector(@This().activate),
167                 .premise = .{
168                     .class = .checked_semantic_fact,
169                     .authority = .checker,
170                 },
171             },
172             .teardown = .{
173                 .family = alloc_phase.capacity.selector(@This().deinit),
174                 .premise = .{
175                     .class = .checked_semantic_fact,
176                     .authority = .checker,
177                 },
178             },
179         },
180     };
181     pub const Limits = TableLimits;
182     pub const Capacity = TableCapacity;
183 
184     phase: alloc_phase.capacity.Phase,
185     capacity: TableCapacity,
186     widths: []usize,
187     output: []u8,
188     high_water: usize,
189 
190     pub fn init(allocator: Allocator, limits: TableLimits) !Table {
191         const capacity = try TableCapacity.derive(limits);
192         const widths = try allocator.alloc(usize, capacity.limits.columns);
193         errdefer allocator.free(widths);
194         const output = try allocator.alloc(u8, capacity.output_bytes);
195         return .{
196             .phase = .initialization,
197             .capacity = capacity,
198             .widths = widths,
199             .output = output,
200             .high_water = 0,
201         };
202     }
203 
204     pub fn activate(self: *Table) void {
205         std.debug.assert(self.phase == .initialization);
206         std.debug.assert(self.widths.len == self.capacity.limits.columns);
207         std.debug.assert(self.output.len == self.capacity.output_bytes);
208         self.phase = .steady;
209     }
210 
211     pub fn render(
212         self: *Table,
213         columns: []const Column,
214         rows: []const []const []const u8,
215         total: ?usize,
216     ) RenderError![]const u8 {
217         std.debug.assert(self.phase == .steady);
218         const inspected = TableLimits.inspect(columns, rows, total) catch
219             return error.CapacityModelViolation;
220         if (!std.meta.eql(inspected, self.capacity.limits)) return error.InputChanged;
221         format.measureWidths(self.widths, columns, rows);
222         var writer = std.Io.Writer.fixed(self.output);
223         format.renderInto(&writer, self.widths, columns, rows, total) catch
224             return error.CapacityModelViolation;
225         const rendered = writer.buffered();
226         self.high_water = @max(self.high_water, rendered.len);
227         std.debug.assert(self.high_water <= self.capacity.output_bytes);
228         return rendered;
229     }
230 
231     pub fn highWater(self: *const Table) usize {
232         std.debug.assert(self.phase == .steady);
233         std.debug.assert(self.high_water <= self.capacity.output_bytes);
234         return self.high_water;
235     }
236 
237     pub fn deinit(self: *Table, allocator: Allocator) void {
238         std.debug.assert(self.phase != .teardown);
239         self.phase = .teardown;
240         allocator.free(self.output);
241         allocator.free(self.widths);
242         self.output = &.{};
243         self.widths = &.{};
244     }
245 };
246 
247 comptime {
248     alloc_phase.capacity.requireAllocatorExactOwnerShape(Table);
249 }
250 
251 /// Renders one compact table into a caller-owned allocation.
252 pub fn table(
253     allocator: Allocator,
254     columns: []const Column,
255     rows: []const []const []const u8,
256     total: ?usize,
257 ) ![]u8 {
258     const limits = try Limits.inspect(columns, rows, total);
259     var owner = try Table.init(allocator, limits);
260     owner.activate();
261     defer owner.deinit(allocator);
262     return try allocator.dupe(u8, try owner.render(columns, rows, total));
263 }
264 
265 const fixture_columns = [_]Column{
266     .{ .header = "name" },
267     .{ .header = "count", .alignment = .right, .max_width = 10 },
268 };
269 const fixture_row_1 = [_][]const u8{ "alpha", "42" };
270 const fixture_row_2 = [_][]const u8{ "beta", "7" };
271 const fixture_row_3 = [_][]const u8{ "longer", "3" };
272 const fixture_rows = [_][]const []const u8{
273     &fixture_row_1,
274     &fixture_row_2,
275     &fixture_row_3,
276 };
277 
278 test "compact table capacity matches an independent typed-byte model" {
279     comptime {
280         alloc_phase.capacity.record(
281             alloc_phase.capacity.witness(Table, "pretty_compact_table_capacity_capacity_model"),
282         );
283     }
284     comptime {
285         alloc_phase.capacity.record(
286             alloc_phase.capacity.witness(Table, "pretty_compact_table_capacity_overload"),
287         );
288     }
289 
290     const limits = try Limits.inspect(&fixture_columns, &fixture_rows, 5);
291     const capacity = try Capacity.derive(limits);
292     const maximum_line_bytes = "longer".len + format.gutter.len + "count".len + 1;
293     const footer_bytes = "(showing 3 of 5".len + format.footer_tail.len;
294     const output_bytes = maximum_line_bytes * (fixture_rows.len + 2) + footer_bytes;
295     try std.testing.expectEqual(@as(usize, 2), limits.columns);
296     try std.testing.expectEqual(maximum_line_bytes, limits.maximum_line_bytes);
297     try std.testing.expectEqual(footer_bytes, limits.footer_bytes);
298     try std.testing.expectEqual(output_bytes, capacity.output_bytes);
299     try std.testing.expectEqual(2 * @sizeOf(usize), capacity.width_bytes);
300     try std.testing.expectEqual(
301         capacity.width_bytes + capacity.output_bytes,
302         capacity.working_bytes,
303     );
304 }
305 
306 fn checkTableInitFailures(allocator: Allocator, limits: Limits) !void {
307     var owner = try Table.init(allocator, limits);
308     defer owner.deinit(allocator);
309     try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, owner.phase);
310 }
311 
312 test "compact table initialization cleans every allocation failure and retries" {
313     comptime {
314         alloc_phase.capacity.record(
315             alloc_phase.capacity.witness(Table, "pretty_compact_table_oom_retry"),
316         );
317     }
318 
319     const limits = try Limits.inspect(&fixture_columns, &fixture_rows, 5);
320     try std.testing.checkAllAllocationFailures(
321         std.testing.allocator,
322         checkTableInitFailures,
323         .{limits},
324     );
325     var owner = try Table.init(std.testing.allocator, limits);
326     defer owner.deinit(std.testing.allocator);
327     owner.activate();
328     try std.testing.expect((try owner.render(&fixture_columns, &fixture_rows, 5)).len > 0);
329 }
330 
331 test "compact table is sealed before its first maximum render" {
332     comptime {
333         alloc_phase.capacity.record(
334             alloc_phase.capacity.witness(Table, "pretty_compact_table_sealed_transitive_risk"),
335         );
336     }
337     comptime {
338         alloc_phase.capacity.record(
339             alloc_phase.capacity.witness(Table, "pretty_compact_table_sealed_foreign_risk"),
340         );
341     }
342 
343     const limits = try Limits.inspect(&fixture_columns, &fixture_rows, 5);
344     var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
345     var owner = try Table.init(phase_allocator.initializationAllocator(), limits);
346     const widths_pointer = owner.widths.ptr;
347     const output_pointer = owner.output.ptr;
348     phase_allocator.seal();
349     owner.activate();
350     const first = try owner.render(&fixture_columns, &fixture_rows, 5);
351     const second = try owner.render(&fixture_columns, &fixture_rows, 5);
352     try std.testing.expectEqualStrings(first, second);
353     try std.testing.expectEqual(widths_pointer, owner.widths.ptr);
354     try std.testing.expectEqual(output_pointer, owner.output.ptr);
355     try std.testing.expect(owner.highWater() <= owner.capacity.output_bytes);
356     phase_allocator.beginTeardown();
357     owner.deinit(phase_allocator.teardownAllocator());
358     phase_allocator.deinit();
359 }
360 
361 test "compact table detects changed admitted input" {
362     comptime {
363         alloc_phase.capacity.record(
364             alloc_phase.capacity.witness(Table, "pretty_compact_table_limit_drift"),
365         );
366     }
367 
368     const limits = try Limits.inspect(&fixture_columns, &fixture_rows, 5);
369     var owner = try Table.init(std.testing.allocator, limits);
370     defer owner.deinit(std.testing.allocator);
371     owner.activate();
372     try std.testing.expectError(
373         error.InputChanged,
374         owner.render(&fixture_columns, fixture_rows[0..2], 5),
375     );
376     const above_capacity_rows = fixture_rows ++ [_][]const []const u8{&fixture_row_1};
377     try std.testing.expectError(
378         error.InputChanged,
379         owner.render(&fixture_columns, &above_capacity_rows, 5),
380     );
381     const changed_row = [_][]const u8{ "zeta", "8" };
382     const changed_rows = [_][]const []const u8{
383         &fixture_row_1,
384         &changed_row,
385         &fixture_row_3,
386     };
387     const rendered = try owner.render(&fixture_columns, &changed_rows, 5);
388     try std.testing.expect(std.mem.indexOf(u8, rendered, "zeta") != null);
389 }
390 
391 test "compact table has no trailing whitespace and preserves footer" {
392     comptime {
393         alloc_phase.capacity.record(
394             alloc_phase.capacity.witness(Table, "pretty_compact_table_semantics"),
395         );
396     }
397 
398     const out = try table(
399         std.testing.allocator,
400         &fixture_columns,
401         &fixture_rows,
402         5,
403     );
404     defer std.testing.allocator.free(out);
405     try std.testing.expect(std.mem.indexOf(u8, out, "showing 3 of 5") != null);
406     var lines = std.mem.splitScalar(u8, out, '\n');
407     while (lines.next()) |line| {
408         if (line.len == 0) continue;
409         try std.testing.expect(line[line.len - 1] != ' ');
410     }
411 }
412 
413 test "compact table capacity rejects arithmetic overflow" {
414     comptime {
415         alloc_phase.capacity.record(
416             alloc_phase.capacity.witness(Table, "pretty_compact_table_overflow"),
417         );
418     }
419 
420     const limits = Limits{
421         .columns = std.math.maxInt(usize) / @sizeOf(usize) + 1,
422         .rows = 0,
423         .maximum_line_bytes = 1,
424         .footer_bytes = 0,
425         .output_bytes = 1,
426     };
427     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(limits));
428 }