Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Zig
uses: mlugg/setup-zig@v2
with:
version: 0.16.0
version: 0.17.0

- name: Zig fmt check
shell: bash
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Prerequisites

- Zig `0.16.0`
- Zig `0.17.0`
- Git

## Local Development Workflow
Expand All @@ -21,6 +21,8 @@ zig build bench-compare
zig build conformance
```

Use `zig build test -j1 -Dtest-jobs=1` to bound both compilation and test-process concurrency.

## Style

- Format Zig code before pushing:
Expand Down
2 changes: 1 addition & 1 deletion DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This is the canonical manual for usage, API, selector behavior, performance work

## Requirements

- Zig `0.16.0`
- Zig `0.17.0`
- Mutable input buffers (`[]u8`) for destructive parsing
- `[]const u8` inputs are supported when `ParseOptions.non_destructive = true`

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

High-throughput HTML parser + CSS selector engine for Zig.

[![zig](https://img.shields.io/badge/zig-0.16.0-orange)](https://ziglang.org/)
[![zig](https://img.shields.io/badge/zig-0.17.0-orange)](https://ziglang.org/)
[![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)

## ⚠️ Conformance Warning
Expand Down
21 changes: 13 additions & 8 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const IntLen = enum {
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const test_jobs = b.option(usize, "test-jobs", "Maximum concurrent test processes");
const intlen = b.option(IntLen, "intlen", "Integer width used for document spans and node indexes") orelse .u32;

const config_options = b.addOptions();
Expand Down Expand Up @@ -113,20 +114,19 @@ pub fn build(b: *std.Build) void {
docs_check_cmd.step.dependOn(b.getInstallStep());
examples_check_cmd.step.dependOn(b.getInstallStep());

if (b.args) |args| {
tools_cmd.addArgs(args);
compare_cmd.addArgs(args);
interleaved_cmd.addArgs(args);
conformance_cmd.addArgs(args);
docs_check_cmd.addArgs(args);
examples_check_cmd.addArgs(args);
}
tools_cmd.addPassthruArgs();
compare_cmd.addPassthruArgs();
interleaved_cmd.addPassthruArgs();
conformance_cmd.addPassthruArgs();
docs_check_cmd.addPassthruArgs();
examples_check_cmd.addPassthruArgs();

const mod_tests = b.addTest(.{
.root_module = mod,
.test_runner = .{ .path = b.path("tools/test_runner.zig"), .mode = .simple },
});
const run_mod_tests = b.addRunArtifact(mod_tests);
if (test_jobs) |jobs| run_mod_tests.addArgs(&.{ "--jobs", b.fmt("{d}", .{jobs}) });

const examples_tests = b.addTest(.{
.root_module = b.createModule(.{
Expand All @@ -148,6 +148,7 @@ pub fn build(b: *std.Build) void {
.test_runner = .{ .path = b.path("tools/test_runner.zig"), .mode = .simple },
});
const run_examples_tests = b.addRunArtifact(examples_tests);
if (test_jobs) |jobs| run_examples_tests.addArgs(&.{ "--jobs", b.fmt("{d}", .{jobs}) });

const behavioral_tests = b.addTest(.{
.root_module = b.createModule(.{
Expand All @@ -161,6 +162,7 @@ pub fn build(b: *std.Build) void {
.test_runner = .{ .path = b.path("tools/test_runner.zig"), .mode = .simple },
});
const run_behavioral_tests = b.addRunArtifact(behavioral_tests);
if (test_jobs) |jobs| run_behavioral_tests.addArgs(&.{ "--jobs", b.fmt("{d}", .{jobs}) });

const public_api_tests = b.addTest(.{
.root_module = b.createModule(.{
Expand All @@ -174,6 +176,7 @@ pub fn build(b: *std.Build) void {
.test_runner = .{ .path = b.path("tools/test_runner.zig"), .mode = .simple },
});
const run_public_api_tests = b.addRunArtifact(public_api_tests);
if (test_jobs) |jobs| run_public_api_tests.addArgs(&.{ "--jobs", b.fmt("{d}", .{jobs}) });
const public_api_test_step = b.step("test-public-api", "Run exhaustive consumer-facing public API tests");
public_api_test_step.dependOn(&run_public_api_tests.step);

Expand All @@ -186,6 +189,7 @@ pub fn build(b: *std.Build) void {
.test_runner = .{ .path = b.path("tools/test_runner.zig"), .mode = .simple },
});
const run_scripts_tests = b.addRunArtifact(scripts_tests);
if (test_jobs) |jobs| run_scripts_tests.addArgs(&.{ "--jobs", b.fmt("{d}", .{jobs}) });

const bench_tests = b.addTest(.{
.root_module = b.createModule(.{
Expand All @@ -200,6 +204,7 @@ pub fn build(b: *std.Build) void {
.test_runner = .{ .path = b.path("tools/test_runner.zig"), .mode = .simple },
});
const run_bench_tests = b.addRunArtifact(bench_tests);
if (test_jobs) |jobs| run_bench_tests.addArgs(&.{ "--jobs", b.fmt("{d}", .{jobs}) });

const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.name = .html,
.version = "0.1.0",
.fingerprint = 0x1879f8e53a69d776,
.minimum_zig_version = "0.16.0",
.minimum_zig_version = "0.17.0",
.dependencies = .{},
.paths = .{
"build.zig",
Expand Down
6 changes: 3 additions & 3 deletions src/html/attr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ pub const CompactIterator = struct {
self.cursor += 1;
return .{ .name = name, .value = null };
}
const marker: CompactValueMarker = @enumFromInt(self.source[self.cursor]);
const marker: CompactValueMarker = @fromBackingInt(@intCast(self.source[self.cursor]));
self.cursor += 1;
const value_start = self.cursor;
while (self.cursor < self.source.len and self.source[self.cursor] != 0) : (self.cursor += 1) {}
Expand Down Expand Up @@ -308,12 +308,12 @@ pub fn materializeAttributes(comptime entity_decoding: entities.EntityDecoding,
const raw_slice = source[raw.start..raw.end];
const decoded = entities.decodeAttributeInPlaceResultWithMode(entity_decoding, raw_slice, null);
if (decoded.complete) {
source[write] = @intFromEnum(CompactValueMarker.decoded);
source[write] = @backingInt(CompactValueMarker.decoded);
write += 1;
std.mem.copyForwards(u8, source[write .. write + decoded.len], source[raw.start .. raw.start + decoded.len]);
write += decoded.len;
} else {
source[write] = @intFromEnum(rawCompactMarker(raw));
source[write] = @backingInt(rawCompactMarker(raw));
write += 1;
std.mem.copyForwards(u8, source[write .. write + raw_slice.len], raw_slice);
for (source[write .. write + raw_slice.len]) |*b| {
Expand Down
23 changes: 12 additions & 11 deletions src/html/document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,9 @@ fn GetNode(comptime options: ParseOptions) type {
const end: usize = node.name_or_text.end();
if (end >= doc.source.len) return .raw;
return switch (doc.source[end]) {
@intFromEnum(RwTextState.decoded) => .decoded,
@intFromEnum(RwTextState.decode_failed) => .decode_failed,
@intFromEnum(RwTextState.raw_normalized) => .raw_normalized,
@backingInt(RwTextState.decoded) => .decoded,
@backingInt(RwTextState.decode_failed) => .decode_failed,
@backingInt(RwTextState.raw_normalized) => .raw_normalized,
else => .raw,
};
}
Expand All @@ -496,7 +496,7 @@ fn GetNode(comptime options: ParseOptions) type {
if (comptime options.non_destructive) return;
if (state == .raw) return;
const end: usize = node.name_or_text.end();
if (end < doc.source.len) doc.source[end] = @intFromEnum(state);
if (end < doc.source.len) doc.source[end] = @backingInt(state);
}

/// Opaque children preserve both markup-looking bytes and entity syntax.
Expand Down Expand Up @@ -2124,7 +2124,7 @@ test "innerTextOwned returns allocated output and materializes RW source text" {

const text_node_after = doc.nodes[node.index + 1];
try std.testing.expectEqualStrings("a & b", text_node_after.name_or_text.slice(doc.source));
try std.testing.expectEqual(@as(u8, @intFromEnum(RwTextState.decoded)), doc.source[text_node_after.name_or_text.end()]);
try std.testing.expectEqual(@as(u8, @backingInt(RwTextState.decoded)), doc.source[text_node_after.name_or_text.end()]);
}

test "RW text decoding stores and moves the trailing decoded marker" {
Expand All @@ -2140,13 +2140,13 @@ test "RW text decoding stores and moves the trailing decoded marker" {
const decoded = try p.innerTextWithOptions(alloc, .{ .normalize_whitespace = false });
try std.testing.expectEqualStrings("a& b ", decoded.value);
const decoded_end: usize = doc.nodes[text_idx].name_or_text.end();
try std.testing.expectEqual(@as(u8, @intFromEnum(RwTextState.decoded)), doc.source[decoded_end]);
try std.testing.expectEqual(@as(u8, @backingInt(RwTextState.decoded)), doc.source[decoded_end]);

const normalized = try p.innerTextWithOptions(alloc, .{ .unescape = false, .normalize_whitespace = true });
try std.testing.expectEqualStrings("a& b", normalized.value);
const normalized_end: usize = doc.nodes[text_idx].name_or_text.end();
try std.testing.expect(normalized_end < decoded_end);
try std.testing.expectEqual(@as(u8, @intFromEnum(RwTextState.decoded)), doc.source[normalized_end]);
try std.testing.expectEqual(@as(u8, @backingInt(RwTextState.decoded)), doc.source[normalized_end]);
}

test "RW terminal text marks only when decoding creates room" {
Expand All @@ -2161,7 +2161,7 @@ test "RW terminal text marks only when decoding creates room" {
try std.testing.expectEqualStrings("x&y", decoded.value);
const encoded_text = encoded_doc.nodes[encoded_p.index + 1];
try std.testing.expect(encoded_text.name_or_text.end() < encoded_doc.source.len);
try std.testing.expectEqual(@as(u8, @intFromEnum(RwTextState.decoded)), encoded_doc.source[encoded_text.name_or_text.end()]);
try std.testing.expectEqual(@as(u8, @backingInt(RwTextState.decoded)), encoded_doc.source[encoded_text.name_or_text.end()]);

var plain_doc = GetDocument(.{}).init(alloc);
defer plain_doc.deinit();
Expand Down Expand Up @@ -2190,7 +2190,7 @@ test "RW multi-node owned text materializes each span and still normalizes after
if (!doc.nodes[idx].isText(idx)) continue;
const end: usize = doc.nodes[idx].name_or_text.end();
try std.testing.expect(end < doc.source.len);
try std.testing.expectEqual(@as(u8, @intFromEnum(RwTextState.decoded)), doc.source[end]);
try std.testing.expectEqual(@as(u8, @backingInt(RwTextState.decoded)), doc.source[end]);
try std.testing.expect(std.mem.indexOf(u8, doc.nodes[idx].name_or_text.slice(doc.source), "&amp;") == null);
}
}
Expand Down Expand Up @@ -2386,7 +2386,7 @@ test "expanding full entities use marked allocating fallbacks" {
const text_idx = div.index + 1;
const text_end: usize = doc.nodes[text_idx].name_or_text.end();
try std.testing.expect(text_end < doc.source.len);
try std.testing.expectEqual(@as(u8, @intFromEnum(RwTextState.decode_failed)), doc.source[text_end]);
try std.testing.expectEqual(@as(u8, @backingInt(RwTextState.decode_failed)), doc.source[text_end]);

var never_out: std.Io.Writer.Allocating = .init(alloc);
defer never_out.deinit();
Expand Down Expand Up @@ -4612,7 +4612,8 @@ test "query collect frees partial output when growth fails" {
defer alloc.free(doc_source);
try resetParsed(.{}, &doc, doc_source);

var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 1 });
// Reject in-place growth too, so SafeAllocator cannot bypass the failing allocation.
var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 1, .resize_fail_index = 0 });
var it = doc.query("span");
try std.testing.expectError(error.OutOfMemory, it.collect(failing.allocator()));
}
Expand Down
2 changes: 1 addition & 1 deletion src/html/entities.zig
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ fn replacementDecoded(consumed: usize) Decoded {
}

const NumericDigitTable = blk: {
var table = [_]u8{InvalidDigit} ** 256;
var table: [256]u8 = @splat(InvalidDigit);
var c: u8 = '0';
while (c <= '9') : (c += 1) table[c] = c - '0';
c = 'a';
Expand Down
6 changes: 3 additions & 3 deletions src/html/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn ParseState(comptime opts: ParseOptions) type {
parse_stack_heap_owned: bool = false,
/// Optional-end-tag source classes currently present on the open stack.
implicit_source_mask: u8 = 0,
implicit_source_counts: [8]IndexInt = .{0} ** 8,
implicit_source_counts: [8]IndexInt = @splat(0),
/// Malformed-close index exists only after a deep full-stack miss.
tag_index: ?*open_tag_index.LiveIndex(OpenElem) = null,

Expand Down Expand Up @@ -735,8 +735,8 @@ fn ParseState(comptime opts: ParseOptions) type {

inline fn addNode(noalias self: *Self, name_or_text: anytype, is_element: bool, overrides: anytype) !void {
const Overrides = @TypeOf(overrides);
comptime for (@typeInfo(Overrides).@"struct".fields) |field| {
std.debug.assert(std.mem.eql(u8, field.name, "parent"));
comptime for (@typeInfo(Overrides).@"struct".field_names) |field_name| {
std.debug.assert(std.mem.eql(u8, field_name, "parent"));
};
const parent_idx: IndexInt = @intCast(if (@hasField(Overrides, "parent")) overrides.parent else self.currentParent());
const idx: IndexInt = @intCast(self.nodes.items.len);
Expand Down
4 changes: 2 additions & 2 deletions src/html/scanner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub noinline fn countByte(source: []const u8, value: u8) usize {
if (std.simd.suggestVectorLength(u8)) |suggested_len| {
const Block = @Vector(suggested_len, u8);
const BoolBlock = @Vector(suggested_len, bool);
const MaskInt = std.meta.Int(.unsigned, suggested_len);
const MaskInt = @Int(.unsigned, suggested_len);
const mask: Block = @splat(value);

while (index + suggested_len <= source.len) : (index += suggested_len) {
Expand Down Expand Up @@ -65,7 +65,7 @@ pub fn findBytePosOrEnd(source: []const u8, start: usize, value: u8) usize {
const block_cnt = @max(1, block_len_sum / block_len);
const Block = @Vector(block_len, u8);
const BoolBlock = @Vector(block_len, bool);
const MaskInt = std.meta.Int(.unsigned, block_len);
const MaskInt = @Int(.unsigned, block_len);
const mask: Block = @splat(value);

if (index + block_len_sum <= source.len) while (true) {
Expand Down
4 changes: 2 additions & 2 deletions src/html/stream.zig
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ fn State(comptime Ctx: type, comptime callback: anytype, comptime static_options
stack_heap: bool = false,
tag_index: open_tag_index.LiveIndex(OpenTag) = .{},
implicit_source_mask: u8 = 0,
implicit_source_counts: [8]IndexInt = .{0} ** 8,
implicit_source_counts: [8]IndexInt = @splat(0),
slow_close_misses: u8 = 0,

const Self = @This();
Expand All @@ -207,7 +207,7 @@ fn State(comptime Ctx: type, comptime callback: anytype, comptime static_options
};
const blocker_by_boundary = blk: {
const B = tags.ImplicitCloseBoundaryMask;
var table = [_]u8{0} ** 32;
var table: [32]u8 = @splat(0);
table[B.regular] = ImplicitBlockers.regular;
table[B.button] = ImplicitBlockers.button;
table[B.list_item] = ImplicitBlockers.list_item;
Expand Down
4 changes: 2 additions & 2 deletions src/html/tables.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test {
/// Builds a 256-entry boolean lookup table from a predicate.
pub fn makeClassTable(comptime predicate: fn (u8) bool) [256]bool {
@setEvalBranchQuota(10_000);
var table = [_]bool{false} ** 256;
var table: [256]bool = @splat(false);
inline for (0..256) |i| {
table[i] = predicate(@as(u8, @intCast(i)));
}
Expand Down Expand Up @@ -46,7 +46,7 @@ pub const TagNameCharTable = makeClassTable(isTagNameChar);
/// Canonical lowercase byte for tag-name bytes; zero marks a tokenizer terminator.
pub const TagNameLowerTable = blk: {
@setEvalBranchQuota(10_000);
var table = [_]u8{0} ** 256;
var table: [256]u8 = @splat(0);
for (0..256) |i| {
const c: u8 = @intCast(i);
if (isTagNameChar(c)) table[i] = std.ascii.toLower(c);
Expand Down
6 changes: 3 additions & 3 deletions src/selector/execution_plan.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub const Mask = enum(usize) {
scope_lineage,
};

pub const MaskCount = @typeInfo(Mask).@"enum".fields.len;
pub const MaskCount = @typeInfo(Mask).@"enum".field_names.len;

pub const StatefulGroup = struct {
compound_start: IndexInt,
Expand Down Expand Up @@ -162,12 +162,12 @@ pub const Plan = struct {
}

pub fn mask(self: *const @This(), which: Mask) []const u64 {
const start = @intFromEnum(which) * self.word_count;
const start = @backingInt(which) * self.word_count;
return self.masks[start .. start + self.word_count];
}

pub fn maskMut(self: *@This(), which: Mask) []u64 {
const start = @intFromEnum(which) * self.word_count;
const start = @backingInt(which) * self.word_count;
return self.masks[start .. start + self.word_count];
}

Expand Down
6 changes: 3 additions & 3 deletions src/selector/forward.zig
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ pub fn Executor(comptime Doc: type) type {
seed_workspace: ?*matcher.MatchWorkspace = null,
stats: if (builtin.is_test) Stats else void = if (builtin.is_test) .{} else {},
initialized: bool = false,
tag_cache: [16]TagCacheEntry = [_]TagCacheEntry{.{}} ** 16,
tag_cache: [16]TagCacheEntry = @splat(.{}),

const Self = @This();
const TagCacheEntry = struct {
Expand Down Expand Up @@ -490,7 +490,7 @@ pub fn WideExecutor(comptime Doc: type) type {
stack: std.ArrayListUnmanaged(WideFrame) = .empty,
node_ctx: matcher.NodeContext = .{},
predicate_word_count: usize = 0,
small_seen_predicates: [SmallPredicateWordLimit]u64 = [_]u64{0} ** SmallPredicateWordLimit,
small_seen_predicates: [SmallPredicateWordLimit]u64 = @splat(0),
seen_predicates: []u64 = &.{},
matched_predicates: []u64 = &.{},
state_fanned_predicates: []u64 = &.{},
Expand Down Expand Up @@ -1032,7 +1032,7 @@ pub fn WideExecutor(comptime Doc: type) type {
}

fn state(self: *Self, slot: usize, which: State) []u64 {
const start = (slot * 4 + @intFromEnum(which)) * self.word_count;
const start = (slot * 4 + @backingInt(which)) * self.word_count;
return self.states.items[start .. start + self.word_count];
}

Expand Down
Loading
Loading