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 }