| ... | ... | @@ -0,0 +1,215 @@ |
| 1 | const std = @import("std"); |
| 2 | const fatal = std.process.fatal; |
| 3 | const Allocator = std.mem.Allocator; |
| 4 | |
| 5 | pub 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 | |
| 75 | const 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 | |
| 109 | const 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 | }; |