lib/reticulum/src/carrier/memory.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const carrier = @import("root.zig");
  4 
  5 const MemoryLimits = struct {
  6     frames_max: usize,
  7 };
  8 
  9 const MemoryCapacity = struct {
 10     frames_max: usize,
 11     storage_bytes: usize,
 12 
 13     pub const DeriveError = error{ InvalidLimit, CapacityOverflow };
 14 
 15     pub fn derive(limits: MemoryLimits) DeriveError!MemoryCapacity {
 16         if (limits.frames_max == 0) return error.InvalidLimit;
 17         const frames_max = limits.frames_max;
 18         const storage_bytes = alloc_phase.capacity.mul(
 19             usize,
 20             frames_max,
 21             @sizeOf(carrier.Frame),
 22         ) catch return error.CapacityOverflow;
 23         return .{ .frames_max = frames_max, .storage_bytes = storage_bytes };
 24     }
 25 };
 26 
 27 pub const Memory = struct {
 28     phase: alloc_phase.capacity.Phase,
 29     capacity: MemoryCapacity,
 30     storage: Storage,
 31     frames: []carrier.Frame,
 32     start: usize = 0,
 33     len: usize = 0,
 34 
 35     pub const storage_alignment: usize = 8;
 36     pub const Storage = []align(storage_alignment) u8;
 37     pub const Limits: type = MemoryLimits;
 38     pub const Capacity: type = MemoryCapacity;
 39     pub const Exhaustion = error{Full};
 40     pub const InitError = Capacity.DeriveError || error{StorageLengthMismatch};
 41     pub const work_limits: alloc_phase.capacity.WorkLimits = .{
 42         .transition_steps_max = 1,
 43         .cleanup_steps_per_call_max = 0,
 44         .cleanup_calls_at_capacity_max = 0,
 45     };
 46     pub const claim: alloc_phase.capacity.Declaration = .{
 47         .source = .{
 48             .id = "reticulum.carrier_memory",
 49             .kind = .phase_static,
 50             .limit_source = .caller,
 51             .storage = .{
 52                 .covered = &.{.{
 53                     .id = "caller_frame_fifo",
 54                     .lifetime = .transferred,
 55                     .detail = "caller storage for a bounded FIFO of inline carrier frames",
 56                 }},
 57                 .excluded = &.{
 58                     "caller frame inputs and returned frame values",
 59                     "interface devices and operating-system transport state",
 60                 },
 61             },
 62             .capacity = .{
 63                 .inputs = &.{alloc_phase.capacity.bindInput(
 64                     Limits,
 65                     "frames_max",
 66                     "frames_max",
 67                 )},
 68                 .type_selectors = &.{alloc_phase.capacity.bindType(carrier.Frame, "frame")},
 69                 .nodes = &.{
 70                     .{ .input = 0 },
 71                     .{ .scale = .{
 72                         .node = 0,
 73                         .coefficient = .{ .size_of_concrete_type = 0 },
 74                     } },
 75                 },
 76                 .assertions = &.{.{
 77                     .scope = .closure_total,
 78                     .measure = .retained,
 79                     .relation = .exact,
 80                     .expression = 1,
 81                 }},
 82             },
 83             .overload = .{
 84                 .kind = .reject_before_mutation,
 85                 .detail = "full frame admission preserves the retained FIFO",
 86             },
 87             .risks = .{
 88                 .transitive = .{
 89                     .status = .excluded,
 90                     .detail = "carrier memory calls no allocating owner",
 91                 },
 92                 .foreign = .{
 93                     .status = .excluded,
 94                     .detail = "carrier memory crosses no operating-system boundary",
 95                 },
 96             },
 97             .work = .{ .equation = "push and pop each perform one bounded transition" },
 98             .obligations = &.{
 99                 .{ .key = "reticulum_carrier_memory_capacity", .role = .capacity_model },
100                 .{ .key = "reticulum_carrier_memory_overload", .role = .overload },
101                 .{ .key = "reticulum_carrier_memory_work", .role = .work_bound },
102             },
103         },
104         .bindings = .{
105             .owner = @This(),
106             .seal = .{
107                 .family = alloc_phase.capacity.selector(@This().activate),
108                 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
109             },
110             .teardown = .{
111                 .family = alloc_phase.capacity.selector(@This().deinit),
112                 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
113             },
114         },
115     };
116 
117     pub fn init(storage: Storage, limits: Limits) InitError!Memory {
118         const capacity = try Capacity.derive(limits);
119         if (storage.len != capacity.storage_bytes) return error.StorageLengthMismatch;
120         const frames = std.mem.bytesAsSlice(carrier.Frame, storage);
121         for (frames) |*frame| frame.* = .{};
122         return .{
123             .phase = .initialization,
124             .capacity = capacity,
125             .storage = storage,
126             .frames = frames,
127         };
128     }
129 
130     pub fn activate(self: *Memory) void {
131         std.debug.assert(self.phase == .initialization);
132         std.debug.assert(self.len == 0);
133         self.phase = .steady;
134     }
135 
136     pub fn push(self: *Memory, frame: carrier.Frame) Exhaustion!void {
137         std.debug.assert(self.phase == .steady);
138         std.debug.assert(self.len <= self.capacity.frames_max);
139         if (self.len == self.capacity.frames_max) return error.Full;
140         const index = (self.start + self.len) % self.capacity.frames_max;
141         self.frames[index] = frame;
142         self.len += 1;
143     }
144 
145     pub fn pop(self: *Memory) ?carrier.Frame {
146         std.debug.assert(self.phase == .steady);
147         std.debug.assert(self.len <= self.capacity.frames_max);
148         if (self.len == 0) return null;
149         const frame = self.frames[self.start];
150         self.start = (self.start + 1) % self.capacity.frames_max;
151         self.len -= 1;
152         return frame;
153     }
154 
155     pub fn count(self: *const Memory) usize {
156         std.debug.assert(self.phase == .steady);
157         return self.len;
158     }
159 
160     pub fn deinit(self: *Memory) Storage {
161         std.debug.assert(self.phase == .steady);
162         self.phase = .teardown;
163         const storage = self.storage;
164         self.* = undefined;
165         return storage;
166     }
167 };
168 
169 comptime {
170     alloc_phase.capacity.requireProvisionedRejectingOwnerShape(Memory);
171 }
172 
173 test "carrier memory admits maximum and rejects maximum plus one" {
174     comptime {
175         @stardustClaim(alloc_phase.capacity.witness(
176             Memory,
177             "reticulum_carrier_memory_capacity",
178         ), null, null, null, null, null, null);
179         @stardustClaim(alloc_phase.capacity.witness(
180             Memory,
181             "reticulum_carrier_memory_overload",
182         ), null, null, null, null, null, null);
183         @stardustClaim(alloc_phase.capacity.witness(
184             Memory,
185             "reticulum_carrier_memory_work",
186         ), null, null, null, null, null, null);
187     }
188     const capacity = comptime MemoryCapacity.derive(.{ .frames_max = 3 }) catch unreachable;
189     var bytes: [capacity.storage_bytes]u8 align(Memory.storage_alignment) = undefined;
190     var memory = try Memory.init(&bytes, .{ .frames_max = 3 });
191     memory.activate();
192     defer _ = memory.deinit();
193     for (0..3) |value| try memory.push(try carrier.Frame.init(&.{@intCast(value)}));
194     try std.testing.expectError(error.Full, memory.push(try carrier.Frame.init("full")));
195     try std.testing.expectEqual(@as(usize, 3), memory.count());
196     for (0..3) |value| {
197         const frame = memory.pop().?;
198         try std.testing.expectEqualSlices(u8, &.{@intCast(value)}, frame.slice());
199     }
200     try std.testing.expect(memory.pop() == null);
201 }
202 
203 test "carrier frame admits maximum and rejects maximum plus one" {
204     var bytes: [carrier.frame_bytes_max + 1]u8 = @splat(0xa5);
205     const frame = try carrier.Frame.init(bytes[0..carrier.frame_bytes_max]);
206     try std.testing.expectEqual(@as(usize, carrier.frame_bytes_max), frame.slice().len);
207     try std.testing.expectError(error.TooLong, carrier.Frame.init(&bytes));
208 }