Skip to documentation
SLOP

tiny.syn.span.storage

Reference 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.

No direct callersNo direct callsspanstorage
Static calls · unresolved targets: unknown · external targets: unknown.

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

Definitions1
Public names1
Members0
Version26.7.0
Revisiondaab053ee433