authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-19 17:41:04-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-20 01:06:29-07:00
logea2c45227af0000353fe3626293bc22e1f04d1f7
treec59072f42b02fcdeb4aab3c97db74ca620d9d819
parent7b1d3e72761e646083774e11ccf6a122f7798d35

init incremental compilation check tool


2 files changed, 228 insertions(+), 0 deletions(-)

test/incremental/hello created+13
......@@ -0,0 +1,13 @@
1#target=x86_64-linux
2#update=initial version
3#file=main.zig
4pub fn main() !void {
5 try std.io.getStdOut().writeAll("good morning\n");
6}
7#expect_stdout="good morning\n"
8#update=change the string
9#file=main.zig
10pub fn main() !void {
11 try std.io.getStdOut().writeAll("おはようございます\n");
12}
13#expect_stdout="おはようございます\n"
tools/incr-check.zig created+215
......@@ -0,0 +1,215 @@
1const std = @import("std");
2const fatal = std.process.fatal;
3const Allocator = std.mem.Allocator;
4
5pub fn main() !void {
6 var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
7 defer arena_instance.deinit();
8 const arena = arena_instance.allocator();
9
10 const args = try std.process.argsAlloc(arena);
11 const zig_exe = args[1];
12 const input_file_name = args[2];
13
14 const input_file_bytes = try std.fs.cwd().readFileAlloc(arena, input_file_name, std.math.maxInt(u32));
15 const case = try Case.parse(arena, input_file_bytes);
16
17 const prog_node = std.Progress.start(.{});
18 defer prog_node.end();
19
20 const rand_int = std.crypto.random.int(u64);
21 const tmp_dir_path = "tmp_" ++ std.fmt.hex(rand_int);
22 const local_cache_path = tmp_dir_path ++ std.fs.path.sep_str ++ ".local-cache";
23 const global_cache_path = tmp_dir_path ++ std.fs.path.sep_str ++ ".global-cache";
24 const tmp_dir = try std.fs.cwd().makeOpenPath(tmp_dir_path, .{});
25
26 const child_prog_node = prog_node.start("zig build-exe", 0);
27 defer child_prog_node.end();
28
29 var child = std.process.Child.init(&.{
30 zig_exe,
31 "build-exe",
32 case.root_source_file,
33 "-fno-llvm",
34 "-fno-lld",
35 "-fincremental",
36 "--listen=-",
37 "-target",
38 case.target_query,
39 "--cache-dir",
40 local_cache_path,
41 "--global-cache-dir",
42 global_cache_path,
43 }, arena);
44
45 child.stdin_behavior = .Pipe;
46 child.stdout_behavior = .Pipe;
47 child.stderr_behavior = .Pipe;
48 child.progress_node = child_prog_node;
49
50 var eval: Eval = .{
51 .case = case,
52 .tmp_dir = tmp_dir,
53 .child = &child,
54 };
55
56 eval.write(case.updates[0]);
57
58 try child.spawn();
59
60 var poller = std.io.poll(arena, enum { stdout, stderr }, .{
61 .stdout = child.stdout.?,
62 .stderr = child.stderr.?,
63 });
64 defer poller.deinit();
65
66 try eval.check(case.updates[0]);
67
68 for (case.updates[1..]) |update| {
69 eval.write(update);
70 try eval.requestIncrementalUpdate();
71 try eval.check(update);
72 }
73}
74
75const Eval = struct {
76 case: Case,
77 tmp_dir: std.fs.Dir,
78 child: *std.process.Child,
79
80 /// Currently this function assumes the previous updates have already been written.
81 fn write(eval: *Eval, update: Case.Update) void {
82 for (update.changes) |full_contents| {
83 eval.tmp_dir.writeFile(.{
84 .sub_path = full_contents.name,
85 .data = full_contents.bytes,
86 }) catch |err| {
87 fatal("failed to update '{s}': {s}", .{ full_contents.name, @errorName(err) });
88 };
89 }
90 for (update.deletes) |doomed_name| {
91 eval.tmp_dir.deleteFile(doomed_name) catch |err| {
92 fatal("failed to delete '{s}': {s}", .{ doomed_name, @errorName(err) });
93 };
94 }
95 }
96
97 fn check(eval: *Eval, update: Case.Update) !void {
98 _ = eval;
99 _ = update;
100 @panic("TODO: read messages from the compiler");
101 }
102
103 fn requestIncrementalUpdate(eval: *Eval) !void {
104 _ = eval;
105 @panic("TODO: send update request to the compiler");
106 }
107};
108
109const Case = struct {
110 updates: []Update,
111 root_source_file: []const u8,
112 target_query: []const u8,
113
114 const Update = struct {
115 name: []const u8,
116 outcome: Outcome,
117 changes: []const FullContents = &.{},
118 deletes: []const []const u8 = &.{},
119 };
120
121 const FullContents = struct {
122 name: []const u8,
123 bytes: []const u8,
124 };
125
126 const Outcome = union(enum) {
127 unknown,
128 compile_errors: []const ExpectedError,
129 stdout: []const u8,
130 exit_code: u8,
131 };
132
133 const ExpectedError = struct {
134 file_name: ?[]const u8 = null,
135 line: ?u32 = null,
136 column: ?u32 = null,
137 msg_exact: ?[]const u8 = null,
138 msg_substring: ?[]const u8 = null,
139 };
140
141 fn parse(arena: Allocator, bytes: []const u8) !Case {
142 var updates: std.ArrayListUnmanaged(Update) = .{};
143 var changes: std.ArrayListUnmanaged(FullContents) = .{};
144 var target_query: ?[]const u8 = null;
145 var it = std.mem.splitScalar(u8, bytes, '\n');
146 var line_n: usize = 1;
147 var root_source_file: ?[]const u8 = null;
148 while (it.next()) |line| : (line_n += 1) {
149 if (std.mem.startsWith(u8, line, "#")) {
150 var line_it = std.mem.splitScalar(u8, line, '=');
151 const key = line_it.first()[1..];
152 const val = line_it.rest();
153 if (val.len == 0) {
154 fatal("line {d}: missing value", .{line_n});
155 } else if (std.mem.eql(u8, key, "target")) {
156 if (target_query != null) fatal("line {d}: duplicate target", .{line_n});
157 target_query = val;
158 } else if (std.mem.eql(u8, key, "update")) {
159 if (updates.items.len > 0) {
160 const last_update = &updates.items[updates.items.len - 1];
161 last_update.changes = try changes.toOwnedSlice(arena);
162 }
163 try updates.append(arena, .{
164 .name = val,
165 .outcome = .unknown,
166 });
167 } else if (std.mem.eql(u8, key, "file")) {
168 if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
169
170 if (root_source_file == null)
171 root_source_file = val;
172
173 const start_index = it.index.?;
174 const src = while (true) : (line_n += 1) {
175 const old = it;
176 const next_line = it.next() orelse fatal("line {d}: unexpected EOF", .{line_n});
177 if (std.mem.startsWith(u8, next_line, "#")) {
178 const end_index = old.index.?;
179 const src = bytes[start_index..end_index];
180 it = old;
181 break src;
182 }
183 };
184
185 try changes.append(arena, .{
186 .name = val,
187 .bytes = src,
188 });
189 } else if (std.mem.eql(u8, key, "expect_stdout")) {
190 if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
191 const last_update = &updates.items[updates.items.len - 1];
192 if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n});
193 last_update.outcome = .{
194 .stdout = std.zig.string_literal.parseAlloc(arena, val) catch |err| {
195 fatal("line {d}: bad string literal: {s}", .{ line_n, @errorName(err) });
196 },
197 };
198 } else {
199 fatal("line {d}: unrecognized key '{s}'", .{ line_n, key });
200 }
201 }
202 }
203
204 if (changes.items.len > 0) {
205 const last_update = &updates.items[updates.items.len - 1];
206 last_update.changes = try changes.toOwnedSlice(arena);
207 }
208
209 return .{
210 .updates = updates.items,
211 .root_source_file = root_source_file orelse fatal("missing root source file", .{}),
212 .target_query = target_query orelse fatal("missing target", .{}),
213 };
214 }
215};