lib/zen/src/diagram/ascii/plan.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const diagram = @import("../root.zig");
  4 const model = @import("model.zig");
  5 
  6 /// Maximum scratch and output regions admitted by ASCII render storage.
  7 pub const Limits = struct {
  8     /// Maximum distinct categorical bound slots.
  9     max_categories: usize,
 10     /// Maximum stacked-series total slots.
 11     max_stack_totals: usize,
 12     /// Maximum width times height canvas bytes.
 13     max_canvas_bytes: usize,
 14     /// Maximum compacted row bytes, including line endings.
 15     max_output_bytes: usize,
 16 };
 17 
 18 /// Exact, clamped storage demand for one document and option set.
 19 pub const Plan = struct {
 20     /// Admitted width after clamping.
 21     width: usize,
 22     /// Admitted height after clamping.
 23     height: usize,
 24     /// Categorical bound slots required by the document.
 25     categories: usize,
 26     /// Stacked-series total slots required by the document.
 27     stack_totals: usize,
 28     /// Exact canvas bytes required.
 29     canvas_bytes: usize,
 30     /// Maximum output bytes required before row compaction.
 31     output_bytes: usize,
 32 
 33     /// Measures document scratch and clamps dimensions without allocating.
 34     pub fn inspect(
 35         document: *const diagram.Document,
 36         options: model.Options,
 37     ) error{CapacityOverflow}!Plan {
 38         const width = clamp(options.width, 24, 200);
 39         const height = clamp(options.height, 8, 80);
 40         const scratch = diagram.bounds.ScratchPlan.inspect(document);
 41         return .{
 42             .width = width,
 43             .height = height,
 44             .categories = scratch.categories,
 45             .stack_totals = scratch.stack_totals,
 46             .canvas_bytes = try multiplied(width, height),
 47             .output_bytes = try multiplied(try added(width, 1), height),
 48         };
 49     }
 50 
 51     /// Returns the smallest limits that admit this render.
 52     pub fn exactLimits(self: Plan) Limits {
 53         return .{
 54             .max_categories = self.categories,
 55             .max_stack_totals = self.stack_totals,
 56             .max_canvas_bytes = self.canvas_bytes,
 57             .max_output_bytes = self.output_bytes,
 58         };
 59     }
 60 
 61     /// Rejects when any measured region exceeds supplied limits.
 62     pub fn require(self: Plan, limits: Limits) model.Exhaustion!void {
 63         if (self.categories > limits.max_categories) {
 64             return error.AsciiCategoryCapacityExceeded;
 65         }
 66         if (self.stack_totals > limits.max_stack_totals) {
 67             return error.AsciiStackTotalCapacityExceeded;
 68         }
 69         if (self.canvas_bytes > limits.max_canvas_bytes) {
 70             return error.AsciiCanvasByteCapacityExceeded;
 71         }
 72         if (self.output_bytes > limits.max_output_bytes) {
 73             return error.AsciiOutputByteCapacityExceeded;
 74         }
 75     }
 76 };
 77 
 78 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
 79     return std.math.add(usize, left, right) catch error.CapacityOverflow;
 80 }
 81 
 82 fn multiplied(left: usize, right: usize) error{CapacityOverflow}!usize {
 83     return std.math.mul(usize, left, right) catch error.CapacityOverflow;
 84 }
 85 
 86 fn clamp(value: usize, min: usize, max: usize) usize {
 87     return @max(min, @min(max, value));
 88 }
 89 
 90 test "ASCII render plan exposes exact clamped regions" {
 91     var document = diagram.Document{ .allocator = std.testing.allocator };
 92     const plan = try Plan.inspect(&document, .{ .width = 1, .height = std.math.maxInt(usize) });
 93     try std.testing.expectEqual(@as(usize, 24), plan.width);
 94     try std.testing.expectEqual(@as(usize, 80), plan.height);
 95     try std.testing.expectEqual(@as(usize, 1_920), plan.canvas_bytes);
 96     try std.testing.expectEqual(@as(usize, 2_000), plan.output_bytes);
 97     try std.testing.expectEqual(plan.exactLimits(), Limits{
 98         .max_categories = 0,
 99         .max_stack_totals = 0,
100         .max_canvas_bytes = 1_920,
101         .max_output_bytes = 2_000,
102     });
103 }
104 
105 test "ASCII render plan rejects every resource limit" {
106     const plan = Plan{
107         .width = 24,
108         .height = 8,
109         .categories = 1,
110         .stack_totals = 1,
111         .canvas_bytes = 192,
112         .output_bytes = 200,
113     };
114     const exact = plan.exactLimits();
115     try plan.require(exact);
116     try std.testing.expectError(error.AsciiCategoryCapacityExceeded, plan.require(.{
117         .max_categories = 0,
118         .max_stack_totals = exact.max_stack_totals,
119         .max_canvas_bytes = exact.max_canvas_bytes,
120         .max_output_bytes = exact.max_output_bytes,
121     }));
122     try std.testing.expectError(error.AsciiStackTotalCapacityExceeded, plan.require(.{
123         .max_categories = exact.max_categories,
124         .max_stack_totals = 0,
125         .max_canvas_bytes = exact.max_canvas_bytes,
126         .max_output_bytes = exact.max_output_bytes,
127     }));
128     try std.testing.expectError(error.AsciiCanvasByteCapacityExceeded, plan.require(.{
129         .max_categories = exact.max_categories,
130         .max_stack_totals = exact.max_stack_totals,
131         .max_canvas_bytes = exact.max_canvas_bytes - 1,
132         .max_output_bytes = exact.max_output_bytes,
133     }));
134     try std.testing.expectError(error.AsciiOutputByteCapacityExceeded, plan.require(.{
135         .max_categories = exact.max_categories,
136         .max_stack_totals = exact.max_stack_totals,
137         .max_canvas_bytes = exact.max_canvas_bytes,
138         .max_output_bytes = exact.max_output_bytes - 1,
139     }));
140 }