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 }