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 };