diff options
Diffstat (limited to 'src/locations_cache.zig')
| -rw-r--r-- | src/locations_cache.zig | 213 |
1 files changed, 0 insertions, 213 deletions
diff --git a/src/locations_cache.zig b/src/locations_cache.zig deleted file mode 100644 index a079226b..00000000 --- a/src/locations_cache.zig +++ /dev/null @@ -1,213 +0,0 @@ -const std = @import("std"); -const syntax = @import("syntax.zig"); - -/// A cache hit borrows its analysis; an entry too large to retain transfers -/// ownership instead. Call deinit after consuming either result. -pub const Result = struct { - analysis: syntax.SourceAnalysis, - owned: bool = false, - - pub fn deinit(result: *Result, gpa: std.mem.Allocator) void { - if (result.owned) result.analysis.deinit(gpa); - result.* = undefined; - } -}; - -/// Owned source snapshots keep analysis valid across filesystem changes and -/// borrowed editor buffers. Entries are ordered from least to most recent. -pub const Cache = struct { - pub const max_bytes = 64 * 1024 * 1024; - pub const max_entries = 64; - - const Entry = struct { - path: []u8, - source: []u8, - analysis: syntax.SourceAnalysis, - declarations_ready: bool, - colors_ready: bool, - - fn size(entry: Entry) usize { - return entry.path.len + entry.source.len + entry.analysis.colors.len + - entry.analysis.declarations.len * @sizeOf(syntax.ContextDeclaration); - } - - fn deinit(entry: *Entry, gpa: std.mem.Allocator) void { - gpa.free(entry.path); - gpa.free(entry.source); - entry.analysis.deinit(gpa); - } - }; - - entries: [max_entries]Entry = undefined, - len: usize = 0, - bytes: usize = 0, - analyses: usize = 0, - hits: usize = 0, - - pub fn deinit(cache: *Cache, gpa: std.mem.Allocator) void { - for (cache.entries[0..cache.len]) |*entry| entry.deinit(gpa); - cache.* = .{}; - } - - /// Borrowed slices remain valid until the next get or cache deinit; an - /// owned result remains valid until Result.deinit. All calls for this - /// cache must use the same allocator. Unsupported languages - /// are cached too, including their empty analysis arrays. - pub fn get(cache: *Cache, gpa: std.mem.Allocator, path: []const u8, source: []const u8, want_declarations: bool, want_colors: bool) !Result { - return cache.getBounded(gpa, path, source, want_declarations, want_colors, max_bytes); - } - - fn getBounded(cache: *Cache, gpa: std.mem.Allocator, path: []const u8, source: []const u8, want_declarations: bool, want_colors: bool, budget: usize) !Result { - if (source.len > budget or path.len > budget - source.len) return error.SourceTooLarge; - var previous: ?usize = null; - var declarations = want_declarations; - var colors = want_colors; - for (cache.entries[0..cache.len], 0..) |entry, index| { - if (!std.mem.eql(u8, entry.path, path)) continue; - previous = index; - if (!std.mem.eql(u8, entry.source, source)) break; - if ((!want_declarations or entry.declarations_ready) and (!want_colors or entry.colors_ready)) { - // Moving the small ownership record keeps the underlying - // allocations intact and avoids timestamp/overflow state. - std.mem.copyForwards(Entry, cache.entries[index .. cache.len - 1], cache.entries[index + 1 .. cache.len]); - cache.entries[cache.len - 1] = entry; - cache.hits +|= 1; - return .{ .analysis = entry.analysis }; - } - declarations = declarations or entry.declarations_ready; - colors = colors or entry.colors_ready; - break; - } - // A supported full-source color map needs one byte per source byte. - // Reject that known minimum before parsing, so the caller's uncached - // path does not repeat an expensive analysis for oversized inputs. - const input_bytes = path.len + source.len; - if (colors and syntax.supportsPath(path) and source.len > budget - input_bytes) return error.SourceTooLarge; - var analysis = try syntax.analyzeSource(gpa, path, source, declarations, colors); - errdefer analysis.deinit(gpa); - cache.analyses +|= 1; - const analysis_bytes = analysis.colors.len + analysis.declarations.len * @sizeOf(syntax.ContextDeclaration); - if (analysis_bytes > budget - input_bytes) return .{ .analysis = analysis, .owned = true }; - const owned_path = try gpa.dupe(u8, path); - errdefer gpa.free(owned_path); - const owned_source = try gpa.dupe(u8, source); - errdefer gpa.free(owned_source); - const next: Entry = .{ - .path = owned_path, - .source = owned_source, - .analysis = analysis, - .declarations_ready = declarations, - .colors_ready = colors, - }; - // Commit only after every allocation succeeds. A failed refresh leaves - // the previous cached snapshot usable by the next request. - if (previous) |index| cache.remove(gpa, index); - while (cache.len == max_entries or cache.bytes > budget - next.size()) cache.remove(gpa, 0); - cache.entries[cache.len] = next; - cache.len += 1; - cache.bytes += next.size(); - return .{ .analysis = next.analysis }; - } - - fn remove(cache: *Cache, gpa: std.mem.Allocator, index: usize) void { - cache.bytes -= cache.entries[index].size(); - cache.entries[index].deinit(gpa); - std.mem.copyForwards(Entry, cache.entries[index .. cache.len - 1], cache.entries[index + 1 .. cache.len]); - cache.len -= 1; - } -}; - -test "locations cache validates exact source and path and upgrades analysis" { - const gpa = std.testing.allocator; - syntax.start(gpa); - defer syntax.stop(); - var cache: Cache = .{}; - defer cache.deinit(gpa); - const source = "const Thing = struct {\n value: u32,\n};\n"; - _ = try cache.get(gpa, "a.zig", source, true, false); - try std.testing.expectEqual(@as(usize, 1), cache.analyses); - _ = try cache.get(gpa, "a.zig", source, true, false); - try std.testing.expectEqual(@as(usize, 1), cache.analyses); - try std.testing.expectEqual(@as(usize, 1), cache.hits); - _ = try cache.get(gpa, "a.zig", source, false, true); - try std.testing.expectEqual(@as(usize, 2), cache.analyses); - try std.testing.expect(cache.entries[0].declarations_ready and cache.entries[0].colors_ready); - _ = try cache.get(gpa, "a.zig", source, true, true); - try std.testing.expectEqual(@as(usize, 2), cache.analyses); - _ = try cache.get(gpa, "a.zig", "const Thing = struct {\n other: u32,\n};\n", true, true); - try std.testing.expectEqual(@as(usize, 3), cache.analyses); - try std.testing.expectEqual(@as(usize, 1), cache.len); - _ = try cache.get(gpa, "a.txt", source, true, true); - try std.testing.expectEqual(@as(usize, 4), cache.analyses); - try std.testing.expectEqual(@as(usize, 2), cache.len); -} - -test "locations cache bounds memory and entries and evicts least recently used" { - const gpa = std.testing.allocator; - var cache: Cache = .{}; - defer cache.deinit(gpa); - for (0..Cache.max_entries) |index| { - var path: [32]u8 = undefined; - _ = try cache.get(gpa, try std.fmt.bufPrint(&path, "{d}.unknown", .{index}), "source", false, false); - } - _ = try cache.get(gpa, "0.unknown", "source", false, false); - _ = try cache.get(gpa, "new.unknown", "source", false, false); - try std.testing.expectEqual(Cache.max_entries, cache.len); - try std.testing.expectEqualStrings("2.unknown", cache.entries[0].path); - const count = cache.analyses; - _ = try cache.get(gpa, "0.unknown", "source", false, false); - try std.testing.expectEqual(count, cache.analyses); - cache.deinit(gpa); - _ = try cache.getBounded(gpa, "a.unknown", "source", false, false, 40); - _ = try cache.getBounded(gpa, "b.unknown", "source", false, false, 40); - _ = try cache.getBounded(gpa, "c.unknown", "source", false, false, 40); - try std.testing.expectEqual(@as(usize, 2), cache.len); - try std.testing.expect(cache.bytes <= 40); - try std.testing.expectEqualStrings("b.unknown", cache.entries[0].path); - try std.testing.expectError(error.SourceTooLarge, cache.getBounded(gpa, "large.unknown", "x" ** 41, false, false, 40)); - try std.testing.expectEqual(@as(usize, 2), cache.len); - if (syntax.supportsPath("large.zig")) { - const before = cache.analyses; - try std.testing.expectError(error.SourceTooLarge, cache.getBounded(gpa, "large.zig", "x" ** 25, true, true, 40)); - try std.testing.expectEqual(before, cache.analyses); - } - cache.deinit(gpa); - try std.testing.expectEqual(@as(usize, 0), cache.bytes); - try std.testing.expectEqual(@as(usize, 0), cache.len); -} - -test "locations cache keeps its previous snapshot after allocation failure" { - var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); - const gpa = failing.allocator(); - var cache: Cache = .{}; - defer cache.deinit(gpa); - _ = try cache.get(gpa, "a.unknown", "old bytes", false, false); - const retained = cache.bytes; - failing.fail_index = failing.alloc_index; - try std.testing.expectError(error.OutOfMemory, cache.get(gpa, "a.unknown", "new bytes", false, false)); - try std.testing.expectEqual(@as(usize, 1), cache.len); - try std.testing.expectEqual(retained, cache.bytes); - try std.testing.expectEqualStrings("old bytes", cache.entries[0].source); - // This succeeds with allocations still disabled because the old entry - // remains valid and the hit path moves ownership records only. - _ = try cache.get(gpa, "a.unknown", "old bytes", false, false); - try std.testing.expectEqual(@as(usize, 1), cache.hits); -} - -test "locations cache transfers oversized completed analysis without reparsing" { - if (!syntax.supportsPath("large.zig")) return error.SkipZigTest; - const gpa = std.testing.allocator; - syntax.start(gpa); - defer syntax.stop(); - var cache: Cache = .{}; - defer cache.deinit(gpa); - const source = "const Thing = struct {\n value: u32,\n};\n"; - const budget = "large.zig".len + source.len; - var result = try cache.getBounded(gpa, "large.zig", source, true, false, budget); - defer result.deinit(gpa); - try std.testing.expect(result.owned); - try std.testing.expect(result.analysis.declarations.len > 0); - try std.testing.expectEqual(@as(usize, 1), cache.analyses); - try std.testing.expectEqual(@as(usize, 0), cache.len); - try std.testing.expectEqual(@as(usize, 0), cache.bytes); -} |
