tiny.syn.span.storage
Defined in span.
The buffer that receives the marks of each line: one region, allocated when the buffer is created, freed when it is destroyed, and reused for every line in between.
API (2)
Types and contracts
Public types and contracts.
Status: A caller reads it fromStorage.statusto check sizes and to count dropped lines, as the README example and the tests do.Storage: A caller creates one before scanning and passes it to everyhighlightandhighlightLinecall.
Source
Source: lib/syn/src/span/root.zig:11
zig
pub const storage = @import("storage.zig");Source: lib/syn/src/span/storage.zig
zig
//! The buffer that receives the marks of each line: one region, allocated when the buffer is//! created, freed when it is destroyed, and reused for every line in between. A program that marks//! up text inside a loop that must not allocate needs all of that memory taken up front, and a//! check that nothing is taken later. The buffer cannot know in advance how long a line will be, so//! it needs a rule for a line that does not fit. Marks arrive one at a time, some from a routine//! that starts partway through a line, so they need a common origin and a check that they stay in//! order.//!//! `Storage` passes through three stages (*phase*): creation takes the allocator and allocates the//! region, activation makes it ready for scanning, and destruction frees it, and every scanning//! call checks that the storage is active. A line longer than the limit keeps none of its marked//! byte ranges (*span*). The storage counts such a line, and the scan still runs so that its effect//! on later lines stays exact. Spans must arrive in order, touching spans of one kind merge, and an//! offset added to each appended span (*base*) lets a routine scan the tail of a line. A//! compile-time record next to the type (*capacity claim*) states what it allocates, how that size//! follows from the limit, and what a long line does, and the repository's allocation checks read//! it.//!//! - *discarded line*: a line too long for its marks to be kept//! - *text limit*: the longest line, in bytes, whose marks the storage keeps//! - *span storage*: the caller's buffer that receives one line's marks//! - *span capacity*: the number of marks the storage holdsconst std = @import("std");const alloc_phase = @import("alloc_phase");const capacity_mod = @import("capacity.zig");const model = @import("model.zig");/// A caller reads it from `Storage.status` to check sizes and to count dropped lines, as the README/// example and the tests do. The value is a copy of the sizes and counters of span storage at one/// moment.pub const Status = struct { /// The phase of the storage when the copy was taken. phase: alloc_phase.capacity.Phase, /// The bytes of the span region, as `Capacity.storage_bytes` gives them. storage_bytes: usize, /// The text limit the storage was created with. max_text_bytes: usize, /// The number of spans the region holds. span_capacity: usize, /// The number of spans held for the current line. The count is zero after a discarded line. span_count: usize, /// The high-water mark of spans per line: the most spans any one line has held since the /// storage was created. high_water_spans: usize, /// The number of discarded lines since the storage was created. The count stops at the largest /// `u64`. discarded_line_count: u64,};/// A caller creates one before scanning and passes it to every `highlight` and `highlightLine`/// call. Span storage holds the spans of the most recent line in one region allocated at creation./// The storage lifecycle runs `init`, then `activate`, then any number of scans, then `deinit`. No/// scan allocates once it is active. The storage offers no locking, so one storage serves one/// thread at a time.pub const Storage = struct { /// The phase: `initialization` after `init`, `steady` after `activate`, and `teardown` after /// `deinit`. Every scanning call asserts `steady`. phase: alloc_phase.capacity.Phase, /// The sizes derived from the caller's limits at `init`. capacity: capacity_mod.Capacity, /// The one region allocated at `init`, aligned for `Span`, and empty when the limit is zero. /// `deinit` frees it. bytes: []align(capacity_mod.storage_alignment) u8, /// The spans of the current line, kept in `bytes` read as an array of `Span`. The capacity /// equals the span capacity and never grows. spans: std.ArrayList(model.Span), /// The length in bytes of the current line, set by `prepare`. text_bytes: usize = 0, /// The base: the offset `append` adds to each span, reset to zero at the start of every line. base: usize = 0, /// The end of the last span appended for the current line. On a discarded line, `append` still /// moves it to the end of each span it drops. `append` checks that each new span starts at or /// after it. last_end: usize = 0, /// Whether the spans of the current line are kept, `.complete` until a line is discarded. materialization: model.Materialization = .complete, /// The high-water mark, starting at zero. high_water_spans: usize = 0, /// The number of discarded lines since creation, starting at zero and stopping at the largest /// `u64`. discarded_line_count: u64 = 0, /// A caller names the limit type through `Storage`. The type is the same type as /// `capacity.Limits`. pub const Limits: type = capacity_mod.Limits; /// A caller names the size type through `Storage`. The type is the same type as /// `capacity.Capacity`. pub const Capacity: type = capacity_mod.Capacity; /// A caller of `prepare` names the error through `Storage`. The error set is the same error set /// as `model.Exhaustion`. `prepare` returns it for a line longer than the text limit. pub const Exhaustion: type = model.Exhaustion; /// A caller of `init` handles these errors. The error set holds the errors `init` can return: /// `OutOfMemory` from the allocator, or `CapacityOverflow` for a limit too large to size. pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError; /// The repository's allocation checks and tests read it to confirm what the storage allocates /// and when. The declaration is the capacity claim of span storage, fixed at compile time. The /// claim states that the storage holds ordered span values for the steady phase, and that the /// caller's text and scanner state lie outside it. The limit comes from the caller, and the /// retained bytes equal `max_text_bytes` times the size of one `Span` exactly. A line over the /// limit drops its spans while the scan still advances the state. Scanners write only into the /// region, and scanning crosses no operating-system boundary. `activate` seals the storage /// against allocation, and `deinit` ends its life. Each named obligation, such as /// `syn_span_capacity` or `syn_span_boundaries`, points to a test that witnesses one part of /// the claim. pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "syn.span_storage", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "ordered_syntax_span_descriptors", .lifetime = .steady, .detail = "ordered syntax span descriptors", }, }, .excluded = &.{ "caller-owned text bytes", "caller-owned multiline scanner state", "Chic terminal cells and render scratch", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "max_text_bytes", "max_text_bytes"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(model.Span, "span"), }, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 1, }}, }, .overload = .{ .kind = .drop, .detail = "over-capacity lines drop span materialization while exact scanning advances state", }, .risks = .{ .transitive = .{ .status = .witnessed, .detail = "all scanners append only into the activated span region", }, .foreign = .{ .status = .excluded, .detail = "syntax scanning crosses no operating-system or foreign boundary", }, }, .obligations = &.{ .{ .key = "syn_span_capacity", .role = .capacity_model }, .{ .key = "syn_span_acquisition", .role = .custom }, .{ .key = "syn_span_oom", .role = .custom }, .{ .key = "syn_span_boundaries", .role = .overload }, .{ .key = "syn_span_reuse", .role = .custom }, .{ .key = "syn_span_sealed_overload", .role = .overload }, .{ .key = "syn_span_sealed_transitive_risk", .role = .transitive_risk }, .{ .key = "syn_span_sealed_foreign_risk", .role = .foreign_risk }, .{ .key = "syn_span_root", .role = .custom }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, }, }; /// A caller creates the storage once, before any scan, with the allocator that will later free /// it. The call derives the sizes from `limits` and allocates one region of `storage_bytes` /// from `allocator`, aligned for `Span`. The call allocates nothing when the limit is zero. The /// call returns the storage in the initialization phase, so `activate` must come before any /// scan. The call fails with `error.OutOfMemory` when the allocator fails, or /// `error.CapacityOverflow` when the size does not fit in a `usize`, and then holds nothing. /// The same allocator must later be passed to `deinit`. pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage { const capacity = try Capacity.derive(limits); const bytes = if (capacity.storage_bytes == 0) @as([]align(capacity_mod.storage_alignment) u8, &.{}) else try allocator.alignedAlloc( u8, .fromByteUnits(capacity_mod.storage_alignment), capacity.storage_bytes, ); return .{ .phase = .initialization, .capacity = capacity, .bytes = bytes, .spans = .initBuffer(typedSlice(bytes, capacity.span_capacity)), }; } /// A caller calls it once after `init` to make the storage ready for scanning. The call moves /// the storage from initialization to steady. The caller must call it exactly once, right after /// `init`. After activation the storage allocates nothing more. pub fn activate(self: *Storage) void { std.debug.assert(self.phase == .initialization); self.assertStorage(); self.phase = .steady; } /// The scan calls it at the start of each line, so a caller of `highlight` or `highlightLine` /// never calls it directly. The call clears the spans of the previous line and records the /// length of the next one. For a line longer than the text limit, the call marks the line /// discarded, adds one to the discarded count, and fails with /// `error.MaterializationCapacityExceeded`. A line within the limit is marked complete. The /// call requires the steady phase. pub fn prepare(self: *Storage, text_bytes: usize) Exhaustion!void { std.debug.assert(self.phase == .steady); self.spans.clearRetainingCapacity(); self.text_bytes = text_bytes; self.base = 0; self.last_end = 0; if (text_bytes > self.capacity.limits.max_text_bytes) { self.materialization = .discarded; self.discarded_line_count +|= 1; self.assertStorage(); return error.MaterializationCapacityExceeded; } self.materialization = .complete; self.assertStorage(); } /// The scan uses it to record spans for the tail of a line after a carried comment or string /// closes. The call sets the base for every later appended span and returns the previous base, /// so the caller can put it back. The base may not exceed the length of the line. pub fn setBase(self: *Storage, base: usize) usize { std.debug.assert(self.phase == .steady); std.debug.assert(base <= self.text_bytes); const previous = self.base; self.base = base; return previous; } /// Scanners call it once for each token they mark. The call records one span, shifted by the /// base. The call ignores a span of zero length. Each span must start at or after the end of /// the previous one and end within the line, and the call asserts both. The call merges a span /// into the previous one when the two touch and share a kind. On a discarded line, the call /// tracks only where the span ends and stores nothing. The call never allocates, because a line /// within the limit cannot need more spans than the region holds. pub fn append(self: *Storage, span: model.Span) void { std.debug.assert(self.phase == .steady); if (span.start >= span.end) return; std.debug.assert(span.end <= self.text_bytes - self.base); const adjusted = model.Span{ .start = self.base + span.start, .end = self.base + span.end, .kind = span.kind, }; std.debug.assert(self.last_end <= adjusted.start); if (self.materialization == .discarded) { self.last_end = adjusted.end; return; } if (self.spans.items.len != 0) { const last = &self.spans.items[self.spans.items.len - 1]; std.debug.assert(last.end == self.last_end); std.debug.assert(last.end <= adjusted.start); if (last.end == adjusted.start and last.kind == adjusted.kind) { last.end = adjusted.end; self.last_end = adjusted.end; return; } } std.debug.assert(self.spans.items.len < self.spans.capacity); self.spans.appendAssumeCapacity(adjusted); self.last_end = adjusted.end; self.high_water_spans = @max(self.high_water_spans, self.spans.items.len); } /// A renderer reads the spans of the current line here after each scan. The call returns the /// spans of the most recent line, in order. The slice points into the storage, and the next /// scan overwrites it. The slice is empty after a discarded line. The call requires the steady /// phase. pub fn items(self: *const Storage) []const model.Span { std.debug.assert(self.phase == .steady); return self.spans.items; } /// Tests use it to check that a scan marked a given token with a given kind. The call returns /// true when some span of the current line has kind `kind` and covers exactly the bytes `token` /// in `text`. `text` must be the line that was scanned. The function checks every span in turn, /// so its cost grows with the number of spans. Every call in the repository is in a test. pub fn contains( self: *const Storage, text: []const u8, kind: model.Kind, token: []const u8, ) bool { std.debug.assert(self.phase == .steady); for (self.spans.items) |span| { if (span.kind == kind and span.end <= text.len and std.mem.eql(u8, text[span.start..span.end], token)) return true; } return false; } /// A caller reads it to check sizes and to count discarded lines. The call returns a `Status` /// copy of the sizes and counters. The call works in every phase and changes nothing. pub fn status(self: *const Storage) Status { return .{ .phase = self.phase, .storage_bytes = self.capacity.storage_bytes, .max_text_bytes = self.capacity.limits.max_text_bytes, .span_capacity = self.capacity.span_capacity, .span_count = self.spans.items.len, .high_water_spans = self.high_water_spans, .discarded_line_count = self.discarded_line_count, }; } /// A caller releases the region when it has finished marking up text. The call frees the region /// with `allocator`, which must be the allocator given to `init`. The call moves the storage to /// teardown and empties its fields, so a second call trips an assertion. pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void { std.debug.assert(self.phase != .teardown); self.assertStorage(); self.phase = .teardown; allocator.free(self.bytes); self.bytes = &.{}; self.spans = .empty; self.text_bytes = 0; self.base = 0; self.last_end = 0; self.materialization = .complete; } fn assertStorage(self: *const Storage) void { std.debug.assert(self.bytes.len == self.capacity.storage_bytes); std.debug.assert(self.spans.capacity == self.capacity.span_capacity); std.debug.assert(self.spans.items.len <= self.spans.capacity); std.debug.assert(self.high_water_spans <= self.spans.capacity); std.debug.assert(self.base <= self.text_bytes); std.debug.assert(self.last_end <= self.text_bytes); switch (self.materialization) { .complete => { std.debug.assert( self.text_bytes <= self.capacity.limits.max_text_bytes, ); if (self.spans.items.len == 0) { std.debug.assert(self.last_end == 0); } else { std.debug.assert( self.last_end == self.spans.items[self.spans.items.len - 1].end, ); } }, .discarded => { std.debug.assert( self.text_bytes > self.capacity.limits.max_text_bytes, ); std.debug.assert(self.spans.items.len == 0); }, } }};fn typedSlice( bytes: []align(capacity_mod.storage_alignment) u8, count: usize,) []model.Span { const region: []align(@alignOf(model.Span)) u8 = @alignCast(bytes); return std.mem.bytesAsSlice(model.Span, region)[0..count];}fn checkInitFailures(allocator: std.mem.Allocator) !void { var storage = try Storage.init(allocator, .{ .max_text_bytes = 40 }); storage.deinit(allocator);}test "syntax span storage acquires one exact region" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(Storage, "syn_span_acquisition"), null, null, null, null, null, null, ); } var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const limits = capacity_mod.Limits{ .max_text_bytes = 40 }; const capacity = try capacity_mod.Capacity.derive(limits); var storage = try Storage.init(counting.allocator(), limits); defer storage.deinit(counting.allocator()); try std.testing.expectEqual(@as(usize, 1), counting.alloc_index); try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes); try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase); storage.activate(); try std.testing.expectEqual(@intFromPtr(storage.bytes.ptr), @intFromPtr(storage.spans.allocatedSlice().ptr));}test "syntax span storage retries after every allocation failure" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(Storage, "syn_span_oom"), null, null, null, null, null, null, ); } try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});}comptime { alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);}Audit
| Definitions | 1 |
|---|---|
| Public names | 1 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |