lib/chant/src/parse/state/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const chant = @import("../../root.zig");
  4 const ast = @import("../../ast/root.zig");
  5 const capacity_mod = @import("capacity.zig");
  6 const origin = @import("origin.zig");
  7 
  8 const Token = chant.token.Token;
  9 
 10 const NodeExhaustion = error{
 11     NodeCapacityExceeded,
 12     SurveyTokensMismatch,
 13 };
 14 const PlacementExhaustion = error{NodeCapacityExceeded};
 15 
 16 /// The record stores the address and length of one lexed token slice, and a
 17 /// token limit equal to that length. The record comes from `survey(tokens)` (a
 18 /// first pass counting what a second pass will store). The caller sizes parser
 19 /// node storage (a byte buffer the caller allocates) from its limit, and passes
 20 /// the record to parsing, so the storage can check that it parses the tokens it
 21 /// was sized for. Admission into parser node storage compares both with the
 22 /// slice it receives and returns `error.SurveyTokensMismatch` on any
 23 /// difference. The token slice stays at the same address and unchanged from the
 24 /// survey through parsing.
 25 pub const Survey = struct {
 26     tokens_ptr: [*]const Token,
 27     tokens_len: usize,
 28     limits: Limits,
 29 };
 30 
 31 /// The storage divides one buffer the caller supplies into slots for up to two
 32 /// expression nodes, one statement node and two type nodes per admitted token,
 33 /// along with the source token and cached inferred type of each node. A caller
 34 /// allocates one aligned buffer of `Capacity.storage_bytes`, hands it to
 35 /// `init`, activates it, and passes it to `parse`, and every node of the
 36 /// resulting syntax tree (the parsed translation unit) lives here. The buffer
 37 /// belongs to the caller, and `deinit` hands it back. Each parse starts by
 38 /// clearing the node counts, so a second parse into the same storage
 39 /// invalidates every node pointer from the first, and `deinit` invalidates them
 40 /// as well. A request for a node when its slots are full returns
 41 /// `error.NodeCapacityExceeded` before anything is written. The order of use is
 42 /// `init`, `activate`, any number of parses, then `deinit`.
 43 pub const Storage = struct {
 44     phase: alloc_phase.capacity.Phase,
 45     capacity: capacity_mod.Capacity,
 46     storage: @This().Storage,
 47     expressions: []ast.Expr,
 48     statements: []ast.Stmt,
 49     types: []ast.Type,
 50     type_origins: []origin.TypeOrigin,
 51     expression_origins: []usize,
 52     expression_types: []?*const ast.Type,
 53     type_charges: []u8,
 54     admitted_tokens_len: usize,
 55     expression_count: usize,
 56     statement_count: usize,
 57     type_count: usize,
 58 
 59     pub const storage_alignment: usize = capacity_mod.storage_alignment;
 60     pub const Storage = []align(storage_alignment) u8;
 61     pub const Limits: type = capacity_mod.Limits;
 62     pub const Capacity: type = capacity_mod.Capacity;
 63     pub const Exhaustion: type = NodeExhaustion;
 64     pub const InitError = capacity_mod.DeriveError || error{StorageTooShort};
 65     pub const work_limits: alloc_phase.capacity.WorkLimits = .{
 66         .transition_steps_max = std.math.maxInt(usize),
 67         .cleanup_steps_per_call_max = 0,
 68         .cleanup_calls_at_capacity_max = 0,
 69     };
 70     pub const claim: alloc_phase.capacity.Declaration = .{
 71         .source = .{
 72             .id = "chant.parser_node_storage",
 73             .kind = .phase_static,
 74             .limit_source = .caller,
 75             .storage = .{
 76                 .covered = &.{
 77                     .{
 78                         .id = "at_most_two_expression_nodes_per_lexed_token",
 79                         .lifetime = .steady,
 80                         .detail = "at most two expression nodes per lexed token",
 81                     },
 82                     .{
 83                         .id = "at_most_one_statement_node_per_lexed_token",
 84                         .lifetime = .steady,
 85                         .detail = "at most one statement node per lexed token",
 86                     },
 87                     .{
 88                         .id = "at_most_two_dynamic_type_nodes_per_lexed_token",
 89                         .lifetime = .steady,
 90                         .detail = "at most two dynamic type nodes per lexed token",
 91                     },
 92                     .{
 93                         .id = "node_origin_inferred_type_cache_and_two_charge_toke_1bf7f8a5a1ad",
 94                         .lifetime = .steady,
 95                         .detail = "node origin, inferred-type cache, and two-charge token ledger metadata",
 96                     },
 97                 },
 98                 .excluded = &.{
 99                     "list and map backing bytes plus their container metadata",
100                     "object bindings, scopes, and decoded token-text bytes",
101                     "caller-owned lexed tokens and their borrowed source and file bytes",
102                     "allocator metadata for caller acquisition and excluded parser allocations",
103                     "Choir context and lowered IR storage",
104                 },
105             },
106             .capacity = .{
107                 .inputs = &.{
108                     alloc_phase.capacity.bindInput(capacity_mod.Limits, "tokens", "tokens"),
109                 },
110                 .type_selectors = &.{
111                     alloc_phase.capacity.bindType(ast.Expr, "expr"),
112                     alloc_phase.capacity.bindType(ast.Stmt, "stmt"),
113                     alloc_phase.capacity.bindType(ast.Type, "type"),
114                     alloc_phase.capacity.bindType(origin.TypeOrigin, "typeorigin"),
115                     alloc_phase.capacity.bindType(usize, "usize"),
116                     alloc_phase.capacity.bindType(?*const ast.Type, "type_type"),
117                     alloc_phase.capacity.bindType(u8, "u8"),
118                 },
119                 .nodes = &.{
120                     .{ .input = 0 },
121                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
122                     .{ .scale = .{ .node = 1, .coefficient = .{ .size_of_concrete_type = 0 } } },
123                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 1 } } },
124                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
125                     .{ .scale = .{ .node = 4, .coefficient = .{ .size_of_concrete_type = 2 } } },
126                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
127                     .{ .scale = .{ .node = 6, .coefficient = .{ .size_of_concrete_type = 3 } } },
128                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
129                     .{ .scale = .{ .node = 8, .coefficient = .{ .size_of_concrete_type = 4 } } },
130                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
131                     .{ .scale = .{ .node = 10, .coefficient = .{ .size_of_concrete_type = 5 } } },
132                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 6 } } },
133                     .{ .constant = 0 },
134                     .{ .alignment = .{ .node = 13, .alignment = .{ .concrete_type = 0 } } },
135                     .{ .add = .{ .left = 14, .right = 2 } },
136                     .{ .alignment = .{ .node = 15, .alignment = .{ .concrete_type = 1 } } },
137                     .{ .add = .{ .left = 16, .right = 3 } },
138                     .{ .alignment = .{ .node = 17, .alignment = .{ .concrete_type = 2 } } },
139                     .{ .add = .{ .left = 18, .right = 5 } },
140                     .{ .alignment = .{ .node = 19, .alignment = .{ .concrete_type = 3 } } },
141                     .{ .add = .{ .left = 20, .right = 7 } },
142                     .{ .alignment = .{ .node = 21, .alignment = .{ .concrete_type = 4 } } },
143                     .{ .add = .{ .left = 22, .right = 9 } },
144                     .{ .alignment = .{ .node = 23, .alignment = .{ .concrete_type = 5 } } },
145                     .{ .add = .{ .left = 24, .right = 11 } },
146                     .{ .alignment = .{ .node = 25, .alignment = .{ .concrete_type = 6 } } },
147                     .{ .add = .{ .left = 26, .right = 12 } },
148                     .{ .alignment = .{ .node = 27, .alignment = .{ .literal = 1 } } },
149                 },
150                 .assertions = &.{.{
151                     .scope = .closure_total,
152                     .measure = .retained,
153                     .relation = .exact,
154                     .expression = 28,
155                 }},
156             },
157             .overload = .{
158                 .kind = .reject_before_mutation,
159                 .detail = "short regions and mismatched or over-limit token surveys reject before node placement",
160             },
161             .risks = .{
162                 .transitive = .{
163                     .status = .witnessed,
164                     .detail = "all parser node creation dispatches exhaustively into the three typed slices",
165                 },
166                 .foreign = .{
167                     .status = .excluded,
168                     .detail = "node placement reads borrowed memory and crosses no foreign boundary",
169                 },
170             },
171             .work = .{ .equation = "each node placement is one checked slice write bounded by lexed tokens" },
172             .dependencies = &.{"chant.lexed_token_storage"},
173             .obligations = &.{
174                 .{ .key = "chant_parser_nodes_capacity", .role = .capacity_model },
175                 .{ .key = "chant_parser_nodes_boundary", .role = .overload },
176                 .{ .key = "chant_parser_nodes_oom", .role = .custom },
177                 .{ .key = "chant_parser_nodes_identity", .role = .overload },
178                 .{ .key = "chant_parser_nodes_prepublication", .role = .overload },
179                 .{ .key = "chant_parser_nodes_sealed_transitive_risk", .role = .transitive_risk },
180                 .{ .key = "chant_parser_nodes_sealed_foreign_risk", .role = .foreign_risk },
181                 .{ .key = "chant_parser_nodes_sealed_work_bound", .role = .work_bound },
182                 .{ .key = "chant_parser_nodes_differential", .role = .transitive_risk },
183                 .{ .key = "chant_parser_nodes_adversarial", .role = .transitive_risk },
184                 .{ .key = "chant_parser_type_charge", .role = .transitive_risk },
185                 .{ .key = "chant_parser_type_cache", .role = .transitive_risk },
186             },
187         },
188         .bindings = .{
189             .owner = @This(),
190             .seal = .{
191                 .family = alloc_phase.capacity.selector(@This().activate),
192                 .premise = .{
193                     .class = .checked_semantic_fact,
194                     .authority = .checker,
195                 },
196             },
197             .teardown = .{
198                 .family = alloc_phase.capacity.selector(@This().deinit),
199                 .premise = .{
200                     .class = .checked_semantic_fact,
201                     .authority = .checker,
202                 },
203             },
204         },
205     };
206 
207     pub const Status = struct {
208         expressions: usize,
209         statements: usize,
210         types: usize,
211     };
212 
213     pub fn init(
214         storage: @This().Storage,
215         limits: capacity_mod.Limits,
216     ) InitError!@This() {
217         const capacity = try capacity_mod.Capacity.derive(limits);
218         if (storage.len < capacity.storage_bytes) return error.StorageTooShort;
219         const owned = storage[0..capacity.storage_bytes];
220         return .{
221             .phase = .initialization,
222             .capacity = capacity,
223             .storage = owned,
224             .expressions = typedSlice(
225                 ast.Expr,
226                 owned,
227                 capacity.expressions_offset,
228                 capacity.expressions,
229             ),
230             .statements = typedSlice(
231                 ast.Stmt,
232                 owned,
233                 capacity.statements_offset,
234                 capacity.statements,
235             ),
236             .types = typedSlice(
237                 ast.Type,
238                 owned,
239                 capacity.types_offset,
240                 capacity.types,
241             ),
242             .type_origins = typedSlice(
243                 origin.TypeOrigin,
244                 owned,
245                 capacity.type_origins_offset,
246                 capacity.types,
247             ),
248             .expression_origins = typedSlice(
249                 usize,
250                 owned,
251                 capacity.expression_origins_offset,
252                 capacity.expressions,
253             ),
254             .expression_types = typedSlice(
255                 ?*const ast.Type,
256                 owned,
257                 capacity.expression_types_offset,
258                 capacity.expressions,
259             ),
260             .type_charges = typedSlice(
261                 u8,
262                 owned,
263                 capacity.type_charges_offset,
264                 capacity.limits.tokens,
265             ),
266             .admitted_tokens_len = 0,
267             .expression_count = 0,
268             .statement_count = 0,
269             .type_count = 0,
270         };
271     }
272 
273     pub fn activate(self: *@This()) void {
274         std.debug.assert(self.phase == .initialization);
275         self.assertStorage();
276         self.phase = .steady;
277     }
278 
279     pub fn admit(
280         self: *@This(),
281         token_survey: Survey,
282         tokens: []const Token,
283     ) NodeExhaustion!void {
284         std.debug.assert(self.phase == .steady);
285         try requireIdentity(token_survey, tokens);
286         if (token_survey.limits.tokens > self.capacity.limits.tokens) {
287             return error.NodeCapacityExceeded;
288         }
289         @memset(self.type_charges, 0);
290         self.admitted_tokens_len = tokens.len;
291         self.expression_count = 0;
292         self.statement_count = 0;
293         self.type_count = 0;
294     }
295 
296     pub fn createExpr(self: *@This(), value: ast.Expr) PlacementExhaustion!*ast.Expr {
297         return self.createExprAt(value, null);
298     }
299 
300     pub fn createExprAt(
301         self: *@This(),
302         value: ast.Expr,
303         token_index: ?usize,
304     ) PlacementExhaustion!*ast.Expr {
305         std.debug.assert(self.phase == .steady);
306         if (self.expression_count >= self.expressions.len) {
307             return error.NodeCapacityExceeded;
308         }
309         if (token_index) |index| {
310             if (index >= self.admitted_tokens_len) {
311                 return error.NodeCapacityExceeded;
312             }
313         }
314         const result = &self.expressions[self.expression_count];
315         self.expression_origins[self.expression_count] = token_index orelse
316             origin.no_token;
317         self.expression_types[self.expression_count] = null;
318         self.expression_count += 1;
319         result.* = value;
320         return result;
321     }
322 
323     pub fn createStmt(self: *@This(), value: ast.Stmt) PlacementExhaustion!*ast.Stmt {
324         std.debug.assert(self.phase == .steady);
325         if (self.statement_count >= self.statements.len) {
326             return error.NodeCapacityExceeded;
327         }
328         const result = &self.statements[self.statement_count];
329         self.statement_count += 1;
330         result.* = value;
331         return result;
332     }
333 
334     pub fn createType(
335         self: *@This(),
336         value: ast.Type,
337         placement: origin.TypePlacement,
338     ) PlacementExhaustion!*ast.Type {
339         std.debug.assert(self.phase == .steady);
340         if (self.type_count >= self.types.len) return error.NodeCapacityExceeded;
341         if (!self.validTypePlacement(placement)) {
342             return error.NodeCapacityExceeded;
343         }
344         if (self.type_charges[placement.charged] >= 2) {
345             return error.NodeCapacityExceeded;
346         }
347         const result = &self.types[self.type_count];
348         self.type_charges[placement.charged] += 1;
349         self.type_origins[self.type_count] = placement.origin;
350         self.type_count += 1;
351         result.* = value;
352         return result;
353     }
354 
355     pub fn typeOrigin(
356         self: *const @This(),
357         value: *const ast.Type,
358     ) ?origin.TypeOrigin {
359         const index = pointerIndex(ast.Type, self.types, value) orelse return null;
360         std.debug.assert(index < self.type_count);
361         return self.type_origins[index];
362     }
363 
364     pub fn expressionOrigin(
365         self: *const @This(),
366         value: *const ast.Expr,
367     ) usize {
368         const index = pointerIndex(ast.Expr, self.expressions, value) orelse
369             unreachable;
370         std.debug.assert(index < self.expression_count);
371         const token_index = self.expression_origins[index];
372         std.debug.assert(token_index != origin.no_token);
373         return token_index;
374     }
375 
376     pub fn inferredType(
377         self: *const @This(),
378         value: *const ast.Expr,
379     ) ?*const ast.Type {
380         const index = pointerIndex(ast.Expr, self.expressions, value) orelse
381             unreachable;
382         std.debug.assert(index < self.expression_count);
383         return self.expression_types[index];
384     }
385 
386     pub fn setInferredType(
387         self: *@This(),
388         value: *const ast.Expr,
389         inferred: *const ast.Type,
390     ) void {
391         const index = pointerIndex(ast.Expr, self.expressions, value) orelse
392             unreachable;
393         std.debug.assert(index < self.expression_count);
394         std.debug.assert(self.expression_types[index] == null);
395         self.expression_types[index] = inferred;
396     }
397 
398     pub fn status(self: *const @This()) Status {
399         std.debug.assert(self.phase == .steady);
400         return .{
401             .expressions = self.expression_count,
402             .statements = self.statement_count,
403             .types = self.type_count,
404         };
405     }
406 
407     pub fn deinit(self: *@This()) @This().Storage {
408         std.debug.assert(self.phase == .steady);
409         self.phase = .teardown;
410         const storage = self.storage;
411         self.* = undefined;
412         return storage;
413     }
414 
415     fn assertStorage(self: *const @This()) void {
416         const expected = capacity_mod.Capacity.derive(
417             self.capacity.limits,
418         ) catch unreachable;
419         std.debug.assert(std.meta.eql(expected, self.capacity));
420         std.debug.assert(self.storage.len == self.capacity.storage_bytes);
421         std.debug.assert(self.expressions.len == self.capacity.expressions);
422         std.debug.assert(self.statements.len == self.capacity.statements);
423         std.debug.assert(self.types.len == self.capacity.types);
424         std.debug.assert(self.type_origins.len == self.capacity.types);
425         std.debug.assert(self.expression_origins.len == self.capacity.expressions);
426         std.debug.assert(self.expression_types.len == self.capacity.expressions);
427         std.debug.assert(self.type_charges.len == self.capacity.limits.tokens);
428     }
429 
430     fn validTypePlacement(
431         self: *const @This(),
432         placement: origin.TypePlacement,
433     ) bool {
434         if (placement.charged >= self.admitted_tokens_len) return false;
435         if (placement.origin.primary >= self.admitted_tokens_len) return false;
436         if (placement.origin.secondary != origin.no_token and
437             placement.origin.secondary >= self.admitted_tokens_len)
438         {
439             return false;
440         }
441         return placement.charged == placement.origin.primary or
442             placement.charged == placement.origin.secondary;
443     }
444 };
445 
446 pub const Limits = capacity_mod.Limits;
447 pub const Capacity = capacity_mod.Capacity;
448 pub const CapacityError = capacity_mod.DeriveError;
449 pub const Exhaustion = NodeExhaustion;
450 pub const TypeOrigin = origin.TypeOrigin;
451 pub const TypePlacement = origin.TypePlacement;
452 
453 pub fn survey(tokens: []const Token) Survey {
454     return .{
455         .tokens_ptr = tokens.ptr,
456         .tokens_len = tokens.len,
457         .limits = .{ .tokens = tokens.len },
458     };
459 }
460 
461 fn requireIdentity(token_survey: Survey, tokens: []const Token) Exhaustion!void {
462     if (token_survey.tokens_ptr != tokens.ptr) return error.SurveyTokensMismatch;
463     if (token_survey.tokens_len != tokens.len) return error.SurveyTokensMismatch;
464     if (token_survey.limits.tokens != tokens.len) return error.SurveyTokensMismatch;
465 }
466 
467 fn typedSlice(
468     comptime T: type,
469     storage: Storage.Storage,
470     offset: usize,
471     count: usize,
472 ) []T {
473     const bytes = storage[offset..][0 .. count * @sizeOf(T)];
474     const aligned: []align(@alignOf(T)) u8 = @alignCast(bytes);
475     return std.mem.bytesAsSlice(T, aligned);
476 }
477 
478 fn pointerIndex(
479     comptime T: type,
480     values: []const T,
481     pointer: *const T,
482 ) ?usize {
483     const start = @intFromPtr(values.ptr);
484     const address = @intFromPtr(pointer);
485     const bytes = values.len * @sizeOf(T);
486     if (address < start or address >= start + bytes) return null;
487     const offset = address - start;
488     if (offset % @sizeOf(T) != 0) return null;
489     return offset / @sizeOf(T);
490 }
491 
492 fn initFailures(allocator: std.mem.Allocator) !void {
493     var tokens: [4]Token = undefined;
494     const token_survey = survey(&tokens);
495     const capacity = try Capacity.derive(token_survey.limits);
496     const bytes = try allocator.alignedAlloc(
497         u8,
498         .fromByteUnits(Storage.storage_alignment),
499         capacity.storage_bytes,
500     );
501     defer allocator.free(bytes);
502     var storage = try Storage.init(bytes, token_survey.limits);
503     storage.activate();
504     try storage.admit(token_survey, &tokens);
505     _ = try storage.createExpr(.{
506         .integer_literal = .{ .value = 1, .type = &ast.types.int_type },
507     });
508     _ = try storage.createStmt(.empty);
509     _ = try storage.createType(
510         ast.types.int_type,
511         .{ .charged = 0, .origin = .{ .primary = 0 } },
512     );
513     _ = storage.deinit();
514 }
515 
516 test "parser node storage acquisition retries after allocation failure" {
517     comptime {
518         @stardustClaim(
519             @import("alloc_phase").capacity.witness(Storage, "chant_parser_nodes_oom"),
520             null,
521             null,
522             null,
523             null,
524             null,
525             null,
526         );
527     }
528 
529     try std.testing.checkAllAllocationFailures(
530         std.testing.allocator,
531         initFailures,
532         .{},
533     );
534 }
535 
536 test "parser node storage admits exact capacity and rejects short storage" {
537     comptime {
538         @stardustClaim(
539             @import("alloc_phase").capacity.witness(Storage, "chant_parser_nodes_boundary"),
540             null,
541             null,
542             null,
543             null,
544             null,
545             null,
546         );
547     }
548 
549     var tokens: [2]Token = undefined;
550     const token_survey = survey(&tokens);
551     const capacity = try Capacity.derive(token_survey.limits);
552     var bytes: [4096]u8 align(Storage.storage_alignment) = undefined;
553     try std.testing.expect(capacity.storage_bytes < bytes.len);
554 
555     var exact = try Storage.init(
556         bytes[0..capacity.storage_bytes],
557         token_survey.limits,
558     );
559     exact.activate();
560     try exact.admit(token_survey, &tokens);
561     for (0..capacity.expressions) |value| {
562         _ = try exact.createExpr(.{
563             .integer_literal = .{
564                 .value = @intCast(value),
565                 .type = &ast.types.int_type,
566             },
567         });
568     }
569     for (0..capacity.statements) |_| _ = try exact.createStmt(.empty);
570     for (0..capacity.types) |index| {
571         const token_index = index / 2;
572         _ = try exact.createType(
573             ast.types.int_type,
574             .{
575                 .charged = token_index,
576                 .origin = .{ .primary = token_index },
577             },
578         );
579     }
580     const full = exact.status();
581     try std.testing.expectEqual(capacity.expressions, full.expressions);
582     try std.testing.expectEqual(capacity.statements, full.statements);
583     try std.testing.expectEqual(capacity.types, full.types);
584     try std.testing.expectError(
585         error.NodeCapacityExceeded,
586         exact.createExpr(.{
587             .integer_literal = .{ .value = 0, .type = &ast.types.int_type },
588         }),
589     );
590     try std.testing.expectError(
591         error.NodeCapacityExceeded,
592         exact.createStmt(.empty),
593     );
594     try std.testing.expectError(
595         error.NodeCapacityExceeded,
596         exact.createType(
597             ast.types.int_type,
598             .{ .charged = 0, .origin = .{ .primary = 0 } },
599         ),
600     );
601     _ = exact.deinit();
602 
603     try std.testing.expectError(
604         error.StorageTooShort,
605         Storage.init(
606             bytes[0 .. capacity.storage_bytes - 1],
607             token_survey.limits,
608         ),
609     );
610 
611     const smaller_limits = Limits{ .tokens = token_survey.limits.tokens - 1 };
612     const smaller_capacity = try Capacity.derive(smaller_limits);
613     var smaller = try Storage.init(
614         bytes[0..smaller_capacity.storage_bytes],
615         smaller_limits,
616     );
617     smaller.activate();
618     _ = try smaller.createExpr(.{
619         .integer_literal = .{ .value = 0, .type = &ast.types.int_type },
620     });
621     const before = smaller.status();
622     try std.testing.expectError(
623         error.NodeCapacityExceeded,
624         smaller.admit(token_survey, &tokens),
625     );
626     try std.testing.expectEqual(before, smaller.status());
627     _ = smaller.deinit();
628 }
629 
630 test "parser node storage requires the surveyed token identity" {
631     comptime {
632         @stardustClaim(
633             @import("alloc_phase").capacity.witness(Storage, "chant_parser_nodes_identity"),
634             null,
635             null,
636             null,
637             null,
638             null,
639             null,
640         );
641     }
642 
643     var tokens: [2]Token = undefined;
644     var other_tokens: [2]Token = undefined;
645     std.mem.doNotOptimizeAway(&tokens);
646     std.mem.doNotOptimizeAway(&other_tokens);
647     try std.testing.expect(@intFromPtr(&tokens) != @intFromPtr(&other_tokens));
648 
649     const token_survey = survey(&tokens);
650     const capacity = try Capacity.derive(token_survey.limits);
651     var bytes: [4096]u8 align(Storage.storage_alignment) = undefined;
652     try std.testing.expect(capacity.storage_bytes <= bytes.len);
653     var storage = try Storage.init(
654         bytes[0..capacity.storage_bytes],
655         token_survey.limits,
656     );
657     storage.activate();
658     _ = try storage.createStmt(.empty);
659     const before = storage.status();
660     try std.testing.expectError(
661         error.SurveyTokensMismatch,
662         storage.admit(token_survey, &other_tokens),
663     );
664     try std.testing.expectEqual(before, storage.status());
665     var forged_survey = token_survey;
666     forged_survey.limits.tokens -= 1;
667     try std.testing.expectError(
668         error.SurveyTokensMismatch,
669         storage.admit(forged_survey, &tokens),
670     );
671     try std.testing.expectEqual(before, storage.status());
672     _ = storage.deinit();
673 }
674 
675 test "parser type origins enforce two charges per token" {
676     comptime {
677         @stardustClaim(
678             @import("alloc_phase").capacity.witness(Storage, "chant_parser_type_charge"),
679             null,
680             null,
681             null,
682             null,
683             null,
684             null,
685         );
686     }
687 
688     var tokens: [2]Token = undefined;
689     const token_survey = survey(&tokens);
690     const capacity = try Capacity.derive(token_survey.limits);
691     var bytes: [8192]u8 align(Storage.storage_alignment) = undefined;
692     try std.testing.expect(capacity.storage_bytes <= bytes.len);
693     var storage = try Storage.init(
694         bytes[0..capacity.storage_bytes],
695         token_survey.limits,
696     );
697     storage.activate();
698     try storage.admit(token_survey, &tokens);
699     const placement = TypePlacement{
700         .charged = 0,
701         .origin = .{ .primary = 0 },
702     };
703     _ = try storage.createType(ast.types.int_type, placement);
704     _ = try storage.createType(ast.types.int_type, placement);
705     const before = storage.status();
706     try std.testing.expect(before.types < capacity.types);
707     try std.testing.expectError(
708         error.NodeCapacityExceeded,
709         storage.createType(ast.types.int_type, placement),
710     );
711     try std.testing.expectEqual(before, storage.status());
712     _ = storage.deinit();
713 }
714 
715 comptime {
716     alloc_phase.capacity.requireProvisionedRejectingOwnerShape(Storage);
717 }