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 }