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.
Capacity.deriveIndex.group: The entries in one key space, or an empty slice when the sheet has none.Limits.growLimits.inspect: Counts the entriesparsedneeds without writing one.Storage.activateStorage.admitsStorage.deinitStorage.index: Indexesparsed, asserting admission rather than growing.Storage.initSummary.ancestorPseudoSensitive: This predicate returns whether ancestor_pseudo carries selector.bit(class).Summary.ancestorSensitive: Whether an ancestor carryingidinspacecan change matching.build: Fillsentriesfromparsedand returns the sorted index.matchElement: Writes every selector inindexthat matchesnodeintoout, ordered by source order and then specificity.summarize: Folds every selector inparsedinto one conservative summary.
Types and contracts
Public types and contracts.
Capacity: The byte layout oneLimitsproduces.Entry: One selector's place in the index.Gather: The outcome of one element's candidate walk.Index: The sorted entries plus the sheet arrays a match walk reads.Limits: The bound one sheet produces, which is its selector count.Space: The key spaces a bucket entry lives in, in the order the index sorts them.Storage: The single aligned block one bucket index owns.Storage.CapacityStorage.LimitsSummary: What a mutation must carry before it can change what a sheet matches.
Values and defaults
Public values and defaults.
Storage.claimmax_element_classes: The most classes one element carries into a candidate walk.storage_alignment: The alignment the index block is acquired at.
Source
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
| Definitions | 28 |
|---|---|
| Public names | 29 |
| Members | 28 |
| Version | 26.7.0 |
| Revision | daab053ee433 |