lib/chant/src/driver.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const choir = @import("choir");
3 const sys = @import("sys");
4
5 const lexer = @import("lexer/root.zig");
6 const parse = @import("parse/root.zig");
7 const preprocess_mod = @import("preprocess/root.zig");
8 const lower = @import("lower/root.zig");
9
10 pub const Error = preprocess_mod.Error || lexer.Error || lexer.CapacityError ||
11 lexer.Exhaustion || lexer.Storage.InitError || parse.Error ||
12 parse.CapacityError || parse.Exhaustion || parse.Storage.InitError || lower.Error;
13
14 pub const Diagnostic = preprocess_mod.Diagnostic;
15 pub const Options = preprocess_mod.Options;
16
17 pub fn preprocess(
18 arena: std.mem.Allocator,
19 environ: std.process.Environ,
20 source_path: []const u8,
21 options: Options,
22 diagnostic: ?*Diagnostic,
23 ) Error![]const u8 {
24 return preprocess_mod.run(arena, environ, source_path, options, diagnostic);
25 }
26
27 pub const Compiled = struct {
28 unit: lower.Unit,
29 diagnostic: parse.Diagnostic = .{},
30 };
31
32 pub fn compileSource(
33 gpa: std.mem.Allocator,
34 arena: std.mem.Allocator,
35 ctx: *choir.Context,
36 source: []const u8,
37 file: []const u8,
38 diagnostic: ?*parse.Diagnostic,
39 ) Error!Compiled {
40 const token_survey = try lexer.survey(source, file);
41 const token_capacity = try lexer.Capacity.derive(token_survey.limits);
42 const token_bytes = try arena.alignedAlloc(
43 u8,
44 .fromByteUnits(lexer.Storage.storage_alignment),
45 token_capacity.storage_bytes,
46 );
47 var token_storage = try lexer.Storage.init(token_bytes, token_survey.limits);
48 token_storage.activate();
49 defer _ = token_storage.deinit();
50 const tokens = try token_storage.fill(token_survey, source, file);
51 const node_survey = parse.survey(tokens);
52 const node_capacity = try parse.Capacity.derive(node_survey.limits);
53 const node_bytes = try arena.alignedAlloc(
54 u8,
55 .fromByteUnits(parse.Storage.storage_alignment),
56 node_capacity.storage_bytes,
57 );
58 var node_storage = try parse.Storage.init(node_bytes, node_survey.limits);
59 node_storage.activate();
60 defer _ = node_storage.deinit();
61 var parser = try parse.init(arena, &node_storage, node_survey, tokens);
62 const tree = parse.parseTranslationUnit(&parser) catch |err| {
63 if (diagnostic) |out| out.* = parser.diagnostic;
64 return err;
65 };
66 const unit = try lower.lowerUnit(gpa, arena, ctx, tree);
67 return .{ .unit = unit };
68 }
69
70 pub fn compileFile(
71 gpa: std.mem.Allocator,
72 arena: std.mem.Allocator,
73 ctx: *choir.Context,
74 environ: std.process.Environ,
75 source_path: []const u8,
76 options: Options,
77 diagnostic: ?*Diagnostic,
78 ) Error!Compiled {
79 const source = try preprocess(arena, environ, source_path, options, diagnostic);
80 return compileSource(gpa, arena, ctx, source, source_path, diagnostic);
81 }
82
83 test "compileSource drives the full pipeline" {
84 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
85 defer arena_state.deinit();
86 const arena = arena_state.allocator();
87
88 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
89 defer ctx.deinit(arena);
90 try choir.dialects.registerAllDialects(&ctx);
91
92 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
93 \\void scale(int n, double a, double *x) {
94 \\ int i;
95 \\ for (i = 0; i < n; i++)
96 \\ x[i] = x[i] * a;
97 \\}
98 , "scale.c", null);
99 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
100 try std.testing.expectEqualStrings("scale", compiled.unit.lowered.items[0]);
101 }
102
103 test "compileSource accepts c23 bool and separated constants" {
104 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
105 defer arena_state.deinit();
106 const arena = arena_state.allocator();
107
108 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
109 defer ctx.deinit(arena);
110 try choir.dialects.registerAllDialects(&ctx);
111
112 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
113 \\void c23(int n, double *x) {
114 \\ bool enabled = true;
115 \\ int stride = 0b10'00;
116 \\ double scale = 1.5'0;
117 \\ for (int i = 0; i < n; i++)
118 \\ if (enabled)
119 \\ x[i] = x[i] + scale * stride;
120 \\}
121 , "c23.c", null);
122 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
123 try std.testing.expectEqualStrings("c23", compiled.unit.lowered.items[0]);
124 }
125
126 test "compileSource accepts c23 parser syntax" {
127 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
128 defer arena_state.deinit();
129 const arena = arena_state.allocator();
130
131 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
132 defer ctx.deinit(arena);
133 try choir.dialects.registerAllDialects(&ctx);
134
135 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
136 \\static_assert(alignof(double) == 8, "lp64");
137 \\[[maybe_unused]] thread_local alignas(16) int global [[maybe_unused]] = {};
138 \\void c23_syntax(int n, double *x) {
139 \\start:
140 \\ int i = {};
141 \\ for (i = 0; i < n; i++) {
142 \\ body:
143 \\ x[i] = x[i] + 1.0;
144 \\ end:
145 \\ }
146 \\}
147 , "c23-syntax.c", null);
148 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
149 try std.testing.expectEqualStrings("c23_syntax", compiled.unit.lowered.items[0]);
150 }
151
152 test "compileSource accepts c23 inferred scalar syntax" {
153 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
154 defer arena_state.deinit();
155 const arena = arena_state.allocator();
156
157 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
158 defer ctx.deinit(arena);
159 try choir.dialects.registerAllDialects(&ctx);
160
161 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
162 \\typedef typeof(nullptr) nullptr_t;
163 \\static_assert(!nullptr);
164 \\static_assert(sizeof(typeof(int *)) == 8);
165 \\constexpr int lanes = 4;
166 \\static_assert(lanes == 4);
167 \\alignas(lanes) int aligned_lanes = 0;
168 \\void c23_types(int n, double *x) {
169 \\ constexpr auto stride = 2;
170 \\ static_assert(stride == 2);
171 \\ unsigned _BitInt(stride + 1) mask = 3uwb;
172 \\ typeof_unqual(*x) scale = 1.0;
173 \\ for (typeof(n) i = 0; i < n; i++)
174 \\ x[i] = x[i] + scale * stride + mask;
175 \\}
176 , "c23-types.c", null);
177 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
178 try std.testing.expectEqualStrings("c23_types", compiled.unit.lowered.items[0]);
179 }
180
181 test "compileSource accepts c23 bit precise integers" {
182 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
183 defer arena_state.deinit();
184 const arena = arena_state.allocator();
185
186 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
187 defer ctx.deinit(arena);
188 try choir.dialects.registerAllDialects(&ctx);
189
190 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
191 \\void c23_bitint(int n, double *x) {
192 \\ _BitInt(17) offset = 3wb;
193 \\ unsigned _BitInt(6) mask = 0b101010uwb;
194 \\ for (int i = 0; i < n; i++)
195 \\ x[i] = x[i] + offset + (_BitInt(17))mask;
196 \\}
197 , "c23-bitint.c", null);
198 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
199 try std.testing.expectEqualStrings("c23_bitint", compiled.unit.lowered.items[0]);
200 }
201
202 test "compileSource accepts c23 decimal floating syntax" {
203 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
204 defer arena_state.deinit();
205 const arena = arena_state.allocator();
206
207 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
208 defer ctx.deinit(arena);
209 try choir.dialects.registerAllDialects(&ctx);
210
211 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
212 \\void c23_decimal(int n, double *x) {
213 \\ _Decimal32 step = 1.25df;
214 \\ _Decimal64 scale = 2.5DD;
215 \\ for (int i = 0; i < n; i++)
216 \\ x[i] = x[i] + step + scale;
217 \\}
218 , "c23-decimal.c", null);
219 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
220 try std.testing.expectEqualStrings("c23_decimal", compiled.unit.lowered.items[0]);
221 }
222
223 test "compileSource accepts c23 fixed underlying enums" {
224 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
225 defer arena_state.deinit();
226 const arena = arena_state.allocator();
227
228 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
229 defer ctx.deinit(arena);
230 try choir.dialects.registerAllDialects(&ctx);
231
232 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
233 \\enum Color : unsigned int { RED = 1, GREEN, BLUE = RED + 4 };
234 \\enum Forward : unsigned short;
235 \\enum Forward : unsigned short { SMALL = 3, SMALL_NEXT };
236 \\enum Wide { WIDE = 2147483648L, WIDE_NEXT };
237 \\static_assert(BLUE == 5);
238 \\static_assert(SMALL_NEXT == 4);
239 \\static_assert(WIDE_NEXT == 2147483649L);
240 \\void c23_enum(int n, double *x) {
241 \\ enum Color c = BLUE;
242 \\ enum Forward small = SMALL_NEXT;
243 \\ typeof(WIDE) w = WIDE_NEXT;
244 \\ for (int i = 0; i < n; i++)
245 \\ x[i] = x[i] + c + GREEN + small + w;
246 \\}
247 , "c23-enum.c", null);
248 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
249 try std.testing.expectEqualStrings("c23_enum", compiled.unit.lowered.items[0]);
250 }
251
252 test "compileSource accepts c23 aggregate initializers" {
253 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
254 defer arena_state.deinit();
255 const arena = arena_state.allocator();
256
257 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
258 defer ctx.deinit(arena);
259 try choir.dialects.registerAllDialects(&ctx);
260
261 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
262 \\void c23_init(int n, double *x) {
263 \\ int empty[3] = {};
264 \\ int values[3] = {1, 2, 3,};
265 \\ for (int i = 0; i < n; i++)
266 \\ x[i] = x[i] + empty[0] + values[2];
267 \\}
268 , "c23-init.c", null);
269 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
270 try std.testing.expectEqualStrings("c23_init", compiled.unit.lowered.items[0]);
271 }
272
273 test "compileSource accepts c23 deeper array initializers" {
274 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
275 defer arena_state.deinit();
276 const arena = arena_state.allocator();
277
278 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
279 defer ctx.deinit(arena);
280 try choir.dialects.registerAllDialects(&ctx);
281
282 const compiled = try compileSource(std.testing.allocator, arena, &ctx,
283 \\void c23_init_deep(int n, double *x) {
284 \\ char word[] = "az";
285 \\ int inferred[] = {1, 2, [4] = 5};
286 \\ int nested[2][3] = {{1, 2}, [1][2] = 6};
287 \\ int flat[][2] = {1, 2, 3, 4};
288 \\ for (int i = 0; i < n; i++)
289 \\ x[i] = x[i] + word[1] + inferred[4] + nested[1][2] + flat[1][1];
290 \\}
291 , "c23-init-deep.c", null);
292 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
293 try std.testing.expectEqualStrings("c23_init_deep", compiled.unit.lowered.items[0]);
294 }
295
296 test "compileFile accepts c23 standard header surface" {
297 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
298 defer arena_state.deinit();
299 const arena = arena_state.allocator();
300
301 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
302 defer ctx.deinit(arena);
303 try choir.dialects.registerAllDialects(&ctx);
304
305 var tmp = std.testing.tmpDir(.{});
306 defer tmp.cleanup();
307 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
308 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "header-compile.c" });
309 try sys.fs.writeFile(source_path,
310 \\#include <stdbit.h>
311 \\#include <stdckdint.h>
312 \\#include <uchar.h>
313 \\void c23_headers(int n, double *x) {
314 \\ unsigned int stride = stdc_count_ones(15u);
315 \\ unsigned int width = stdc_bit_width(stride);
316 \\ unsigned int floor = stdc_bit_floor(stride);
317 \\ unsigned int zeros = stdc_count_zeros(stride);
318 \\ unsigned int single = stdc_has_single_bit(width);
319 \\ unsigned int leading = stdc_leading_zeros(width);
320 \\ unsigned int trailing = stdc_trailing_zeros(floor);
321 \\ unsigned int first = stdc_first_trailing_one(floor);
322 \\ unsigned int ceil = stdc_bit_ceil(stride);
323 \\ unsigned int checked = 0;
324 \\ unsigned int overflow = ckd_add(&checked, stride, width);
325 \\ overflow += ckd_sub(&checked, checked, floor);
326 \\ overflow += ckd_mul(&checked, checked, 2u);
327 \\ char8_t marker = u8'a';
328 \\ for (int i = 0; i < n; i++)
329 \\ x[i] = x[i] + stride + width + floor + zeros + single + leading + trailing + first + ceil + checked + overflow + marker;
330 \\}
331 \\
332 );
333
334 const include_dir = try includeDir(arena);
335 const environ = sys.env.current();
336 const compiled = compileFile(std.testing.allocator, arena, &ctx, environ, source_path, .{
337 .include_dirs = &.{include_dir},
338 }, null) catch |err| switch (err) {
339 error.PreprocessorUnavailable => return error.SkipZigTest,
340 else => return err,
341 };
342 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
343 try std.testing.expectEqualStrings("c23_headers", compiled.unit.lowered.items[0]);
344 }
345
346 test "compileFile accepts c23 string and time header surface" {
347 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
348 defer arena_state.deinit();
349 const arena = arena_state.allocator();
350
351 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
352 defer ctx.deinit(arena);
353 try choir.dialects.registerAllDialects(&ctx);
354
355 var tmp = std.testing.tmpDir(.{});
356 defer tmp.cleanup();
357 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
358 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "lib-header-compile.c" });
359 try sys.fs.writeFile(source_path,
360 \\#include <string.h>
361 \\#include <time.h>
362 \\static_assert(__STDC_VERSION_STRING_H__ == 202311L);
363 \\static_assert(__STDC_VERSION_TIME_H__ == 202311L);
364 \\void c23_lib_headers(int n, char *dst, char *src, time_t *timer, double *x) {
365 \\ size_t len = strlen(src);
366 \\ int same = strcmp(dst, src);
367 \\ int prefix = strncmp(dst, src, len);
368 \\ int bytes = memcmp(dst, src, len);
369 \\ time_t now = time(timer);
370 \\ double diff = difftime(now, 0);
371 \\ clock_t ticks = clock();
372 \\ for (int i = 0; i < n; i++)
373 \\ x[i] = x[i] + len + same + prefix + bytes + now + diff + ticks;
374 \\}
375 \\
376 );
377
378 const include_dir = try includeDir(arena);
379 const environ = sys.env.current();
380 const compiled = compileFile(std.testing.allocator, arena, &ctx, environ, source_path, .{
381 .include_dirs = &.{include_dir},
382 }, null) catch |err| switch (err) {
383 error.PreprocessorUnavailable => return error.SkipZigTest,
384 else => return err,
385 };
386 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
387 try std.testing.expectEqualStrings("c23_lib_headers", compiled.unit.lowered.items[0]);
388 }
389
390 test "compileFile accepts c23 stdlib header surface" {
391 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
392 defer arena_state.deinit();
393 const arena = arena_state.allocator();
394
395 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
396 defer ctx.deinit(arena);
397 try choir.dialects.registerAllDialects(&ctx);
398
399 var tmp = std.testing.tmpDir(.{});
400 defer tmp.cleanup();
401 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
402 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "stdlib-header-compile.c" });
403 try sys.fs.writeFile(source_path,
404 \\#include <stdlib.h>
405 \\static_assert(__STDC_VERSION_STDLIB_H__ == 202311L);
406 \\void c23_stdlib(int n, char *text, char *buffer, double *x) {
407 \\ int as_int = atoi(text);
408 \\ double as_double = atof(text);
409 \\ long as_long = atol(text);
410 \\ long as_wide = atoll(text);
411 \\ int magnitude = abs(as_int);
412 \\ long wide = labs(as_long) + llabs(as_wide);
413 \\ size_t alignment = memalignment(buffer);
414 \\ free_sized(buffer, alignment);
415 \\ free_aligned_sized(buffer, alignment, alignment);
416 \\ for (int i = 0; i < n; i++)
417 \\ x[i] = x[i] + as_int + as_double + magnitude + wide + alignment + EXIT_SUCCESS + EXIT_FAILURE;
418 \\}
419 \\
420 );
421
422 const include_dir = try includeDir(arena);
423 const environ = sys.env.current();
424 const compiled = compileFile(std.testing.allocator, arena, &ctx, environ, source_path, .{
425 .include_dirs = &.{include_dir},
426 }, null) catch |err| switch (err) {
427 error.PreprocessorUnavailable => return error.SkipZigTest,
428 else => return err,
429 };
430 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
431 try std.testing.expectEqualStrings("c23_stdlib", compiled.unit.lowered.items[0]);
432 }
433
434 test "compileFile accepts c23 math header surface" {
435 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
436 defer arena_state.deinit();
437 const arena = arena_state.allocator();
438
439 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
440 defer ctx.deinit(arena);
441 try choir.dialects.registerAllDialects(&ctx);
442
443 var tmp = std.testing.tmpDir(.{});
444 defer tmp.cleanup();
445 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
446 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "math-header-compile.c" });
447 try sys.fs.writeFile(source_path,
448 \\#include <math.h>
449 \\static_assert(__STDC_VERSION_MATH_H__ == 202311L);
450 \\void c23_math(int n, double *x) {
451 \\ for (int i = 0; i < n; i++) {
452 \\ double expo = exp10(x[i]) + exp10m1(x[i]) + exp2m1(x[i]);
453 \\ double logs = logp1(x[i]) + log2p1(x[i]) + log10p1(x[i]);
454 \\ double trig = acospi(0.25) + asinpi(0.25) + atanpi(x[i]) + atan2pi(x[i], 1.0);
455 \\ trig += sinpi(x[i]) + cospi(x[i]) + tanpi(x[i]);
456 \\ double powers = compoundn(x[i], 2) + pown(x[i], 2) + powr(2.0, x[i]) + rootn(x[i], 2) + rsqrt(x[i]);
457 \\ double rounded = roundeven(x[i]) + fromfp(x[i], FP_INT_TONEAREST, 32) + ufromfp(x[i], FP_INT_TOWARDZERO, 32);
458 \\ rounded += fromfpx(x[i], FP_INT_UPWARD, 32) + ufromfpx(x[i], FP_INT_DOWNWARD, 32);
459 \\ double adjacent = nextup(x[i]) + nextdown(x[i]);
460 \\ double extrema = fmaximum(x[i], 1.0) + fminimum(x[i], 1.0) + fmaximum_mag(x[i], -1.0) + fminimum_mag(x[i], -1.0);
461 \\ extrema += fmaximum_num(x[i], 1.0) + fminimum_num(x[i], 1.0) + fmaximum_mag_num(x[i], -1.0) + fminimum_mag_num(x[i], -1.0);
462 \\ long exponent = llogb(x[i]);
463 \\ x[i] = x[i] + expo + logs + trig + powers + rounded + adjacent + extrema + exponent;
464 \\ }
465 \\}
466 \\
467 );
468
469 const include_dir = try includeDir(arena);
470 const environ = sys.env.current();
471 const compiled = compileFile(std.testing.allocator, arena, &ctx, environ, source_path, .{
472 .include_dirs = &.{include_dir},
473 }, null) catch |err| switch (err) {
474 error.PreprocessorUnavailable => return error.SkipZigTest,
475 else => return err,
476 };
477 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
478 try std.testing.expectEqualStrings("c23_math", compiled.unit.lowered.items[0]);
479 }
480
481 test "preprocess delegates to the system compiler when present" {
482 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
483 defer arena_state.deinit();
484 const arena = arena_state.allocator();
485
486 var tmp = std.testing.tmpDir(.{});
487 defer tmp.cleanup();
488 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
489 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "driver.c" });
490 try sys.fs.writeFile(source_path, "#define N 4\nint values[N];\n");
491
492 const environ = sys.env.current();
493 const output = preprocess(arena, environ, source_path, .{}, null) catch |err| switch (err) {
494 error.PreprocessorUnavailable => return error.SkipZigTest,
495 else => return err,
496 };
497 try std.testing.expect(std.mem.indexOf(u8, output, "int values[4];") != null);
498 }
499
500 test "preprocess accepts c23 standard headers" {
501 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
502 defer arena_state.deinit();
503 const arena = arena_state.allocator();
504
505 var tmp = std.testing.tmpDir(.{});
506 defer tmp.cleanup();
507 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
508 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "header-preprocess.c" });
509 try sys.fs.writeFile(source_path,
510 \\#include <limits.h>
511 \\#include <math.h>
512 \\#include <stdbool.h>
513 \\#include <stdbit.h>
514 \\#include <stdckdint.h>
515 \\#include <stdlib.h>
516 \\#include <string.h>
517 \\#include <time.h>
518 \\#include <uchar.h>
519 \\#if __STDC_VERSION_LIMITS_H__ != 202311L
520 \\#error bad limits version
521 \\#endif
522 \\#if BITINT_MAXWIDTH != ULLONG_WIDTH
523 \\#error bad bitint max width
524 \\#endif
525 \\#if __STDC_VERSION_MATH_H__ != 202311L
526 \\#error bad math version
527 \\#endif
528 \\#if __STDC_VERSION_STDBIT_H__ != 202311L
529 \\#error bad stdbit version
530 \\#endif
531 \\#if __STDC_VERSION_STDCKDINT_H__ != 202311L
532 \\#error bad stdckdint version
533 \\#endif
534 \\#if __STDC_VERSION_STDLIB_H__ != 202311L
535 \\#error bad stdlib version
536 \\#endif
537 \\#if __STDC_VERSION_STRING_H__ != 202311L
538 \\#error bad string version
539 \\#endif
540 \\#if __STDC_VERSION_TIME_H__ != 202311L
541 \\#error bad time version
542 \\#endif
543 \\#if __STDC_VERSION_UCHAR_H__ != 202311L
544 \\#error bad uchar version
545 \\#endif
546 \\#if __STDC_ENDIAN_NATIVE__ != __STDC_ENDIAN_LITTLE__ && __STDC_ENDIAN_NATIVE__ != __STDC_ENDIAN_BIG__
547 \\#error bad endian value
548 \\#endif
549 \\int checked(int a, int b) {
550 \\ int out;
551 \\ bool overflow = ckd_add(&out, a, b);
552 \\ char8_t byte = u8'a';
553 \\ return overflow || stdc_count_ones((unsigned int)byte) == 0;
554 \\}
555 \\double math_probe(double x) {
556 \\ return roundeven(x) + nextup(x) + logp1(x);
557 \\}
558 \\
559 );
560
561 const include_dir = try includeDir(arena);
562 const environ = sys.env.current();
563 const output = preprocess(arena, environ, source_path, .{
564 .include_dirs = &.{include_dir},
565 }, null) catch |err| switch (err) {
566 error.PreprocessorUnavailable => return error.SkipZigTest,
567 else => return err,
568 };
569 try std.testing.expect(std.mem.indexOf(u8, output, "__builtin_add_overflow") != null);
570 try std.testing.expect(std.mem.indexOf(u8, output, "char8_t byte") != null);
571 try std.testing.expect(std.mem.indexOf(u8, output, "stdc_count_ones_ull") != null);
572 try std.testing.expect(std.mem.indexOf(u8, output, "[[unsequenced]]") != null);
573 try std.testing.expect(std.mem.indexOf(u8, output, "free_sized") != null);
574 try std.testing.expect(std.mem.indexOf(u8, output, "memalignment") != null);
575 try std.testing.expect(std.mem.indexOf(u8, output, "memset_explicit") != null);
576 try std.testing.expect(std.mem.indexOf(u8, output, "timespec_getres") != null);
577 try std.testing.expect(std.mem.indexOf(u8, output, "roundeven") != null);
578 }
579
580 test "preprocess accepts c23 preprocessor additions" {
581 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
582 defer arena_state.deinit();
583 const arena = arena_state.allocator();
584
585 var tmp = std.testing.tmpDir(.{});
586 defer tmp.cleanup();
587 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
588 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "preprocessor.c" });
589 const data_path = try std.fs.path.join(arena, &.{ tmp_path, "resource.bin" });
590 const empty_path = try std.fs.path.join(arena, &.{ tmp_path, "empty.bin" });
591 try sys.fs.writeFile(data_path, &.{ 11, 12, 13 });
592 try sys.fs.writeFile(empty_path, "");
593 try sys.fs.writeFile(source_path,
594 \\#include <stddef.h>
595 \\#warning chant c23 warning smoke
596 \\#if !__has_include(<stddef.h>)
597 \\#error missing has_include
598 \\#endif
599 \\#if !__has_c_attribute(maybe_unused)
600 \\#error missing has_c_attribute
601 \\#endif
602 \\#if __has_embed("resource.bin") != __STDC_EMBED_FOUND__
603 \\#error missing has_embed
604 \\#endif
605 \\#if __has_embed("empty.bin") != __STDC_EMBED_EMPTY__
606 \\#error missing empty has_embed
607 \\#endif
608 \\#if 0
609 \\#error bad elifdef branch
610 \\#elifdef __has_embed
611 \\int elifdef_has_embed = 1;
612 \\#else
613 \\#error missing elifdef
614 \\#endif
615 \\#if 0
616 \\#error bad elifndef branch
617 \\#elifndef __has_embed
618 \\#error has_embed should be defined
619 \\#else
620 \\int elifndef_has_embed = 1;
621 \\#endif
622 \\#define C23_VA(base, ...) base __VA_OPT__(+) __VA_ARGS__
623 \\int va_empty = C23_VA(5);
624 \\int va_full = C23_VA(5, 7);
625 \\unsigned char bytes[] = {
626 \\#embed "resource.bin" limit(2) prefix(1,) suffix(, 2)
627 \\};
628 \\int empty_value =
629 \\#embed "empty.bin" if_empty(9)
630 \\;
631 \\
632 );
633
634 const include_dir = try includeDir(arena);
635 const output = preprocess(arena, sys.env.current(), source_path, .{
636 .include_dirs = &.{include_dir},
637 }, null) catch |err| switch (err) {
638 error.PreprocessorUnavailable => return error.SkipZigTest,
639 else => return err,
640 };
641 try std.testing.expect(std.mem.indexOf(u8, output, "int elifdef_has_embed = 1;") != null);
642 try std.testing.expect(std.mem.indexOf(u8, output, "int elifndef_has_embed = 1;") != null);
643 try std.testing.expect(std.mem.indexOf(u8, output, "int va_empty = 5 ;") != null);
644 try std.testing.expect(std.mem.indexOf(u8, output, "int va_full = 5 + 7;") != null);
645 try std.testing.expect(std.mem.indexOf(u8, output, "1,11,12, 2") != null);
646 try std.testing.expect(std.mem.indexOf(u8, output, "int empty_value =\n9") != null);
647 }
648
649 test "compileFile accepts c23 embed output" {
650 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
651 defer arena_state.deinit();
652 const arena = arena_state.allocator();
653
654 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
655 defer ctx.deinit(arena);
656 try choir.dialects.registerAllDialects(&ctx);
657
658 var tmp = std.testing.tmpDir(.{});
659 defer tmp.cleanup();
660 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
661 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "embed-compile.c" });
662 const data_path = try std.fs.path.join(arena, &.{ tmp_path, "resource.bin" });
663 try sys.fs.writeFile(data_path, &.{5});
664 try sys.fs.writeFile(source_path,
665 \\#if __has_embed("resource.bin") != __STDC_EMBED_FOUND__
666 \\#error missing embed resource
667 \\#endif
668 \\void c23_embed(int n, double *x) {
669 \\ int add =
670 \\#embed "resource.bin"
671 \\ ;
672 \\ for (int i = 0; i < n; i++)
673 \\ x[i] = x[i] + add;
674 \\}
675 \\
676 );
677
678 const compiled = compileFile(std.testing.allocator, arena, &ctx, sys.env.current(), source_path, .{}, null) catch |err| switch (err) {
679 error.PreprocessorUnavailable => return error.SkipZigTest,
680 else => return err,
681 };
682 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
683 try std.testing.expectEqualStrings("c23_embed", compiled.unit.lowered.items[0]);
684 }
685
686 test "compileFile accepts c23 embed inside included headers" {
687 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
688 defer arena_state.deinit();
689 const arena = arena_state.allocator();
690
691 var ctx = try choir.Context.init(arena, choir.Context.Limits.testing);
692 defer ctx.deinit(arena);
693 try choir.dialects.registerAllDialects(&ctx);
694
695 var tmp = std.testing.tmpDir(.{});
696 defer tmp.cleanup();
697 const tmp_path = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], arena);
698 const include_dir = try std.fs.path.join(arena, &.{ tmp_path, "include" });
699 try sys.fs.createDirPath(include_dir);
700 const source_path = try std.fs.path.join(arena, &.{ tmp_path, "header-embed-main.c" });
701 const header_path = try std.fs.path.join(arena, &.{ include_dir, "header_embed.h" });
702 const data_path = try std.fs.path.join(arena, &.{ include_dir, "payload.bin" });
703 try sys.fs.writeFile(data_path, &.{ 17, 18 });
704 try sys.fs.writeFile(header_path,
705 \\#define HEADER_RESOURCE "payload.bin"
706 \\#define HEADER_LIMIT limit(1)
707 \\void c23_header_embed(int n, double *x) {
708 \\ int add =
709 \\#embed HEADER_RESOURCE HEADER_LIMIT
710 \\ ;
711 \\ for (int i = 0; i < n; i++)
712 \\ x[i] = x[i] + add;
713 \\}
714 \\
715 );
716 try sys.fs.writeFile(source_path,
717 \\#include <header_embed.h>
718 \\
719 );
720
721 const compiled = compileFile(std.testing.allocator, arena, &ctx, sys.env.current(), source_path, .{
722 .include_dirs = &.{include_dir},
723 }, null) catch |err| switch (err) {
724 error.PreprocessorUnavailable => return error.SkipZigTest,
725 else => return err,
726 };
727 try std.testing.expectEqual(@as(usize, 1), compiled.unit.lowered.items.len);
728 try std.testing.expectEqualStrings("c23_header_embed", compiled.unit.lowered.items[0]);
729 }
730
731 pub fn includeDir(arena: std.mem.Allocator) ![]const u8 {
732 if (directoryExists("include")) return std.fs.path.resolve(arena, &.{"include"});
733 return std.fs.path.resolve(arena, &.{ "lib", "chant", "include" });
734 }
735
736 fn directoryExists(path: []const u8) bool {
737 sys.fs.cwd().access(sys.fs.debugIo(), path, .{}) catch return false;
738 return true;
739 }