diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-24 12:52:49 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 8de5bf6fecc19cf2be6093b4338691fe4af84815 (patch) | |
| tree | f64a62c4a720150574d796ebc2d62c0f21cbff02 /src/syntax.zig | |
| parent | 40197552bed794b92410bdf803532162c8f5e27d (diff) | |
| download | pardes-8de5bf6fecc19cf2be6093b4338691fe4af84815.tar.gz pardes-8de5bf6fecc19cf2be6093b4338691fe4af84815.zip | |
Hand every C library's allocator hook the same C heap
C's malloc, calloc, realloc and free over a Zig allocator were written three
times: syntax.zig's for tree-sitter, pdf.zig's for MuPDF, and gui.zig's for
FreeType and HarfBuzz. They are now one, src/c_heap.zig's Heap. Each hook
names the allocator variable its heap reads; gui.zig exports its heap under
the ui_malloc names font.c and HarfBuzz call. It is a module of its own
because pdf.zig is the MuPDF module, and a file cannot belong to that module
and the core's at once.
A block starts with a 16-byte header holding its size and the allocator that
made it, so it goes back where it came from even after the heap is repointed,
as the fonts' copy did. The fonts' copy carried the 16-byte Allocator itself
in a 32-byte header; here the header holds the allocator's index in a table of
every allocator a heap has used (entries are published once and never change,
and a heap remembers where its current one was last found). The difference is
measurable: with the 32-byte header, pardes-pdf-bench's filtered page render
at 96 dpi took 1.1% longer than before this change, slower in 15 of 16 pinned
rounds in two separate runs, and highlighting all of src/pardes.zig (305k
tree-sitter blocks) up to 0.6% longer, against an A/A pair within 0.7%. With
the 16-byte header both are back within the A/A pair's noise.
Two other things change. For tree-sitter and MuPDF, a block made before the
allocator was repointed now goes back to the one that made it rather than
through the current one. For the fonts, realloc(block, 0) now frees and
returns null, as glibc's does and as the other two copies already did;
neither FreeType nor HarfBuzz asks for it. The heap's test replaces the
tests of the old copies.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to 'src/syntax.zig')
| -rw-r--r-- | src/syntax.zig | 69 |
1 files changed, 5 insertions, 64 deletions
diff --git a/src/syntax.zig b/src/syntax.zig index e637bf67..5c8b9c0f 100644 --- a/src/syntax.zig +++ b/src/syntax.zig @@ -3,6 +3,7 @@ const config = @import("pardes_config"); const tracy = @import("tracy.zig"); const grammar_manifest = @import("grammar_manifest.zig"); const look = @import("look.zig"); +const c_heap = @import("c_heap"); pub const enabled = config.syntax_highlighting; const zig_grammar = config.syntax_zig_grammar; @@ -139,8 +140,10 @@ fn initSpecs() [specCount()]Spec { var specs = initSpecs(); -const allocation_header_size = 16; +/// What the tree-sitter runtime allocates from: `start`'s allocator, through +/// the shared C heap. var syntax_allocator: std.mem.Allocator = undefined; +const syntax_heap = c_heap.Heap(&syntax_allocator); var syntax_started = false; extern fn ts_set_allocator( @@ -150,56 +153,13 @@ extern fn ts_set_allocator( new_free: ?*const fn (ptr: ?*anyopaque) callconv(.c) void, ) void; -fn syntaxAlloc(size_arg: usize) callconv(.c) ?*anyopaque { - const size = @max(size_arg, 1); - const total = std.math.add(usize, allocation_header_size, size) catch return null; - const bytes = syntax_allocator.alignedAlloc(u8, .@"16", total) catch return null; - const header: *align(16) usize = @ptrCast(bytes.ptr); - header.* = total; - return @ptrCast(bytes.ptr + allocation_header_size); -} - -fn syntaxCalloc(count: usize, size: usize) callconv(.c) ?*anyopaque { - const len = std.math.mul(usize, count, size) catch return null; - const pointer = syntaxAlloc(len) orelse return null; - const bytes: [*]u8 = @ptrCast(pointer); - @memset(bytes[0..len], 0); - return pointer; -} - -fn syntaxRealloc(ptr: ?*anyopaque, new_size: usize) callconv(.c) ?*anyopaque { - const pointer = ptr orelse return syntaxAlloc(new_size); - if (new_size == 0) { - syntaxFree(pointer); - return null; - } - const user: [*]u8 = @ptrCast(pointer); - const header: *align(16) usize = @ptrCast(@alignCast(user - allocation_header_size)); - const old_total = header.*; - const old_bytes: []align(16) u8 = @as([*]align(16) u8, @ptrCast(header))[0..old_total]; - const new_total = std.math.add(usize, allocation_header_size, new_size) catch return null; - const new_bytes = syntax_allocator.realloc(old_bytes, new_total) catch return null; - const new_header: *align(16) usize = @ptrCast(new_bytes.ptr); - new_header.* = new_total; - return @ptrCast(new_bytes.ptr + allocation_header_size); -} - -fn syntaxFree(ptr: ?*anyopaque) callconv(.c) void { - const pointer = ptr orelse return; - const user: [*]u8 = @ptrCast(pointer); - const header: *align(16) usize = @ptrCast(@alignCast(user - allocation_header_size)); - const total = header.*; - const bytes: []align(16) u8 = @as([*]align(16) u8, @ptrCast(header))[0..total]; - syntax_allocator.free(bytes); -} - pub fn start(gpa: std.mem.Allocator) void { if (comptime enabled) { std.debug.assert(!syntax_started); stop(); syntax_allocator = gpa; syntax_started = true; - ts_set_allocator(syntaxAlloc, syntaxCalloc, syntaxRealloc, syntaxFree); + ts_set_allocator(syntax_heap.malloc, syntax_heap.calloc, syntax_heap.realloc, syntax_heap.free); } } @@ -935,25 +895,6 @@ test "syntax codeAfterLocation takes whole-token locations and nothing else" { try std.testing.expect(codeAfterLocation(" leading space") == null); } -test "syntax tree-sitter allocator callbacks preserve and free exact allocations" { - syntax_allocator = std.testing.allocator; - defer syntax_allocator = undefined; - - var live: ?*anyopaque = syntaxCalloc(4, 1) orelse return error.OutOfMemory; - defer if (live) |pointer| syntaxFree(pointer); - const original: [*]u8 = @ptrCast(live.?); - try std.testing.expectEqualSlices(u8, &.{ 0, 0, 0, 0 }, original[0..4]); - @memcpy(original[0..4], "data"); - - live = syntaxRealloc(live, 32) orelse return error.OutOfMemory; - const grown: [*]u8 = @ptrCast(live.?); - try std.testing.expectEqualSlices(u8, "data", grown[0..4]); - - try std.testing.expect(syntaxCalloc(std.math.maxInt(usize), 2) == null); - try std.testing.expect(syntaxRealloc(live, 0) == null); - live = null; -} - test "syntax default full grammar set highlights Typst source" { if (!enabled or !full_grammars) return; start(std.testing.allocator); |
