Skip to documentation
SLOP

tiny.css.bucket

Reference tiny.css bucket

Defined in tiny.css.

The rule bucket index: one sorted entry per selector, keyed by the strongest key its rightmost compound carries.

API (27)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Called byCallsNo direct callstest sourcelib.css.src.buckettest: bucket index capacity matches a...bucket.Capacityderive
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callersprivate sourcelib.css.src.bucket.Entrybeforebucket.Indexgroup
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.css.src.bucket.Indexedinitprivate sourcelib.css.src.cascade.test.Styledinitbucket.Limitsinspect
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.css.src.bucket.Indexedinitprivate sourcelib.css.src.cascade.test.Styledinitprivate sourcelib.css.src.bucket.StorageassertStoragebucket.Storageactivate
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsbucket.Storageindexbucket.Storageadmits
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.css.src.bucket.Indexeddeinitprivate sourcelib.css.src.bucketcheckBucketStorageInitprivate sourcelib.css.src.cascade.test.Styleddeinitprivate sourcelib.css.src.bucket.StorageassertStoragebucket.Storagedeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsprivate sourcelib.css.src.bucket.Indexedindexprivate sourcelib.css.src.cascade.test.Styledcomputebucket.Storageadmitsbucketbuildbucket.Storageindex
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsprivate sourcelib.css.src.bucket.Indexedinitprivate sourcelib.css.src.bucketcheckBucketStorageInitprivate sourcelib.css.src.cascade.test.Styledinitprivate sourcelib.css.src.bucketentrySlicebucket.Storageinit
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallsNo direct callersprivate sourcelib.css.src.bucketbitbucket.SummaryancestorSensitive
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsbucket.Storageindexprivate sourcelib.css.src.bucketkeyOfbucketbuild
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.css.src.buckettest: an element gathers only the can...private sourcelib.css.src.cascade.test.Styledcomputetest sourcelib.css.src.cascade.testtest: a custom property declared abov...private sourcelib.css.src.bucketcollectbucketmatchElement
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallstest sourcelib.css.src.buckettest: the summary marks ancestor atom...test sourcelib.css.src.buckettest: the summary records the pseudo ...private sourcelib.css.src.bucketabsorbbucketsummarize
Static calls · unresolved targets: 0 · external targets: 0.

Source: lib/css/src/match/engine.zig:17

zig
/// The most classes one element may report to a compound test.pub const max_element_classes: u32 = 64;

Source: lib/css/src/bucket.zig

zig
//! The rule bucket index: one sorted entry per selector, keyed by the strongest//! key its rightmost compound carries.//!//! Matching an element against a whole sheet is a walk over every selector only//! if nothing narrows the candidates first. A selector whose rightmost compound//! names an id, a class, a role, or an element type can only match an element//! carrying that key, so the index groups selectors by key and an element tests//! its own keys plus the universal group. The groups are one sorted array with//! a binary search, so the index is a flat block with no pointers.//!//! The index also answers the question a style sharing cache asks before it//! reuses a computed style: whether a mutation somewhere above or beside an//! element can change what matches. `Summary` holds conservative Bloom words//! over the atoms that appear left of a combinator, plus the two bits that//! say a sheet uses sibling combinators or positional pseudo-classes at all.//! `Summary` also holds `ancestor_pseudo`, an exact `u32` bitset over the//! thirty `PseudoClass` values rather than a Bloom word. A set bit names a//! pseudo-class some selector puts left of a combinator, and a clear bit//! rules that pseudo-class out.const std = @import("std");const alloc_phase = @import("alloc_phase");const atom = @import("atom.zig");const match = @import("match/root.zig");const selector = @import("selector/root.zig");const sheet = @import("sheet.zig");const Allocator = std.mem.Allocator;/// The key spaces a bucket entry lives in, in the order the index sorts them.pub const Space = enum(u8) {    universal = 0,    kind = 1,    role = 2,    class = 3,    identifier = 4,};/// One selector's place in the index.pub const Entry = extern struct {    key: u32 = 0,    rule: u32 = 0,    selector: u32 = 0,    space: u8 = @backingInt(Space.universal),    fn before(left: Entry, right: Entry) bool {        if (left.space != right.space) return left.space < right.space;        if (left.key != right.key) return left.key < right.key;        if (left.rule != right.rule) return left.rule < right.rule;        return left.selector < right.selector;    }};/// The bound one sheet produces, which is its selector count.pub const Limits = struct {    entries: usize = 0,    /// Counts the entries `parsed` needs without writing one.    pub fn inspect(parsed: *const sheet.StyleSheet) Limits {        var total: usize = 0;        for (parsed.rules) |rule| total += rule.selectors.len;        return .{ .entries = total };    }    pub fn grow(left: Limits, right: Limits) Limits {        return .{ .entries = @max(left.entries, right.entries) };    }};/// The byte layout one `Limits` produces.pub const Capacity = struct {    entries: usize = 0,    total_bytes: usize = 0,    pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {        const bytes = std.math.mul(usize, limits.entries, @sizeOf(Entry)) catch            return error.CapacityOverflow;        return .{ .entries = limits.entries, .total_bytes = bytes };    }};const LimitsType = Limits;const CapacityType = Capacity;/// The alignment the index block is acquired at.pub const storage_alignment: usize = @alignOf(Entry);/// The sorted entries plus the sheet arrays a match walk reads.pub const Index = struct {    entries: []const Entry = &.{},    source: *const sheet.StyleSheet,    /// The entries in one key space, or an empty slice when the sheet has none.    pub fn group(self: Index, space: Space, key: u32) []const Entry {        const wanted = Entry{ .space = @backingInt(space), .key = key };        var low: usize = 0;        var high: usize = self.entries.len;        while (low < high) {            const middle = low + (high - low) / 2;            if (Entry.before(self.entries[middle], wanted)) low = middle + 1 else high = middle;        }        var end = low;        while (end < self.entries.len and            self.entries[end].space == wanted.space and            self.entries[end].key == key) : (end += 1)        {}        return self.entries[low..end];    }    fn selectorAt(self: Index, entry: Entry) selector.Selector {        const rule = self.source.rules[entry.rule];        return rule.selectors[entry.selector];    }};/// The outcome of one element's candidate walk.pub const Gather = struct {    count: u32 = 0,    rejected: u32 = 0,};/// The most classes one element carries into a candidate walk.pub const max_element_classes: u32 = match.max_element_classes;/// Writes every selector in `index` that matches `node` into `out`, ordered by/// source order and then specificity. Candidates past `out` are rejected and/// counted rather than dropped silently.pub fn matchElement(    index: Index,    element: match.Element,    node: u32,    out: []match.Match,) Gather {    var found = Gather{};    const context = index.source.context();    collect(index, context, element, node, out, &found, .universal, 0);    const kind = element.kind(element.context, node);    if (kind != 0) collect(index, context, element, node, out, &found, .kind, kind);    const role = element.role(element.context, node);    if (role != 0) collect(index, context, element, node, out, &found, .role, role);    const identifier = element.identifier(element.context, node);    if (identifier != atom.none) collect(index, context, element, node, out, &found, .identifier, identifier);    var held: [max_element_classes]u32 = undefined;    const classes = element.classes(element.context, node, &held, max_element_classes);    std.debug.assert(classes <= max_element_classes);    for (held[0..classes]) |class| {        collect(index, context, element, node, out, &found, .class, class);    }    std.mem.sort(match.Match, out[0..found.count], {}, earlier);    return found;}fn collect(    index: Index,    context: match.Context,    element: match.Element,    node: u32,    out: []match.Match,    found: *Gather,    space: Space,    key: u32,) void {    for (index.group(space, key)) |entry| {        const item = index.selectorAt(entry);        if (!match.matches(context, item, element, node)) continue;        if (found.count == out.len) {            found.rejected += 1;            continue;        }        out[found.count] = .{            .rule = entry.rule,            .selector = entry.selector,            .specificity = item.specificity,            .order = @intCast(index.source.rules[entry.rule].source_order),        };        found.count += 1;    }}fn earlier(_: void, left: match.Match, right: match.Match) bool {    if (left.order != right.order) return left.order < right.order;    return left.specificity.compare(right.specificity) == .lt;}fn keyOf(item: selector.Selector) Entry {    return switch (item.bucket) {        .universal => .{ .space = @backingInt(Space.universal), .key = 0 },        .kind => |key| .{ .space = @backingInt(Space.kind), .key = key },        .role => |key| .{ .space = @backingInt(Space.role), .key = key },        .class => |key| .{ .space = @backingInt(Space.class), .key = key },        .id => |key| .{ .space = @backingInt(Space.identifier), .key = key },    };}/// Fills `entries` from `parsed` and returns the sorted index.pub fn build(entries: []Entry, parsed: *const sheet.StyleSheet) Index {    var used: usize = 0;    for (parsed.rules, 0..) |rule, rule_index| {        for (rule.selectors, 0..) |item, selector_index| {            std.debug.assert(used < entries.len);            var entry = keyOf(item);            entry.rule = @intCast(rule_index);            entry.selector = @intCast(selector_index);            entries[used] = entry;            used += 1;        }    }    std.mem.sort(Entry, entries[0..used], {}, lessThan);    return .{ .entries = entries[0..used], .source = parsed };}fn lessThan(_: void, left: Entry, right: Entry) bool {    return Entry.before(left, right);}/// What a mutation must carry before it can change what a sheet matches.////// The Bloom words are conservative: a set bit means some selector names an/// atom that folds onto that bit left of a combinator, so a consumer that sees/// a clear bit knows no ancestor change with that atom can matter.pub const Summary = struct {    ancestor_kinds: u64 = 0,    ancestor_roles: u64 = 0,    ancestor_classes: u64 = 0,    ancestor_ids: u64 = 0,    /// A set bit means some selector names that pseudo-class left of a    /// combinator, and a clear bit means no selector does. The word is an exact    /// set rather than a conservative Bloom word, unlike the Bloom words above    /// this field.    ancestor_pseudo: u32 = 0,    sibling_sensitive: bool = false,    positional: bool = false,    /// Whether an ancestor carrying `id` in `space` can change matching.    pub fn ancestorSensitive(self: Summary, space: Space, id: u32) bool {        const word = switch (space) {            .universal => return true,            .kind => self.ancestor_kinds,            .role => self.ancestor_roles,            .class => self.ancestor_classes,            .identifier => self.ancestor_ids,        };        return word & bit(id) != 0;    }    /// This predicate returns whether ancestor_pseudo carries    /// selector.bit(class). The check answers whether any rule in the sheet    /// puts the given pseudo-class above a subject, such as :hover in    /// .row:hover .cell. The alternative is rescanning every selector in the    /// stylesheet.    pub fn ancestorPseudoSensitive(self: Summary, class: selector.PseudoClass) bool {        const word = selector.bit(class);        std.debug.assert(word != 0);        return self.ancestor_pseudo & word != 0;    }};fn bit(id: u32) u64 {    return @as(u64, 1) << @truncate(id);}/// Folds every selector in `parsed` into one conservative summary.pub fn summarize(parsed: *const sheet.StyleSheet) Summary {    var out = Summary{};    for (parsed.rules) |rule| {        for (rule.selectors) |item| {            for (item.combinators) |joint| {                if (joint == .next_sibling or joint == .subsequent_sibling) out.sibling_sensitive = true;            }            for (item.compounds, 0..) |compound, position| {                if (compound.nth != 0) out.positional = true;                if (position + 1 == item.compounds.len) continue;                absorb(&out, parsed, compound);            }        }    }    return out;}fn absorb(out: *Summary, parsed: *const sheet.StyleSheet, compound: selector.Compound) void {    out.ancestor_pseudo |= compound.pseudo;    if (compound.kind != 0) out.ancestor_kinds |= bit(compound.kind);    if (compound.role != 0) out.ancestor_roles |= bit(compound.role);    if (compound.id != atom.none) out.ancestor_ids |= bit(compound.id);    var index: u32 = 0;    while (index < compound.class_count) : (index += 1) {        out.ancestor_classes |= bit(parsed.classes[compound.class_first + index]);    }}/// The single aligned block one bucket index owns.pub const Storage = struct {    pub const Limits = LimitsType;    pub const Capacity = CapacityType;    pub const claim: alloc_phase.capacity.Declaration = .{        .source = .{            .id = "css.bucket_index",            .kind = .phase_static,            .limit_source = .caller,            .storage = .{                .covered = &.{                    .{                        .id = "flat_sorted_bucket_entry_output",                        .lifetime = .steady,                        .detail = "flat sorted rule bucket entry output, one entry per sheet selector",                    },                },                .excluded = &.{                    "stylesheet placement holding the rules and selectors the entries address",                    "caller-owned matched rule output buffer",                },            },            .capacity = .{                .inputs = &.{                    alloc_phase.capacity.bindInput(LimitsType, "entries", "entries"),                },                .type_selectors = &.{                    alloc_phase.capacity.bindType(Entry, "entry"),                },                .nodes = &.{                    .{ .input = 0 },                    .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },                    .{ .constant = 0 },                    .{ .alignment = .{ .node = 2, .alignment = .{ .concrete_type = 0 } } },                    .{ .add = .{ .left = 3, .right = 1 } },                    .{ .alignment = .{ .node = 4, .alignment = .{ .literal = 16 } } },                },                .assertions = &.{.{                    .scope = .closure_total,                    .measure = .retained,                    .relation = .exact,                    .expression = 5,                }},            },            .overload = .{                .kind = .reject_before_seal,                .detail = "the entry count is the sheet selector count, which is known before acquisition; filling and sorting the admitted block performs no allocation",            },            .risks = .{                .transitive = .{                    .status = .open,                    .detail = "the sort and the match walk are allocation-free and witnessed but lack a machine-checked call-graph closure certificate",                },                .foreign = .{                    .status = .excluded,                    .detail = "indexing and matching are process-local transformations with no operating-system edge",                },            },            .obligations = &.{                .{ .key = "css_bucket_capacity_model", .role = .capacity_model },                .{ .key = "css_bucket_overload", .role = .overload },                .{ .key = "css_bucket_acquisition", .role = .custom },                .{ .key = "css_bucket_steady_overload", .role = .overload },                .{ .key = "css_bucket_steady_foreign_risk", .role = .foreign_risk },            },        },        .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,                },            },        },    };    phase: alloc_phase.capacity.Phase,    limits: LimitsType,    capacity: CapacityType,    bytes: []align(storage_alignment) u8,    entries: []Entry,    pub fn init(allocator: Allocator, limits: LimitsType) !Storage {        const capacity = try CapacityType.derive(limits);        const bytes = try allocator.alignedAlloc(u8, .fromByteUnits(storage_alignment), capacity.total_bytes);        return .{            .phase = .initialization,            .limits = limits,            .capacity = capacity,            .bytes = bytes,            .entries = entrySlice(bytes, capacity.entries),        };    }    pub fn activate(self: *Storage) void {        std.debug.assert(self.phase == .initialization);        self.assertStorage();        self.phase = .steady;    }    pub fn admits(self: *const Storage, limits: LimitsType) bool {        std.debug.assert(self.phase == .steady);        return limits.entries <= self.capacity.entries;    }    /// Indexes `parsed`, asserting admission rather than growing.    pub fn index(self: *Storage, parsed: *const sheet.StyleSheet) Index {        std.debug.assert(self.phase == .steady);        std.debug.assert(self.admits(LimitsType.inspect(parsed)));        return build(self.entries, parsed);    }    pub fn deinit(self: *Storage, allocator: Allocator) void {        std.debug.assert(self.phase != .teardown);        self.assertStorage();        self.phase = .teardown;        allocator.free(self.bytes);        self.bytes = &.{};        self.entries = &.{};    }    fn assertStorage(self: *const Storage) void {        const expected = CapacityType.derive(self.limits) catch unreachable;        std.debug.assert(std.meta.eql(expected, self.capacity));        std.debug.assert(self.bytes.len == self.capacity.total_bytes);        std.debug.assert(self.entries.len == self.capacity.entries);    }};comptime {    alloc_phase.capacity.requireAllocatorExactOwnerShape(Storage);}fn entrySlice(bytes: []u8, count: usize) []Entry {    if (count == 0) return &.{};    const region: []align(@alignOf(Entry)) u8 = @alignCast(bytes[0..][0 .. count * @sizeOf(Entry)]);    return std.mem.bytesAsSlice(Entry, region);}const fixture = @import("match/fixture/tree.zig");const Indexed = struct {    workspace: sheet.Workspace,    storage: Storage,    parsed: sheet.StyleSheet = .{},    fn init(source: []const u8) !Indexed {        var workspace = sheet.Workspace.init(std.testing.allocator);        errdefer workspace.deinit();        const parsed = try workspace.parse(source, .default());        var storage = try Storage.init(std.testing.allocator, Limits.inspect(&parsed));        storage.activate();        return .{ .workspace = workspace, .storage = storage, .parsed = parsed };    }    fn deinit(self: *Indexed) void {        self.storage.deinit(std.testing.allocator);        self.workspace.deinit();    }    fn index(self: *Indexed) Index {        return self.storage.index(&self.parsed);    }};fn modelBucketCapacity(limits: Limits) error{CapacityOverflow}!Capacity {    const total = @as(u128, limits.entries) * @sizeOf(Entry);    if (total > std.math.maxInt(usize)) return error.CapacityOverflow;    return .{ .entries = limits.entries, .total_bytes = @intCast(total) };}fn checkBucketStorageInit(allocator: Allocator, limits: Limits) !void {    var storage = try Storage.init(allocator, limits);    defer storage.deinit(allocator);    try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.phase);}test "bucket index capacity matches an independent model" {    comptime {        alloc_phase.capacity.record(            alloc_phase.capacity.witness(Storage, "css_bucket_capacity_model"),        );    }    comptime {        alloc_phase.capacity.record(alloc_phase.capacity.witness(Storage, "css_bucket_overload"));    }    for ([_]Limits{ .{}, .{ .entries = 1 }, .{ .entries = 4097 } }) |limits| {        try std.testing.expectEqual(try modelBucketCapacity(limits), try Capacity.derive(limits));    }    try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{ .entries = std.math.maxInt(usize) }));}test "bucket index acquisition retries after every allocation failure" {    comptime {        alloc_phase.capacity.record(            alloc_phase.capacity.witness(Storage, "css_bucket_acquisition"),        );    }    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkBucketStorageInit,        .{Limits{ .entries = 64 }},    );}test "the bucket index groups selectors by their strongest rightmost key" {    comptime {        alloc_phase.capacity.record(            alloc_phase.capacity.witness(Storage, "css_bucket_steady_overload"),        );    }    comptime {        alloc_phase.capacity.record(            alloc_phase.capacity.witness(Storage, "css_bucket_steady_foreign_risk"),        );    }    var indexed = try Indexed.init(        \\#main { color: red }        \\.card { color: red }        \\article { color: red }        \\* { color: red }        \\[role=button] { color: red }    );    defer indexed.deinit();    var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});    failing.fail_index = failing.alloc_index;    const built = indexed.index();    try std.testing.expectEqual(@as(usize, 5), built.entries.len);    try std.testing.expectEqual(@as(usize, 1), built.group(.universal, 0).len);    try std.testing.expectEqual(@as(usize, 1), built.group(.kind, indexed.parsed.token("article")).len);    try std.testing.expectEqual(@as(usize, 1), built.group(.class, indexed.parsed.token("card")).len);    try std.testing.expectEqual(@as(usize, 1), built.group(.identifier, indexed.parsed.token("main")).len);    try std.testing.expectEqual(@as(usize, 1), built.group(.role, indexed.parsed.token("button")).len);    try std.testing.expectEqual(@as(usize, 0), built.group(.kind, indexed.parsed.token("card")).len);    try std.testing.expect(!failing.has_induced_failure);}test "an element gathers only the candidates its own keys reach" {    var indexed = try Indexed.init(        \\article .card { color: red }        \\#main { color: blue }        \\span { color: green }        \\* { color: black }    );    defer indexed.deinit();    const built = indexed.index();    var tree = fixture.Tree{};    _ = try tree.add(&indexed.parsed.atoms, 0, "article#main");    _ = try tree.add(&indexed.parsed.atoms, 1, "span.card");    tree.finish();    var out: [8]match.Match = undefined;    const leaf = matchElement(built, tree.element(), 1, &out);    try std.testing.expectEqual(@as(u32, 3), leaf.count);    try std.testing.expectEqual(@as(u32, 0), leaf.rejected);    try std.testing.expectEqual(@as(u32, 0), out[0].rule);    try std.testing.expectEqual(@as(u32, 2), out[1].rule);    try std.testing.expectEqual(@as(u32, 3), out[2].rule);    const root = matchElement(built, tree.element(), 0, &out);    try std.testing.expectEqual(@as(u32, 2), root.count);    try std.testing.expectEqual(@as(u32, 1), out[0].rule);    try std.testing.expectEqual(@as(u32, 3), out[1].rule);    var narrow: [1]match.Match = undefined;    const bounded = matchElement(built, tree.element(), 1, &narrow);    try std.testing.expectEqual(@as(u32, 1), bounded.count);    try std.testing.expectEqual(@as(u32, 2), bounded.rejected);}test "the summary records the pseudo classes a sheet raises above a subject" {    var raised = try Indexed.init(        \\.row:hover .cell { color: red }        \\.panel:focus-within .field { color: red }    );    defer raised.deinit();    const above = summarize(&raised.parsed);    try std.testing.expect(above.ancestorPseudoSensitive(.hover));    try std.testing.expect(above.ancestorPseudoSensitive(.focus_within));    try std.testing.expect(!above.ancestorPseudoSensitive(.focus));    try std.testing.expect(!above.ancestorPseudoSensitive(.active));    var subject = try Indexed.init(        \\.cell:hover { color: red }        \\.field:focus-within { color: red }    );    defer subject.deinit();    const rightmost = summarize(&subject.parsed);    try std.testing.expectEqual(@as(u32, 0), rightmost.ancestor_pseudo);    try std.testing.expect(!rightmost.ancestorPseudoSensitive(.hover));    try std.testing.expect(!rightmost.ancestorPseudoSensitive(.focus_within));}test "the summary marks ancestor atoms sibling combinators and positions" {    var indexed = try Indexed.init(        \\article .card { color: red }        \\li:nth-child(2) { color: red }        \\h1 + p { color: red }    );    defer indexed.deinit();    const summary = summarize(&indexed.parsed);    try std.testing.expect(summary.sibling_sensitive);    try std.testing.expect(summary.positional);    try std.testing.expect(summary.ancestorSensitive(.kind, indexed.parsed.token("article")));    try std.testing.expect(summary.ancestorSensitive(.kind, indexed.parsed.token("h1")));    try std.testing.expect(!summary.ancestorSensitive(.class, indexed.parsed.token("card")));    try std.testing.expect(!summary.ancestorSensitive(.kind, indexed.parsed.token("p")));}

Source: lib/css/src/root.zig:36

zig
pub const bucket = @import("bucket.zig");

Audit

Definitions28
Public names29
Members28
Version26.7.0
Revisiondaab053ee433