lib/accy/src/preparation/cache.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const choir = @import("choir");
3 const semantic = @import("../choir/root.zig").semantic;
4 const publication = @import("publication.zig");
5 const product = @import("product.zig");
6 const incremental = choir.product.incremental;
7 const Prepared = product.BackendPreparedModule;
8
9 pub const BackendPreparationCacheUpdate = struct {
10 /// A caller receives this from a refresh and owns the prepared result it names. `prepared`
11 /// holds its own reference to the result, so a later refresh of the cache that replaces its
12 /// result leaves this one valid. `reused` is true when every stage after the first was reused,
13 /// `request_changed` is true when the final stage record, the sealed result of one compile
14 /// stage, differs from the one held before, and `refresh` reports which stored products
15 /// changed. `deinit` releases the prepared result and the refresh report.
16 prepared: *Prepared,
17 reused: bool,
18 request_changed: bool,
19 refresh: incremental.ProductRefreshReport,
20
21 pub fn deinit(self: *BackendPreparationCacheUpdate, allocator: std.mem.Allocator) void {
22 self.prepared.deinit();
23 self.refresh.deinit(allocator);
24 self.* = undefined;
25 }
26 };
27
28 /// A caller keeps one of these per program to recompile it cheaply as its requests change. The
29 /// cache holds one earlier prepared result, its semantic source and the graph of stored products,
30 /// and offers that result as the candidate for the next refresh. The semantic source is the
31 /// immutable handle that keeps the first stage record, so no draft is needed. Reuse of any stage is
32 /// decided only by the exact admission check against that candidate's record. The refresh report
33 /// describes what changed and never causes or permits compiler work.
34 pub const BackendPreparationCache = struct {
35 allocator: std.mem.Allocator,
36 graph: incremental.ProductGraph = .{},
37 prepared: ?*Prepared = null,
38 source: ?*publication.SemanticSource = null,
39
40 pub fn init(allocator: std.mem.Allocator) BackendPreparationCache {
41 return .{ .allocator = allocator };
42 }
43
44 pub fn deinit(self: *BackendPreparationCache) void {
45 if (self.prepared) |prepared| prepared.deinit();
46 if (self.source) |source| source.deinit();
47 self.graph.deinit(self.allocator);
48 self.* = undefined;
49 }
50
51 /// A caller forks the cache to try a larger change against the current result without
52 /// disturbing the original. The fork holds its own references to the same prepared result and
53 /// source and its own copy of the product graph. A refresh or `deinit` on either cache leaves
54 /// the other cache's state unchanged.
55 pub fn fork(self: *const BackendPreparationCache) !BackendPreparationCache {
56 var result = BackendPreparationCache.init(self.allocator);
57 errdefer result.deinit();
58 if (self.prepared) |prepared| result.prepared = try prepared.retain(self.allocator);
59 if (self.source) |source| result.source = try source.retain(self.allocator);
60 result.graph = try incremental.ProductGraph.initFromGraphs(
61 self.allocator,
62 &.{self.graph},
63 &.{},
64 );
65 return result;
66 }
67
68 /// A caller reads the current prepared result to inspect or launch it. The call returns the
69 /// result of the last successful refresh, or null before the first one. The pointer is borrowed
70 /// and stays valid until the next successful refresh, and a caller that needs it longer retains
71 /// it.
72 pub fn currentPrepared(self: *const BackendPreparationCache) ?*const Prepared {
73 return self.prepared;
74 }
75
76 /// A caller uses this to compile a new draft, the caller's mutable semantic module, of the
77 /// program, reusing whatever stages of the current result still match. The call frees the draft
78 /// in every case, on success and on failure. The caller owns `report` and the work receipts in
79 /// it. The cache builds the whole replacement, graph, refresh report, retained result and
80 /// source, before it changes any state it holds, so a failure leaves the cache as it was.
81 pub fn refreshFromSemanticModule(
82 self: *BackendPreparationCache,
83 module: *semantic.SemanticModule,
84 request: publication.PreparationRequest,
85 report: *publication.PreparationReport,
86 comptime configuration: choir.product.operation.Configuration,
87 ) !BackendPreparationCacheUpdate {
88 defer module.deinit();
89 var current = request;
90 current.candidate = self.prepared;
91 const prepared = try publication.prepare(
92 self.allocator,
93 .{ .draft = module },
94 current,
95 report,
96 configuration,
97 );
98 errdefer prepared.deinit();
99 return self.replace(prepared, report);
100 }
101
102 /// A caller uses this to recompile after changing only the request, for example the device or
103 /// schedule options, with no new draft. The call starts from the cache's own semantic source,
104 /// so no draft is kept or needed. The call returns `error.MissingPreparedProduct` when the
105 /// cache holds no source yet and `error.SemanticModuleRequired` when `semantic_revision`
106 /// differs from that source. A change to the producer configuration or workspace can make the
107 /// first stage fail its admission check, and then the call returns
108 /// `error.SemanticModuleRequired` and the caller must refresh from a draft.
109 pub fn refreshFromSemanticRevision(
110 self: *BackendPreparationCache,
111 semantic_revision: *const choir.product.revision.Revision,
112 request: publication.PreparationRequest,
113 report: *publication.PreparationReport,
114 comptime configuration: choir.product.operation.Configuration,
115 ) !BackendPreparationCacheUpdate {
116 const source = self.source orelse return error.MissingPreparedProduct;
117 if (!source.record().eql(semantic_revision)) return error.SemanticModuleRequired;
118 var current = request;
119 current.candidate = self.prepared;
120 const prepared = try publication.prepare(
121 self.allocator,
122 .{ .retained = source },
123 current,
124 report,
125 configuration,
126 );
127 errdefer prepared.deinit();
128 return self.replace(prepared, report);
129 }
130
131 fn replace(
132 self: *BackendPreparationCache,
133 prepared: *Prepared,
134 report: *const publication.PreparationReport,
135 ) !BackendPreparationCacheUpdate {
136 std.debug.assert(report.completed == 7);
137 std.debug.assert(report.failure == null);
138 std.debug.assert(report.source != null);
139 var graph = try prepared.productGraph(self.allocator);
140 errdefer graph.deinit(self.allocator);
141 var live: [7]incremental.ProductRef = undefined;
142 var count: usize = 0;
143 for (report.stages[0..report.completed]) |*stage| {
144 if (stage.* != .cold) continue;
145 live[count] = stage.record().address();
146 count += 1;
147 }
148 var refresh = try graph.collectRefreshReportWithMaterialization(
149 self.allocator,
150 self.graph,
151 live[0..count],
152 .live,
153 );
154 errdefer refresh.deinit(self.allocator);
155 const retained = try prepared.retain(self.allocator);
156 errdefer retained.deinit();
157 const source = try report.source.?.retain(self.allocator);
158 const changed = if (self.prepared) |previous|
159 !previous.stage(.target).eql(prepared.stage(.target))
160 else
161 true;
162 if (self.prepared) |previous| previous.deinit();
163 if (self.source) |previous| previous.deinit();
164 self.graph.deinit(self.allocator);
165 self.prepared = prepared;
166 self.source = source;
167 self.graph = graph;
168 var reused = true;
169 for (report.stages[1..report.completed]) |stage| {
170 if (stage == .cold) reused = false;
171 }
172 return .{
173 .prepared = retained,
174 .reused = reused,
175 .request_changed = changed,
176 .refresh = refresh,
177 };
178 }
179 };