lib/chant/src/preprocess/embed.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const sys = @import("sys");
   3 const types = @import("types.zig");
   4 
   5 const Error = types.Error;
   6 const Options = types.Options;
   7 
   8 const max_resource_bytes = 16 * 1024 * 1024;
   9 const max_include_depth = 64;
  10 const sentinel_prefix = "__CHANT_EMBED_";
  11 
  12 const Status = enum(u8) {
  13     not_found = 0,
  14     found = 1,
  15     empty = 2,
  16 };
  17 
  18 const Params = struct {
  19     limit: ?usize = null,
  20     prefix: ?[]const u8 = null,
  21     suffix: ?[]const u8 = null,
  22     if_empty: ?[]const u8 = null,
  23 };
  24 
  25 const Request = struct {
  26     name: []const u8,
  27     quoted: bool,
  28     params: Params,
  29 };
  30 
  31 const Embed = struct {
  32     raw: []const u8,
  33     source_dir: []const u8,
  34 };
  35 
  36 const Directive = struct {
  37     name: []const u8,
  38     argument: []const u8,
  39 };
  40 
  41 const Macro = struct {
  42     name: []const u8,
  43     replacement: []const u8,
  44     params: []const []const u8 = &.{},
  45     function_like: bool = false,
  46     variadic: bool = false,
  47 };
  48 
  49 const Include = struct {
  50     name: []const u8,
  51     quoted: bool,
  52 };
  53 
  54 const Included = struct {
  55     path: []const u8,
  56     dir: []const u8,
  57     source: []const u8,
  58 };
  59 
  60 const Condition = struct {
  61     parent_active: bool,
  62     active: bool,
  63     branch_taken: bool,
  64 };
  65 
  66 pub const Expander = struct {
  67     arena: std.mem.Allocator,
  68     source_path: []const u8,
  69     source_dir: []const u8,
  70     options: Options,
  71     embeds: std.ArrayListUnmanaged(Embed) = .empty,
  72     macros: std.ArrayListUnmanaged(Macro) = .empty,
  73     conditions: std.ArrayListUnmanaged(Condition) = .empty,
  74     changed: bool = false,
  75 
  76     pub fn init(arena: std.mem.Allocator, source_path: []const u8, options: Options) Error!Expander {
  77         return .{
  78             .arena = arena,
  79             .source_path = source_path,
  80             .source_dir = sourceDirectory(arena, source_path) catch return error.OutOfMemory,
  81             .options = options,
  82         };
  83     }
  84 
  85     pub fn prepare(self: *Expander, source: []const u8) Error![]const u8 {
  86         const body = try self.prepareSource(self.source_path, self.source_dir, source, 0);
  87         if (!self.changed) return source;
  88 
  89         var out = std.ArrayListUnmanaged(u8).empty;
  90         try out.appendSlice(self.arena,
  91             \\#ifndef __STDC_EMBED_NOT_FOUND__
  92             \\#define __STDC_EMBED_NOT_FOUND__ 0
  93             \\#endif
  94             \\#ifndef __STDC_EMBED_FOUND__
  95             \\#define __STDC_EMBED_FOUND__ 1
  96             \\#endif
  97             \\#ifndef __STDC_EMBED_EMPTY__
  98             \\#define __STDC_EMBED_EMPTY__ 2
  99             \\#endif
 100             \\
 101         );
 102         try appendLineMarker(self.arena, &out, self.source_path);
 103         try out.appendSlice(self.arena, body);
 104         return out.toOwnedSlice(self.arena);
 105     }
 106 
 107     fn prepareSource(self: *Expander, source_path: []const u8, source_dir: []const u8, source: []const u8, depth: usize) Error![]const u8 {
 108         if (depth > max_include_depth) return error.PreprocessFailed;
 109         var body = std.ArrayListUnmanaged(u8).empty;
 110         var index: usize = 0;
 111         while (index < source.len) {
 112             const start = index;
 113             while (index < source.len and source[index] != '\n') : (index += 1) {}
 114             const line = source[start..index];
 115             const has_newline = index < source.len;
 116             if (has_newline) index += 1;
 117             var appended = false;
 118             if (parseDirective(line)) |direct| {
 119                 if (try self.appendConditionalDirective(&body, line, direct, source_dir)) {
 120                     appended = true;
 121                 } else if (!self.currentActive()) {
 122                     try body.appendSlice(self.arena, line);
 123                     appended = true;
 124                 } else {
 125                     if (std.mem.eql(u8, direct.name, "define")) {
 126                         try self.recordDefine(direct.argument);
 127                     } else if (std.mem.eql(u8, direct.name, "undef")) {
 128                         self.recordUndef(direct.argument);
 129                     } else if (std.mem.eql(u8, direct.name, "include")) {
 130                         if (try self.readInclude(direct.argument, source_dir)) |included| {
 131                             const prepared = try self.prepareSource(included.path, included.dir, included.source, depth + 1);
 132                             try self.appendPreparedInclude(&body, included.path, source_path, prepared);
 133                             appended = true;
 134                         }
 135                     }
 136                     if (!appended and std.mem.eql(u8, direct.name, "embed")) {
 137                         const id = self.embeds.items.len;
 138                         const expanded = try self.expandMacros(direct.argument);
 139                         try self.embeds.append(self.arena, .{ .raw = expanded, .source_dir = source_dir });
 140                         self.changed = true;
 141                         try appendPrint(self.arena, &body, "{s}{d}__", .{ sentinel_prefix, id });
 142                         appended = true;
 143                     }
 144                     if (!appended and try rewriteHasEmbedDefined(self, &body, direct)) appended = true;
 145                 }
 146             }
 147             if (!appended) {
 148                 if (!try self.appendHasEmbedExpressions(&body, line, source_dir)) {
 149                     try body.appendSlice(self.arena, line);
 150                 }
 151             }
 152             if (has_newline) try body.append(self.arena, '\n');
 153         }
 154         return body.toOwnedSlice(self.arena);
 155     }
 156 
 157     fn readInclude(self: *Expander, raw: []const u8, source_dir: []const u8) Error!?Included {
 158         const expanded = try self.expandMacros(raw);
 159         const include = parseInclude(expanded) orelse return null;
 160         const path = self.findInclude(include, source_dir) orelse return null;
 161         const included: ?[]const u8 = sys.fs.readFileAlloc(self.arena, path, max_resource_bytes) catch |err| switch (err) {
 162             error.OutOfMemory => return error.OutOfMemory,
 163             else => null,
 164         };
 165         const source = included orelse return null;
 166         const dir = sourceDirectory(self.arena, path) catch return error.OutOfMemory;
 167         return .{ .path = path, .dir = dir, .source = source };
 168     }
 169 
 170     fn appendPreparedInclude(self: *Expander, out: *std.ArrayListUnmanaged(u8), path: []const u8, source_path: []const u8, prepared: []const u8) Error!void {
 171         self.changed = true;
 172         try appendLineMarker(self.arena, out, path);
 173         try out.appendSlice(self.arena, prepared);
 174         if (prepared.len == 0 or prepared[prepared.len - 1] != '\n') try out.append(self.arena, '\n');
 175         try appendLineMarker(self.arena, out, source_path);
 176     }
 177 
 178     fn appendConditionalDirective(self: *Expander, out: *std.ArrayListUnmanaged(u8), line: []const u8, direct: Directive, source_dir: []const u8) Error!bool {
 179         if (std.mem.eql(u8, direct.name, "if")) {
 180             const parent_active = self.currentActive();
 181             const condition = try self.evalCondition(direct.argument, source_dir);
 182             try self.conditions.append(self.arena, .{
 183                 .parent_active = parent_active,
 184                 .active = parent_active and condition,
 185                 .branch_taken = condition,
 186             });
 187         } else if (std.mem.eql(u8, direct.name, "ifdef")) {
 188             const parent_active = self.currentActive();
 189             const condition = self.isMacroDefined(std.mem.trim(u8, direct.argument, " \t\r"));
 190             try self.conditions.append(self.arena, .{
 191                 .parent_active = parent_active,
 192                 .active = parent_active and condition,
 193                 .branch_taken = condition,
 194             });
 195         } else if (std.mem.eql(u8, direct.name, "ifndef")) {
 196             const parent_active = self.currentActive();
 197             const condition = !self.isMacroDefined(std.mem.trim(u8, direct.argument, " \t\r"));
 198             try self.conditions.append(self.arena, .{
 199                 .parent_active = parent_active,
 200                 .active = parent_active and condition,
 201                 .branch_taken = condition,
 202             });
 203         } else if (std.mem.eql(u8, direct.name, "elif")) {
 204             if (self.conditions.items.len > 0) {
 205                 const condition = try self.evalCondition(direct.argument, source_dir);
 206                 const top = &self.conditions.items[self.conditions.items.len - 1];
 207                 top.active = top.parent_active and !top.branch_taken and condition;
 208                 top.branch_taken = top.branch_taken or condition;
 209             }
 210         } else if (std.mem.eql(u8, direct.name, "elifdef")) {
 211             if (self.conditions.items.len > 0) {
 212                 const condition = self.isMacroDefined(std.mem.trim(u8, direct.argument, " \t\r"));
 213                 const top = &self.conditions.items[self.conditions.items.len - 1];
 214                 top.active = top.parent_active and !top.branch_taken and condition;
 215                 top.branch_taken = top.branch_taken or condition;
 216             }
 217         } else if (std.mem.eql(u8, direct.name, "elifndef")) {
 218             if (self.conditions.items.len > 0) {
 219                 const condition = !self.isMacroDefined(std.mem.trim(u8, direct.argument, " \t\r"));
 220                 const top = &self.conditions.items[self.conditions.items.len - 1];
 221                 top.active = top.parent_active and !top.branch_taken and condition;
 222                 top.branch_taken = top.branch_taken or condition;
 223             }
 224         } else if (std.mem.eql(u8, direct.name, "else")) {
 225             if (self.conditions.items.len > 0) {
 226                 const top = &self.conditions.items[self.conditions.items.len - 1];
 227                 top.active = top.parent_active and !top.branch_taken;
 228                 top.branch_taken = true;
 229             }
 230         } else if (std.mem.eql(u8, direct.name, "endif")) {
 231             if (self.conditions.items.len > 0) _ = self.conditions.pop();
 232         } else {
 233             return false;
 234         }
 235 
 236         if (try rewriteHasEmbedDefined(self, out, direct)) return true;
 237         if (try self.appendHasEmbedExpressions(out, line, source_dir)) return true;
 238         try out.appendSlice(self.arena, line);
 239         return true;
 240     }
 241 
 242     fn currentActive(self: *const Expander) bool {
 243         if (self.conditions.items.len == 0) return true;
 244         return self.conditions.items[self.conditions.items.len - 1].active;
 245     }
 246 
 247     fn appendHasEmbedExpressions(self: *Expander, out: *std.ArrayListUnmanaged(u8), line: []const u8, source_dir: []const u8) Error!bool {
 248         var replaced = false;
 249         const expanded = try self.expandHasEmbedExpressions(line, source_dir, &replaced);
 250         if (!replaced) return false;
 251         try out.appendSlice(self.arena, expanded);
 252         self.changed = true;
 253         return true;
 254     }
 255 
 256     fn expandHasEmbedExpressions(self: *Expander, line: []const u8, source_dir: []const u8, replaced: *bool) Error![]const u8 {
 257         var index: usize = 0;
 258         var out = std.ArrayListUnmanaged(u8).empty;
 259         while (index < line.len) {
 260             if (line[index] == '"') {
 261                 const end = findStringEnd(line, index) orelse line.len - 1;
 262                 try out.appendSlice(self.arena, line[index .. end + 1]);
 263                 index = end + 1;
 264                 continue;
 265             }
 266             if (line[index] == '\'') {
 267                 const end = findQuotedEnd(line, index, '\'') orelse line.len - 1;
 268                 try out.appendSlice(self.arena, line[index .. end + 1]);
 269                 index = end + 1;
 270                 continue;
 271             }
 272             if (!isIdentStart(line[index])) {
 273                 try out.append(self.arena, line[index]);
 274                 index += 1;
 275                 continue;
 276             }
 277             const start = index;
 278             index += 1;
 279             while (index < line.len and isIdentContinue(line[index])) : (index += 1) {}
 280             const name = line[start..index];
 281             if (!std.mem.eql(u8, name, "__has_embed")) {
 282                 try out.appendSlice(self.arena, name);
 283                 continue;
 284             }
 285             const call = skipHorizontal(line, index);
 286             if (call >= line.len or line[call] != '(') {
 287                 try out.appendSlice(self.arena, name);
 288                 continue;
 289             }
 290             const end = findBalancedEnd(line, call) orelse {
 291                 try out.appendSlice(self.arena, line[start..]);
 292                 return out.toOwnedSlice(self.arena);
 293             };
 294             try appendPrint(self.arena, &out, "{d}", .{@backingInt(try self.hasEmbed(line[call + 1 .. end], source_dir))});
 295             replaced.* = true;
 296             index = end + 1;
 297         }
 298         return out.toOwnedSlice(self.arena);
 299     }
 300 
 301     fn hasEmbed(self: *Expander, raw: []const u8, source_dir: []const u8) Error!Status {
 302         const expanded = try self.expandMacros(raw);
 303         const request = parseRequest(expanded) orelse return .not_found;
 304         const path = self.findResource(request, source_dir) orelse return .not_found;
 305         const resource = sys.fs.readFileAlloc(self.arena, path, max_resource_bytes) catch return .not_found;
 306         const count = if (request.params.limit) |limit| @min(limit, resource.len) else resource.len;
 307         return if (count == 0) .empty else .found;
 308     }
 309 
 310     pub fn expandOutput(self: *Expander, preprocessed: []const u8) Error![]const u8 {
 311         if (self.embeds.items.len == 0) return preprocessed;
 312         var out = std.ArrayListUnmanaged(u8).empty;
 313         var index: usize = 0;
 314         while (index < preprocessed.len) {
 315             if (std.mem.startsWith(u8, preprocessed[index..], sentinel_prefix)) {
 316                 var cursor = index + sentinel_prefix.len;
 317                 const digits_start = cursor;
 318                 while (cursor < preprocessed.len and std.ascii.isDigit(preprocessed[cursor])) : (cursor += 1) {}
 319                 if (cursor > digits_start and cursor + 2 <= preprocessed.len and std.mem.eql(u8, preprocessed[cursor .. cursor + 2], "__")) {
 320                     const id = std.fmt.parseInt(usize, preprocessed[digits_start..cursor], 10) catch null;
 321                     if (id) |embed_id| {
 322                         if (embed_id < self.embeds.items.len) {
 323                             const expanded = try self.expandEmbed(self.embeds.items[embed_id]);
 324                             try out.appendSlice(self.arena, expanded);
 325                             index = cursor + 2;
 326                             continue;
 327                         }
 328                     }
 329                 }
 330             }
 331             try out.append(self.arena, preprocessed[index]);
 332             index += 1;
 333         }
 334         return out.toOwnedSlice(self.arena);
 335     }
 336 
 337     fn expandEmbed(self: *Expander, embed: Embed) Error![]const u8 {
 338         const request = parseRequest(embed.raw) orelse return error.PreprocessFailed;
 339         const path = self.findResource(request, embed.source_dir) orelse return error.PreprocessFailed;
 340         const resource = sys.fs.readFileAlloc(self.arena, path, max_resource_bytes) catch |err| switch (err) {
 341             error.OutOfMemory => return error.OutOfMemory,
 342             else => return error.PreprocessFailed,
 343         };
 344         const count = if (request.params.limit) |limit| @min(limit, resource.len) else resource.len;
 345         if (count == 0) return request.params.if_empty orelse "";
 346 
 347         var out = std.ArrayListUnmanaged(u8).empty;
 348         if (request.params.prefix) |prefix| try out.appendSlice(self.arena, prefix);
 349         for (resource[0..count], 0..) |byte, byte_index| {
 350             if (byte_index != 0) try out.append(self.arena, ',');
 351             try appendPrint(self.arena, &out, "{d}", .{byte});
 352         }
 353         if (request.params.suffix) |suffix| try out.appendSlice(self.arena, suffix);
 354         return out.toOwnedSlice(self.arena);
 355     }
 356 
 357     fn findResource(self: *Expander, request: Request, source_dir: []const u8) ?[]const u8 {
 358         if (std.fs.path.isAbsolute(request.name) and fileExists(request.name)) return request.name;
 359         if (request.quoted) {
 360             if (self.findResourceInDir(source_dir, request.name)) |path| return path;
 361             if (self.findResourceInDir(".", request.name)) |path| return path;
 362         }
 363         for (self.options.include_dirs) |dir| {
 364             if (self.findResourceInDir(dir, request.name)) |path| return path;
 365         }
 366         if (!request.quoted) {
 367             if (self.findResourceInDir(source_dir, request.name)) |path| return path;
 368             if (self.findResourceInDir(".", request.name)) |path| return path;
 369         }
 370         return null;
 371     }
 372 
 373     fn findInclude(self: *Expander, include: Include, source_dir: []const u8) ?[]const u8 {
 374         if (std.fs.path.isAbsolute(include.name) and fileExists(include.name)) return include.name;
 375         if (include.quoted) {
 376             if (self.findResourceInDir(source_dir, include.name)) |path| return path;
 377             if (self.findResourceInDir(".", include.name)) |path| return path;
 378         }
 379         for (self.options.include_dirs) |dir| {
 380             if (self.findResourceInDir(dir, include.name)) |path| return path;
 381         }
 382         if (!include.quoted) {
 383             if (self.findResourceInDir(source_dir, include.name)) |path| return path;
 384             if (self.findResourceInDir(".", include.name)) |path| return path;
 385         }
 386         return null;
 387     }
 388 
 389     fn findResourceInDir(self: *Expander, dir: []const u8, name: []const u8) ?[]const u8 {
 390         const path = std.fs.path.join(self.arena, &.{ dir, name }) catch return null;
 391         if (fileExists(path)) return path;
 392         return null;
 393     }
 394 
 395     fn recordDefine(self: *Expander, raw: []const u8) Error!void {
 396         const macro = (try parseDefine(self.arena, raw)) orelse return;
 397         try self.macros.append(self.arena, macro);
 398     }
 399 
 400     fn recordUndef(self: *Expander, raw: []const u8) void {
 401         const name = parseUndef(raw) orelse return;
 402         var index: usize = 0;
 403         while (index < self.macros.items.len) {
 404             if (std.mem.eql(u8, self.macros.items[index].name, name)) {
 405                 _ = self.macros.orderedRemove(index);
 406                 continue;
 407             }
 408             index += 1;
 409         }
 410     }
 411 
 412     fn expandMacros(self: *Expander, raw: []const u8) Error![]const u8 {
 413         var current = raw;
 414         var pass: usize = 0;
 415         while (pass < 16) : (pass += 1) {
 416             var changed = false;
 417             const expanded = try self.expandMacrosOnce(current, &changed);
 418             if (!changed) return current;
 419             current = expanded;
 420         }
 421         return current;
 422     }
 423 
 424     fn expandMacrosOnce(self: *Expander, raw: []const u8, changed: *bool) Error![]const u8 {
 425         var out = std.ArrayListUnmanaged(u8).empty;
 426         var index: usize = 0;
 427         while (index < raw.len) {
 428             if (raw[index] == '"') {
 429                 const end = findStringEnd(raw, index) orelse raw.len - 1;
 430                 try out.appendSlice(self.arena, raw[index .. end + 1]);
 431                 index = end + 1;
 432                 continue;
 433             }
 434             if (raw[index] == '\'') {
 435                 const end = findQuotedEnd(raw, index, '\'') orelse raw.len - 1;
 436                 try out.appendSlice(self.arena, raw[index .. end + 1]);
 437                 index = end + 1;
 438                 continue;
 439             }
 440             if (isIdentStart(raw[index])) {
 441                 const start = index;
 442                 index += 1;
 443                 while (index < raw.len and isIdentContinue(raw[index])) : (index += 1) {}
 444                 const name = raw[start..index];
 445                 if (self.lookupMacro(name)) |macro| {
 446                     if (!macro.function_like) {
 447                         try out.appendSlice(self.arena, macro.replacement);
 448                         changed.* = true;
 449                     } else {
 450                         const call = skipHorizontal(raw, index);
 451                         if (call >= raw.len or raw[call] != '(') {
 452                             try out.appendSlice(self.arena, name);
 453                         } else if (findBalancedEnd(raw, call)) |end| {
 454                             if (try self.expandFunctionMacro(macro, raw[call + 1 .. end])) |expanded| {
 455                                 try out.appendSlice(self.arena, expanded);
 456                                 changed.* = true;
 457                                 index = end + 1;
 458                             } else {
 459                                 try out.appendSlice(self.arena, raw[start .. end + 1]);
 460                                 index = end + 1;
 461                             }
 462                         } else {
 463                             try out.appendSlice(self.arena, name);
 464                         }
 465                     }
 466                 } else {
 467                     try out.appendSlice(self.arena, name);
 468                 }
 469                 continue;
 470             }
 471             try out.append(self.arena, raw[index]);
 472             index += 1;
 473         }
 474         return out.toOwnedSlice(self.arena);
 475     }
 476 
 477     fn expandFunctionMacro(self: *Expander, macro: Macro, raw_arguments: []const u8) Error!?[]const u8 {
 478         const arguments = try self.parseMacroArguments(macro, raw_arguments) orelse return null;
 479         var expanded = try self.arena.alloc([]const u8, arguments.len);
 480         for (arguments, 0..) |argument, argument_index| {
 481             expanded[argument_index] = try self.expandMacros(argument);
 482         }
 483         return try self.substituteFunctionMacro(macro, expanded);
 484     }
 485 
 486     fn parseMacroArguments(self: *Expander, macro: Macro, raw: []const u8) Error!?[]const []const u8 {
 487         var pieces = std.ArrayListUnmanaged([]const u8).empty;
 488         const trimmed = std.mem.trim(u8, raw, " \t\r");
 489         if (trimmed.len == 0) {
 490             if (macro.params.len == 0) return &.{};
 491             try pieces.append(self.arena, "");
 492         } else {
 493             var start: usize = 0;
 494             var index: usize = 0;
 495             var depth: usize = 0;
 496             while (index < raw.len) : (index += 1) {
 497                 switch (raw[index]) {
 498                     '"' => index = findStringEnd(raw, index) orelse return null,
 499                     '\'' => index = findQuotedEnd(raw, index, '\'') orelse return null,
 500                     '(' => depth += 1,
 501                     ')' => {
 502                         if (depth > 0) depth -= 1;
 503                     },
 504                     ',' => if (depth == 0) {
 505                         try pieces.append(self.arena, std.mem.trim(u8, raw[start..index], " \t\r"));
 506                         start = index + 1;
 507                     },
 508                     else => {},
 509                 }
 510             }
 511             try pieces.append(self.arena, std.mem.trim(u8, raw[start..], " \t\r"));
 512         }
 513 
 514         if (!macro.variadic) {
 515             if (pieces.items.len != macro.params.len) return null;
 516             return try pieces.toOwnedSlice(self.arena);
 517         }
 518 
 519         const fixed_count = macro.params.len - 1;
 520         if (pieces.items.len < fixed_count) return null;
 521         var arguments = try self.arena.alloc([]const u8, macro.params.len);
 522         for (arguments[0..fixed_count], 0..) |*argument, argument_index| argument.* = pieces.items[argument_index];
 523         if (pieces.items.len == fixed_count) {
 524             arguments[fixed_count] = "";
 525         } else if (pieces.items.len == fixed_count + 1) {
 526             arguments[fixed_count] = pieces.items[fixed_count];
 527         } else {
 528             var joined = std.ArrayListUnmanaged(u8).empty;
 529             for (pieces.items[fixed_count..], 0..) |piece, piece_index| {
 530                 if (piece_index != 0) try joined.append(self.arena, ',');
 531                 try joined.appendSlice(self.arena, piece);
 532             }
 533             arguments[fixed_count] = try joined.toOwnedSlice(self.arena);
 534         }
 535         return arguments;
 536     }
 537 
 538     fn substituteFunctionMacro(self: *Expander, macro: Macro, arguments: []const []const u8) Error![]const u8 {
 539         var out = std.ArrayListUnmanaged(u8).empty;
 540         var index: usize = 0;
 541         while (index < macro.replacement.len) {
 542             if (macro.replacement[index] == '"') {
 543                 const end = findStringEnd(macro.replacement, index) orelse macro.replacement.len - 1;
 544                 try out.appendSlice(self.arena, macro.replacement[index .. end + 1]);
 545                 index = end + 1;
 546                 continue;
 547             }
 548             if (macro.replacement[index] == '\'') {
 549                 const end = findQuotedEnd(macro.replacement, index, '\'') orelse macro.replacement.len - 1;
 550                 try out.appendSlice(self.arena, macro.replacement[index .. end + 1]);
 551                 index = end + 1;
 552                 continue;
 553             }
 554             if (!isIdentStart(macro.replacement[index])) {
 555                 try out.append(self.arena, macro.replacement[index]);
 556                 index += 1;
 557                 continue;
 558             }
 559             const start = index;
 560             index += 1;
 561             while (index < macro.replacement.len and isIdentContinue(macro.replacement[index])) : (index += 1) {}
 562             const name = macro.replacement[start..index];
 563             if (macro.variadic and std.mem.eql(u8, name, "__VA_OPT__")) {
 564                 const call = skipHorizontal(macro.replacement, index);
 565                 if (call < macro.replacement.len and macro.replacement[call] == '(') {
 566                     if (findBalancedEnd(macro.replacement, call)) |end| {
 567                         if (std.mem.trim(u8, arguments[arguments.len - 1], " \t\r").len != 0) {
 568                             try out.appendSlice(self.arena, macro.replacement[call + 1 .. end]);
 569                         }
 570                         index = end + 1;
 571                         continue;
 572                     }
 573                 }
 574             }
 575             if (paramIndex(macro, name)) |argument_index| {
 576                 try out.appendSlice(self.arena, arguments[argument_index]);
 577             } else {
 578                 try out.appendSlice(self.arena, name);
 579             }
 580         }
 581         return out.toOwnedSlice(self.arena);
 582     }
 583 
 584     fn lookupMacro(self: *const Expander, name: []const u8) ?Macro {
 585         var index = self.macros.items.len;
 586         while (index > 0) {
 587             index -= 1;
 588             const macro = self.macros.items[index];
 589             if (std.mem.eql(u8, macro.name, name)) return macro;
 590         }
 591         if (builtinMacro(name)) |replacement| return .{ .name = name, .replacement = replacement };
 592         return null;
 593     }
 594 
 595     fn isMacroDefined(self: *const Expander, name: []const u8) bool {
 596         if (std.mem.eql(u8, name, "__has_embed")) return true;
 597         return self.lookupMacro(name) != null;
 598     }
 599 
 600     fn evalCondition(self: *Expander, raw: []const u8, source_dir: []const u8) Error!bool {
 601         var embed_replaced = false;
 602         const with_embed = try self.expandHasEmbedExpressions(raw, source_dir, &embed_replaced);
 603         const with_defined = try self.replaceDefinedOperators(with_embed);
 604         const expanded = try self.expandMacros(with_defined);
 605         var parser = ConditionParser{ .text = expanded };
 606         const value = parser.parse() orelse return true;
 607         return value != 0;
 608     }
 609 
 610     fn replaceDefinedOperators(self: *Expander, raw: []const u8) Error![]const u8 {
 611         var out = std.ArrayListUnmanaged(u8).empty;
 612         var index: usize = 0;
 613         while (index < raw.len) {
 614             if (raw[index] == '"') {
 615                 const end = findStringEnd(raw, index) orelse raw.len - 1;
 616                 try out.appendSlice(self.arena, raw[index .. end + 1]);
 617                 index = end + 1;
 618                 continue;
 619             }
 620             if (raw[index] == '\'') {
 621                 const end = findQuotedEnd(raw, index, '\'') orelse raw.len - 1;
 622                 try out.appendSlice(self.arena, raw[index .. end + 1]);
 623                 index = end + 1;
 624                 continue;
 625             }
 626             if (!isIdentStart(raw[index])) {
 627                 try out.append(self.arena, raw[index]);
 628                 index += 1;
 629                 continue;
 630             }
 631             const start = index;
 632             index += 1;
 633             while (index < raw.len and isIdentContinue(raw[index])) : (index += 1) {}
 634             const name = raw[start..index];
 635             if (!std.mem.eql(u8, name, "defined")) {
 636                 try out.appendSlice(self.arena, name);
 637                 continue;
 638             }
 639 
 640             var cursor = skipHorizontal(raw, index);
 641             var parenthesized = false;
 642             if (cursor < raw.len and raw[cursor] == '(') {
 643                 parenthesized = true;
 644                 cursor = skipHorizontal(raw, cursor + 1);
 645             }
 646             if (cursor >= raw.len or !isIdentStart(raw[cursor])) {
 647                 try out.appendSlice(self.arena, name);
 648                 continue;
 649             }
 650             const macro_start = cursor;
 651             cursor += 1;
 652             while (cursor < raw.len and isIdentContinue(raw[cursor])) : (cursor += 1) {}
 653             const macro_name = raw[macro_start..cursor];
 654             cursor = skipHorizontal(raw, cursor);
 655             if (parenthesized) {
 656                 if (cursor >= raw.len or raw[cursor] != ')') {
 657                     try out.appendSlice(self.arena, name);
 658                     continue;
 659                 }
 660                 cursor += 1;
 661             }
 662             try out.append(self.arena, if (self.isMacroDefined(macro_name)) '1' else '0');
 663             index = cursor;
 664         }
 665         return out.toOwnedSlice(self.arena);
 666     }
 667 };
 668 
 669 const ConditionParser = struct {
 670     text: []const u8,
 671     index: usize = 0,
 672 
 673     fn parse(self: *ConditionParser) ?i128 {
 674         const value = self.parseOr() orelse return null;
 675         self.skip();
 676         if (self.index != self.text.len) return null;
 677         return value;
 678     }
 679 
 680     fn parseOr(self: *ConditionParser) ?i128 {
 681         var value = self.parseAnd() orelse return null;
 682         while (self.consume("||")) {
 683             const right = self.parseAnd() orelse return null;
 684             value = if (value != 0 or right != 0) 1 else 0;
 685         }
 686         return value;
 687     }
 688 
 689     fn parseAnd(self: *ConditionParser) ?i128 {
 690         var value = self.parseBitOr() orelse return null;
 691         while (self.consume("&&")) {
 692             const right = self.parseBitOr() orelse return null;
 693             value = if (value != 0 and right != 0) 1 else 0;
 694         }
 695         return value;
 696     }
 697 
 698     fn parseBitOr(self: *ConditionParser) ?i128 {
 699         var value = self.parseBitXor() orelse return null;
 700         while (self.consumeSingle('|', "||")) {
 701             const right = self.parseBitXor() orelse return null;
 702             value |= right;
 703         }
 704         return value;
 705     }
 706 
 707     fn parseBitXor(self: *ConditionParser) ?i128 {
 708         var value = self.parseBitAnd() orelse return null;
 709         while (self.consume("^")) {
 710             const right = self.parseBitAnd() orelse return null;
 711             value ^= right;
 712         }
 713         return value;
 714     }
 715 
 716     fn parseBitAnd(self: *ConditionParser) ?i128 {
 717         var value = self.parseEquality() orelse return null;
 718         while (self.consumeSingle('&', "&&")) {
 719             const right = self.parseEquality() orelse return null;
 720             value &= right;
 721         }
 722         return value;
 723     }
 724 
 725     fn parseEquality(self: *ConditionParser) ?i128 {
 726         var value = self.parseRelational() orelse return null;
 727         while (true) {
 728             if (self.consume("==")) {
 729                 const right = self.parseRelational() orelse return null;
 730                 value = if (value == right) 1 else 0;
 731             } else if (self.consume("!=")) {
 732                 const right = self.parseRelational() orelse return null;
 733                 value = if (value != right) 1 else 0;
 734             } else {
 735                 return value;
 736             }
 737         }
 738     }
 739 
 740     fn parseRelational(self: *ConditionParser) ?i128 {
 741         var value = self.parseShift() orelse return null;
 742         while (true) {
 743             if (self.consume("<=")) {
 744                 const right = self.parseShift() orelse return null;
 745                 value = if (value <= right) 1 else 0;
 746             } else if (self.consume(">=")) {
 747                 const right = self.parseShift() orelse return null;
 748                 value = if (value >= right) 1 else 0;
 749             } else if (self.consume("<")) {
 750                 const right = self.parseShift() orelse return null;
 751                 value = if (value < right) 1 else 0;
 752             } else if (self.consume(">")) {
 753                 const right = self.parseShift() orelse return null;
 754                 value = if (value > right) 1 else 0;
 755             } else {
 756                 return value;
 757             }
 758         }
 759     }
 760 
 761     fn parseShift(self: *ConditionParser) ?i128 {
 762         var value = self.parseAdd() orelse return null;
 763         while (true) {
 764             if (self.consume("<<")) {
 765                 const right = self.parseAdd() orelse return null;
 766                 if (right < 0 or right > 127) return null;
 767                 value = value << @intCast(right);
 768             } else if (self.consume(">>")) {
 769                 const right = self.parseAdd() orelse return null;
 770                 if (right < 0 or right > 127) return null;
 771                 value = value >> @intCast(right);
 772             } else {
 773                 return value;
 774             }
 775         }
 776     }
 777 
 778     fn parseAdd(self: *ConditionParser) ?i128 {
 779         var value = self.parseMul() orelse return null;
 780         while (true) {
 781             if (self.consume("+")) {
 782                 const right = self.parseMul() orelse return null;
 783                 value += right;
 784             } else if (self.consume("-")) {
 785                 const right = self.parseMul() orelse return null;
 786                 value -= right;
 787             } else {
 788                 return value;
 789             }
 790         }
 791     }
 792 
 793     fn parseMul(self: *ConditionParser) ?i128 {
 794         var value = self.parseUnary() orelse return null;
 795         while (true) {
 796             if (self.consume("*")) {
 797                 const right = self.parseUnary() orelse return null;
 798                 value *= right;
 799             } else if (self.consume("/")) {
 800                 const right = self.parseUnary() orelse return null;
 801                 if (right == 0) return null;
 802                 value = @divTrunc(value, right);
 803             } else if (self.consume("%")) {
 804                 const right = self.parseUnary() orelse return null;
 805                 if (right == 0) return null;
 806                 value = @rem(value, right);
 807             } else {
 808                 return value;
 809             }
 810         }
 811     }
 812 
 813     fn parseUnary(self: *ConditionParser) ?i128 {
 814         if (self.consume("!")) return if ((self.parseUnary() orelse return null) == 0) 1 else 0;
 815         if (self.consume("+")) return self.parseUnary();
 816         if (self.consume("-")) return -(self.parseUnary() orelse return null);
 817         if (self.consume("~")) return ~(self.parseUnary() orelse return null);
 818         return self.parsePrimary();
 819     }
 820 
 821     fn parsePrimary(self: *ConditionParser) ?i128 {
 822         self.skip();
 823         if (self.index >= self.text.len) return null;
 824         if (self.text[self.index] == '(') {
 825             self.index += 1;
 826             const value = self.parseOr() orelse return null;
 827             self.skip();
 828             if (self.index >= self.text.len or self.text[self.index] != ')') return null;
 829             self.index += 1;
 830             return value;
 831         }
 832         if (self.text[self.index] == '\'') return self.parseChar();
 833         if (std.ascii.isDigit(self.text[self.index])) return self.parseNumber();
 834         if (isIdentStart(self.text[self.index])) {
 835             self.index += 1;
 836             while (self.index < self.text.len and isIdentContinue(self.text[self.index])) : (self.index += 1) {}
 837             self.skip();
 838             if (self.index < self.text.len and self.text[self.index] == '(') return null;
 839             return 0;
 840         }
 841         return null;
 842     }
 843 
 844     fn parseNumber(self: *ConditionParser) ?i128 {
 845         const start = self.index;
 846         var base: u8 = 10;
 847         if (self.index + 2 <= self.text.len and self.text[self.index] == '0' and (self.text[self.index + 1] == 'x' or self.text[self.index + 1] == 'X')) {
 848             base = 16;
 849             self.index += 2;
 850         }
 851         const digits_start = self.index;
 852         while (self.index < self.text.len) : (self.index += 1) {
 853             if (base == 16) {
 854                 if (!std.ascii.isHex(self.text[self.index])) break;
 855             } else if (!std.ascii.isDigit(self.text[self.index])) {
 856                 break;
 857             }
 858         }
 859         if (self.index == digits_start) return null;
 860         const digits = self.text[digits_start..self.index];
 861         const value = std.fmt.parseInt(i128, digits, base) catch return null;
 862         while (self.index < self.text.len and (isIdentContinue(self.text[self.index]) or self.text[self.index] == '\'')) : (self.index += 1) {}
 863         _ = start;
 864         return value;
 865     }
 866 
 867     fn parseChar(self: *ConditionParser) ?i128 {
 868         self.index += 1;
 869         if (self.index >= self.text.len) return null;
 870         const value: i128 = if (self.text[self.index] == '\\') value: {
 871             self.index += 1;
 872             if (self.index >= self.text.len) return null;
 873             break :value switch (self.text[self.index]) {
 874                 'n' => 10,
 875                 'r' => 13,
 876                 't' => 9,
 877                 '0' => 0,
 878                 else => self.text[self.index],
 879             };
 880         } else self.text[self.index];
 881         self.index += 1;
 882         if (self.index >= self.text.len or self.text[self.index] != '\'') return null;
 883         self.index += 1;
 884         return value;
 885     }
 886 
 887     fn consume(self: *ConditionParser, token: []const u8) bool {
 888         self.skip();
 889         if (!std.mem.startsWith(u8, self.text[self.index..], token)) return false;
 890         self.index += token.len;
 891         return true;
 892     }
 893 
 894     fn consumeSingle(self: *ConditionParser, token: u8, excluded: []const u8) bool {
 895         self.skip();
 896         if (std.mem.startsWith(u8, self.text[self.index..], excluded)) return false;
 897         if (self.index >= self.text.len or self.text[self.index] != token) return false;
 898         self.index += 1;
 899         return true;
 900     }
 901 
 902     fn skip(self: *ConditionParser) void {
 903         self.index = skipHorizontal(self.text, self.index);
 904     }
 905 };
 906 
 907 fn rewriteHasEmbedDefined(expander: *Expander, out: *std.ArrayListUnmanaged(u8), direct: Directive) Error!bool {
 908     const positive = std.mem.eql(u8, direct.name, "ifdef") or std.mem.eql(u8, direct.name, "elifdef");
 909     const negative = std.mem.eql(u8, direct.name, "ifndef") or std.mem.eql(u8, direct.name, "elifndef");
 910     if (!positive and !negative) return false;
 911     if (!std.mem.eql(u8, std.mem.trim(u8, direct.argument, " \t\r"), "__has_embed")) return false;
 912     const elif = std.mem.startsWith(u8, direct.name, "elif");
 913     try out.appendSlice(expander.arena, if (elif) "#elif " else "#if ");
 914     try out.append(expander.arena, if (positive) '1' else '0');
 915     expander.changed = true;
 916     return true;
 917 }
 918 
 919 fn parseRequest(raw: []const u8) ?Request {
 920     var index = skipHorizontal(raw, 0);
 921     if (index >= raw.len) return null;
 922     const quoted = switch (raw[index]) {
 923         '"' => true,
 924         '<' => false,
 925         else => return null,
 926     };
 927     const end = if (quoted)
 928         findStringEnd(raw, index) orelse return null
 929     else
 930         std.mem.indexOfScalarPos(u8, raw, index + 1, '>') orelse return null;
 931     const name = raw[index + 1 .. end];
 932     index = skipHorizontal(raw, end + 1);
 933     const params = parseParams(raw[index..]) orelse return null;
 934     return .{ .name = name, .quoted = quoted, .params = params };
 935 }
 936 
 937 fn parseInclude(raw: []const u8) ?Include {
 938     var index = skipHorizontal(raw, 0);
 939     if (index >= raw.len) return null;
 940     const quoted = switch (raw[index]) {
 941         '"' => true,
 942         '<' => false,
 943         else => return null,
 944     };
 945     const end = if (quoted)
 946         findStringEnd(raw, index) orelse return null
 947     else
 948         std.mem.indexOfScalarPos(u8, raw, index + 1, '>') orelse return null;
 949     const name = raw[index + 1 .. end];
 950     index = skipHorizontal(raw, end + 1);
 951     if (index != raw.len) return null;
 952     return .{ .name = name, .quoted = quoted };
 953 }
 954 
 955 fn parseDefine(arena: std.mem.Allocator, raw: []const u8) Error!?Macro {
 956     var index = skipHorizontal(raw, 0);
 957     if (index >= raw.len or !isIdentStart(raw[index])) return null;
 958     const start = index;
 959     index += 1;
 960     while (index < raw.len and isIdentContinue(raw[index])) : (index += 1) {}
 961     const name = raw[start..index];
 962     if (index < raw.len and raw[index] == '(') {
 963         index += 1;
 964         var params = std.ArrayListUnmanaged([]const u8).empty;
 965         var variadic = false;
 966         index = skipHorizontal(raw, index);
 967         if (index < raw.len and raw[index] == ')') {
 968             index += 1;
 969         } else {
 970             while (index < raw.len) {
 971                 index = skipHorizontal(raw, index);
 972                 if (index >= raw.len) return null;
 973                 if (index + 3 <= raw.len and std.mem.eql(u8, raw[index .. index + 3], "...")) {
 974                     try params.append(arena, "__VA_ARGS__");
 975                     variadic = true;
 976                     index += 3;
 977                     index = skipHorizontal(raw, index);
 978                     if (index >= raw.len or raw[index] != ')') return null;
 979                     index += 1;
 980                     break;
 981                 }
 982                 if (!isIdentStart(raw[index])) return null;
 983                 const param_start = index;
 984                 index += 1;
 985                 while (index < raw.len and isIdentContinue(raw[index])) : (index += 1) {}
 986                 const param = raw[param_start..index];
 987                 for (params.items) |existing| {
 988                     if (std.mem.eql(u8, existing, param)) return null;
 989                 }
 990                 try params.append(arena, param);
 991                 index = skipHorizontal(raw, index);
 992                 if (index >= raw.len) return null;
 993                 if (raw[index] == ',') {
 994                     index += 1;
 995                     continue;
 996                 }
 997                 if (raw[index] == ')') {
 998                     index += 1;
 999                     break;
1000                 }
1001                 return null;
1002             }
1003         }
1004         const replacement = std.mem.trim(u8, raw[index..], " \t\r");
1005         return .{
1006             .name = name,
1007             .replacement = replacement,
1008             .params = try params.toOwnedSlice(arena),
1009             .function_like = true,
1010             .variadic = variadic,
1011         };
1012     }
1013     const replacement = std.mem.trim(u8, raw[index..], " \t\r");
1014     return .{ .name = name, .replacement = replacement };
1015 }
1016 
1017 fn paramIndex(macro: Macro, name: []const u8) ?usize {
1018     for (macro.params, 0..) |param, index| {
1019         if (std.mem.eql(u8, param, name)) return index;
1020     }
1021     return null;
1022 }
1023 
1024 fn builtinMacro(name: []const u8) ?[]const u8 {
1025     if (std.mem.eql(u8, name, "__STDC_EMBED_NOT_FOUND__")) return "0";
1026     if (std.mem.eql(u8, name, "__STDC_EMBED_FOUND__")) return "1";
1027     if (std.mem.eql(u8, name, "__STDC_EMBED_EMPTY__")) return "2";
1028     return null;
1029 }
1030 
1031 fn parseUndef(raw: []const u8) ?[]const u8 {
1032     var index = skipHorizontal(raw, 0);
1033     if (index >= raw.len or !isIdentStart(raw[index])) return null;
1034     const start = index;
1035     index += 1;
1036     while (index < raw.len and isIdentContinue(raw[index])) : (index += 1) {}
1037     const name = raw[start..index];
1038     index = skipHorizontal(raw, index);
1039     if (index != raw.len) return null;
1040     return name;
1041 }
1042 
1043 fn parseParams(raw: []const u8) ?Params {
1044     var params: Params = .{};
1045     var index: usize = 0;
1046     while (true) {
1047         index = skipHorizontal(raw, index);
1048         if (index >= raw.len) return params;
1049         const name_start = index;
1050         if (!isIdentStart(raw[index])) return null;
1051         index += 1;
1052         while (index < raw.len and isIdentContinue(raw[index])) : (index += 1) {}
1053         const name = raw[name_start..index];
1054         index = skipHorizontal(raw, index);
1055         if (index >= raw.len or raw[index] != '(') return null;
1056         const end = findBalancedEnd(raw, index) orelse return null;
1057         const body = std.mem.trim(u8, raw[index + 1 .. end], " \t\r");
1058         if (std.mem.eql(u8, name, "limit")) {
1059             if (params.limit != null) return null;
1060             params.limit = parseLimit(body) orelse return null;
1061         } else if (std.mem.eql(u8, name, "prefix")) {
1062             if (params.prefix != null) return null;
1063             params.prefix = body;
1064         } else if (std.mem.eql(u8, name, "suffix")) {
1065             if (params.suffix != null) return null;
1066             params.suffix = body;
1067         } else if (std.mem.eql(u8, name, "if_empty")) {
1068             if (params.if_empty != null) return null;
1069             params.if_empty = body;
1070         } else {
1071             return null;
1072         }
1073         index = end + 1;
1074     }
1075 }
1076 
1077 fn parseLimit(raw: []const u8) ?usize {
1078     if (raw.len == 0) return null;
1079     if (std.mem.startsWith(u8, raw, "0x") or std.mem.startsWith(u8, raw, "0X")) return std.fmt.parseInt(usize, raw[2..], 16) catch null;
1080     return std.fmt.parseInt(usize, raw, 10) catch null;
1081 }
1082 
1083 fn parseDirective(line: []const u8) ?Directive {
1084     var index = skipHorizontal(line, 0);
1085     if (index >= line.len or line[index] != '#') return null;
1086     index = skipHorizontal(line, index + 1);
1087     if (index >= line.len or !isIdentStart(line[index])) return null;
1088     const start = index;
1089     index += 1;
1090     while (index < line.len and isIdentContinue(line[index])) : (index += 1) {}
1091     return .{ .name = line[start..index], .argument = line[index..] };
1092 }
1093 
1094 fn findStringEnd(raw: []const u8, start: usize) ?usize {
1095     return findQuotedEnd(raw, start, '"');
1096 }
1097 
1098 fn findQuotedEnd(raw: []const u8, start: usize, quote: u8) ?usize {
1099     var index = start + 1;
1100     while (index < raw.len) : (index += 1) {
1101         if (raw[index] == '\\') {
1102             index += 1;
1103             continue;
1104         }
1105         if (raw[index] == quote) return index;
1106     }
1107     return null;
1108 }
1109 
1110 fn findBalancedEnd(raw: []const u8, start: usize) ?usize {
1111     var depth: usize = 0;
1112     var index = start;
1113     while (index < raw.len) : (index += 1) {
1114         switch (raw[index]) {
1115             '"' => index = findStringEnd(raw, index) orelse return null,
1116             '(' => depth += 1,
1117             ')' => {
1118                 depth -= 1;
1119                 if (depth == 0) return index;
1120             },
1121             else => {},
1122         }
1123     }
1124     return null;
1125 }
1126 
1127 fn appendLineMarker(arena: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8), path: []const u8) Error!void {
1128     try out.appendSlice(arena, "#line 1 \"");
1129     for (path) |byte| {
1130         if (byte == '"' or byte == '\\') try out.append(arena, '\\');
1131         try out.append(arena, byte);
1132     }
1133     try out.appendSlice(arena, "\"\n");
1134 }
1135 
1136 fn appendPrint(
1137     arena: std.mem.Allocator,
1138     out: *std.ArrayListUnmanaged(u8),
1139     comptime fmt: []const u8,
1140     args: anytype,
1141 ) Error!void {
1142     const text = std.fmt.allocPrint(arena, fmt, args) catch return error.OutOfMemory;
1143     try out.appendSlice(arena, text);
1144 }
1145 
1146 fn sourceDirectory(arena: std.mem.Allocator, source_path: []const u8) ![]const u8 {
1147     const dir = std.fs.path.dirname(source_path) orelse ".";
1148     return std.fs.path.resolve(arena, &.{dir});
1149 }
1150 
1151 fn fileExists(path: []const u8) bool {
1152     sys.fs.cwd().access(sys.fs.debugIo(), path, .{}) catch return false;
1153     return true;
1154 }
1155 
1156 fn skipHorizontal(text: []const u8, start: usize) usize {
1157     var index = start;
1158     while (index < text.len and isHorizontal(text[index])) : (index += 1) {}
1159     return index;
1160 }
1161 
1162 fn isHorizontal(byte: u8) bool {
1163     return byte == ' ' or byte == '\t' or byte == '\r';
1164 }
1165 
1166 fn isIdentStart(byte: u8) bool {
1167     return std.ascii.isAlphabetic(byte) or byte == '_';
1168 }
1169 
1170 fn isIdentContinue(byte: u8) bool {
1171     return isIdentStart(byte) or std.ascii.isDigit(byte);
1172 }