zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

main.zig (33691B) - Raw


      1 const std = @import("std");
      2 const log = std.log;
      3 const assert = std.debug.assert;
      4 const Ast = std.zig.Ast;
      5 const Walk = @import("Walk");
      6 const markdown = @import("markdown.zig");
      7 const Decl = Walk.Decl;
      8 
      9 const fileSourceHtml = @import("html_render.zig").fileSourceHtml;
     10 const appendEscaped = @import("html_render.zig").appendEscaped;
     11 const resolveDeclLink = @import("html_render.zig").resolveDeclLink;
     12 const missing_feature_url_escape = @import("html_render.zig").missing_feature_url_escape;
     13 
     14 const gpa = std.heap.wasm_allocator;
     15 
     16 const js = struct {
     17     /// Keep in sync with the `LOG_` constants in `main.js`.
     18     const LogLevel = enum(u8) {
     19         err,
     20         warn,
     21         info,
     22         debug,
     23     };
     24 
     25     extern "js" fn log(level: LogLevel, ptr: [*]const u8, len: usize) void;
     26 };
     27 
     28 pub const std_options: std.Options = .{
     29     .logFn = logFn,
     30     //.log_level = .debug,
     31 };
     32 
     33 pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
     34     _ = st;
     35     _ = addr;
     36     log.err("panic: {s}", .{msg});
     37     @trap();
     38 }
     39 
     40 fn logFn(
     41     comptime message_level: log.Level,
     42     comptime scope: @TypeOf(.enum_literal),
     43     comptime format: []const u8,
     44     args: anytype,
     45 ) void {
     46     const prefix = if (scope == .default) "" else @tagName(scope) ++ ": ";
     47     var buf: [500]u8 = undefined;
     48     const line = std.fmt.bufPrint(&buf, prefix ++ format, args) catch l: {
     49         buf[buf.len - 3 ..][0..3].* = "...".*;
     50         break :l &buf;
     51     };
     52     js.log(@field(js.LogLevel, @tagName(message_level)), line.ptr, line.len);
     53 }
     54 
     55 export fn alloc(n: usize) [*]u8 {
     56     const slice = gpa.alloc(u8, n) catch @panic("OOM");
     57     return slice.ptr;
     58 }
     59 
     60 export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
     61     const tar_bytes = tar_ptr[0..tar_len];
     62     //log.debug("received {d} bytes of tar file", .{tar_bytes.len});
     63 
     64     unpackInner(tar_bytes) catch |err| {
     65         std.debug.panic("unable to unpack tar: {s}", .{@errorName(err)});
     66     };
     67 }
     68 
     69 var query_string: std.ArrayListUnmanaged(u8) = .empty;
     70 var query_results: std.ArrayListUnmanaged(Decl.Index) = .empty;
     71 
     72 /// Resizes the query string to be the correct length; returns the pointer to
     73 /// the query string.
     74 export fn query_begin(query_string_len: usize) [*]u8 {
     75     query_string.resize(gpa, query_string_len) catch @panic("OOM");
     76     return query_string.items.ptr;
     77 }
     78 
     79 /// Executes the query. Returns the pointer to the query results which is an
     80 /// array of u32.
     81 /// The first element is the length of the array.
     82 /// Subsequent elements are Decl.Index values which are all public
     83 /// declarations.
     84 export fn query_exec(ignore_case: bool) [*]Decl.Index {
     85     const query = query_string.items;
     86     log.debug("querying '{s}'", .{query});
     87     query_exec_fallible(query, ignore_case) catch |err| switch (err) {
     88         error.OutOfMemory => @panic("OOM"),
     89     };
     90     query_results.items[0] = @enumFromInt(query_results.items.len - 1);
     91     return query_results.items.ptr;
     92 }
     93 
     94 const max_matched_items = 1000;
     95 
     96 fn query_exec_fallible(query: []const u8, ignore_case: bool) !void {
     97     const Score = packed struct(u32) {
     98         points: u16,
     99         segments: u16,
    100     };
    101     const g = struct {
    102         var full_path_search_text: std.ArrayListUnmanaged(u8) = .empty;
    103         var full_path_search_text_lower: std.ArrayListUnmanaged(u8) = .empty;
    104         var doc_search_text: std.ArrayListUnmanaged(u8) = .empty;
    105         /// Each element matches a corresponding query_results element.
    106         var scores: std.ArrayListUnmanaged(Score) = .empty;
    107     };
    108 
    109     // First element stores the size of the list.
    110     try query_results.resize(gpa, 1);
    111     // Corresponding point value is meaningless and therefore undefined.
    112     try g.scores.resize(gpa, 1);
    113 
    114     decl_loop: for (Walk.decls.items, 0..) |*decl, decl_index| {
    115         const info = decl.extra_info();
    116         if (!info.is_pub) continue;
    117 
    118         try decl.reset_with_path(&g.full_path_search_text);
    119         if (decl.parent != .none)
    120             try Decl.append_parent_ns(&g.full_path_search_text, decl.parent);
    121         try g.full_path_search_text.appendSlice(gpa, info.name);
    122 
    123         try g.full_path_search_text_lower.resize(gpa, g.full_path_search_text.items.len);
    124         @memcpy(g.full_path_search_text_lower.items, g.full_path_search_text.items);
    125 
    126         const ast = decl.file.get_ast();
    127         if (info.first_doc_comment.unwrap()) |first_doc_comment| {
    128             try collect_docs(&g.doc_search_text, ast, first_doc_comment);
    129         }
    130 
    131         if (ignore_case) {
    132             ascii_lower(g.full_path_search_text_lower.items);
    133             ascii_lower(g.doc_search_text.items);
    134         }
    135 
    136         var it = std.mem.tokenizeScalar(u8, query, ' ');
    137         var points: u16 = 0;
    138         var bypass_limit = false;
    139         while (it.next()) |term| {
    140             // exact, case sensitive match of full decl path
    141             if (std.mem.eql(u8, g.full_path_search_text.items, term)) {
    142                 points += 4;
    143                 bypass_limit = true;
    144                 continue;
    145             }
    146             // exact, case sensitive match of just decl name
    147             if (std.mem.eql(u8, info.name, term)) {
    148                 points += 3;
    149                 bypass_limit = true;
    150                 continue;
    151             }
    152             // substring, case insensitive match of full decl path
    153             if (std.mem.indexOf(u8, g.full_path_search_text_lower.items, term) != null) {
    154                 points += 2;
    155                 continue;
    156             }
    157             if (std.mem.indexOf(u8, g.doc_search_text.items, term) != null) {
    158                 points += 1;
    159                 continue;
    160             }
    161             continue :decl_loop;
    162         }
    163 
    164         if (query_results.items.len < max_matched_items or bypass_limit) {
    165             try query_results.append(gpa, @enumFromInt(decl_index));
    166             try g.scores.append(gpa, .{
    167                 .points = points,
    168                 .segments = @intCast(count_scalar(g.full_path_search_text.items, '.')),
    169             });
    170         }
    171     }
    172 
    173     const sort_context: struct {
    174         pub fn swap(sc: @This(), a_index: usize, b_index: usize) void {
    175             _ = sc;
    176             std.mem.swap(Score, &g.scores.items[a_index], &g.scores.items[b_index]);
    177             std.mem.swap(Decl.Index, &query_results.items[a_index], &query_results.items[b_index]);
    178         }
    179 
    180         pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
    181             _ = sc;
    182             const a_score = g.scores.items[a_index];
    183             const b_score = g.scores.items[b_index];
    184             if (b_score.points < a_score.points) {
    185                 return true;
    186             } else if (b_score.points > a_score.points) {
    187                 return false;
    188             } else if (a_score.segments < b_score.segments) {
    189                 return true;
    190             } else if (a_score.segments > b_score.segments) {
    191                 return false;
    192             } else {
    193                 const a_decl = query_results.items[a_index];
    194                 const b_decl = query_results.items[b_index];
    195                 const a_file_path = a_decl.get().file.path();
    196                 const b_file_path = b_decl.get().file.path();
    197                 // This neglects to check the local namespace inside the file.
    198                 return std.mem.lessThan(u8, b_file_path, a_file_path);
    199             }
    200         }
    201     } = .{};
    202 
    203     std.mem.sortUnstableContext(1, query_results.items.len, sort_context);
    204 
    205     if (query_results.items.len > max_matched_items)
    206         query_results.shrinkRetainingCapacity(max_matched_items);
    207 }
    208 
    209 const String = Slice(u8);
    210 
    211 fn Slice(T: type) type {
    212     return packed struct(u64) {
    213         ptr: u32,
    214         len: u32,
    215 
    216         fn init(s: []const T) @This() {
    217             return .{
    218                 .ptr = @intFromPtr(s.ptr),
    219                 .len = s.len,
    220             };
    221         }
    222     };
    223 }
    224 
    225 const ErrorIdentifier = packed struct(u64) {
    226     token_index: Ast.TokenIndex,
    227     decl_index: Decl.Index,
    228 
    229     fn hasDocs(ei: ErrorIdentifier) bool {
    230         const decl_index = ei.decl_index;
    231         const ast = decl_index.get().file.get_ast();
    232         const token_index = ei.token_index;
    233         if (token_index == 0) return false;
    234         return ast.tokenTag(token_index - 1) == .doc_comment;
    235     }
    236 
    237     fn html(ei: ErrorIdentifier, base_decl: Decl.Index, out: *std.ArrayListUnmanaged(u8)) Oom!void {
    238         const decl_index = ei.decl_index;
    239         const ast = decl_index.get().file.get_ast();
    240         const name = ast.tokenSlice(ei.token_index);
    241         const has_link = base_decl != decl_index;
    242 
    243         try out.appendSlice(gpa, "<dt>");
    244         try out.appendSlice(gpa, name);
    245         if (has_link) {
    246             try out.appendSlice(gpa, " <a href=\"#");
    247             _ = missing_feature_url_escape;
    248             try decl_index.get().fqn(out);
    249             try out.appendSlice(gpa, "\">");
    250             try out.appendSlice(gpa, decl_index.get().extra_info().name);
    251             try out.appendSlice(gpa, "</a>");
    252         }
    253         try out.appendSlice(gpa, "</dt>");
    254 
    255         if (Decl.findFirstDocComment(ast, ei.token_index).unwrap()) |first_doc_comment| {
    256             try out.appendSlice(gpa, "<dd>");
    257             try render_docs(out, decl_index, first_doc_comment, false);
    258             try out.appendSlice(gpa, "</dd>");
    259         }
    260     }
    261 };
    262 
    263 var string_result: std.ArrayListUnmanaged(u8) = .empty;
    264 var error_set_result: std.StringArrayHashMapUnmanaged(ErrorIdentifier) = .empty;
    265 
    266 export fn decl_error_set(decl_index: Decl.Index) Slice(ErrorIdentifier) {
    267     return Slice(ErrorIdentifier).init(decl_error_set_fallible(decl_index) catch @panic("OOM"));
    268 }
    269 
    270 export fn error_set_node_list(base_decl: Decl.Index, node: Ast.Node.Index) Slice(ErrorIdentifier) {
    271     error_set_result.clearRetainingCapacity();
    272     addErrorsFromExpr(base_decl, &error_set_result, node) catch @panic("OOM");
    273     sort_error_set_result();
    274     return Slice(ErrorIdentifier).init(error_set_result.values());
    275 }
    276 
    277 export fn fn_error_set_decl(decl_index: Decl.Index, node: Ast.Node.Index) Decl.Index {
    278     return switch (decl_index.get().file.categorize_expr(node)) {
    279         .alias => |aliasee| fn_error_set_decl(aliasee, aliasee.get().ast_node),
    280         else => decl_index,
    281     };
    282 }
    283 
    284 fn decl_error_set_fallible(decl_index: Decl.Index) Oom![]ErrorIdentifier {
    285     error_set_result.clearRetainingCapacity();
    286     try addErrorsFromDecl(decl_index, &error_set_result);
    287     sort_error_set_result();
    288     return error_set_result.values();
    289 }
    290 
    291 fn sort_error_set_result() void {
    292     const sort_context: struct {
    293         pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
    294             _ = sc;
    295             const a_name = error_set_result.keys()[a_index];
    296             const b_name = error_set_result.keys()[b_index];
    297             return std.mem.lessThan(u8, a_name, b_name);
    298         }
    299     } = .{};
    300     error_set_result.sortUnstable(sort_context);
    301 }
    302 
    303 fn addErrorsFromDecl(
    304     decl_index: Decl.Index,
    305     out: *std.StringArrayHashMapUnmanaged(ErrorIdentifier),
    306 ) Oom!void {
    307     switch (decl_index.get().categorize()) {
    308         .error_set => |node| try addErrorsFromExpr(decl_index, out, node),
    309         .alias => |aliasee| try addErrorsFromDecl(aliasee, out),
    310         else => |cat| log.debug("unable to addErrorsFromDecl: {any}", .{cat}),
    311     }
    312 }
    313 
    314 fn addErrorsFromExpr(
    315     decl_index: Decl.Index,
    316     out: *std.StringArrayHashMapUnmanaged(ErrorIdentifier),
    317     node: Ast.Node.Index,
    318 ) Oom!void {
    319     const decl = decl_index.get();
    320     const ast = decl.file.get_ast();
    321 
    322     switch (decl.file.categorize_expr(node)) {
    323         .error_set => |n| switch (ast.nodeTag(n)) {
    324             .error_set_decl => {
    325                 try addErrorsFromNode(decl_index, out, node);
    326             },
    327             .merge_error_sets => {
    328                 const lhs, const rhs = ast.nodeData(n).node_and_node;
    329                 try addErrorsFromExpr(decl_index, out, lhs);
    330                 try addErrorsFromExpr(decl_index, out, rhs);
    331             },
    332             else => unreachable,
    333         },
    334         .alias => |aliasee| {
    335             try addErrorsFromDecl(aliasee, out);
    336         },
    337         else => return,
    338     }
    339 }
    340 
    341 fn addErrorsFromNode(
    342     decl_index: Decl.Index,
    343     out: *std.StringArrayHashMapUnmanaged(ErrorIdentifier),
    344     node: Ast.Node.Index,
    345 ) Oom!void {
    346     const decl = decl_index.get();
    347     const ast = decl.file.get_ast();
    348     const error_token = ast.nodeMainToken(node);
    349     var tok_i = error_token + 2;
    350     while (true) : (tok_i += 1) switch (ast.tokenTag(tok_i)) {
    351         .doc_comment, .comma => {},
    352         .identifier => {
    353             const name = ast.tokenSlice(tok_i);
    354             const gop = try out.getOrPut(gpa, name);
    355             // If there are more than one, take the one with doc comments.
    356             // If they both have doc comments, prefer the existing one.
    357             const new: ErrorIdentifier = .{
    358                 .token_index = tok_i,
    359                 .decl_index = decl_index,
    360             };
    361             if (!gop.found_existing or
    362                 (!gop.value_ptr.hasDocs() and new.hasDocs()))
    363             {
    364                 gop.value_ptr.* = new;
    365             }
    366         },
    367         .r_brace => break,
    368         else => unreachable,
    369     };
    370 }
    371 
    372 export fn type_fn_fields(decl_index: Decl.Index) Slice(Ast.Node.Index) {
    373     return decl_fields(decl_index);
    374 }
    375 
    376 export fn decl_fields(decl_index: Decl.Index) Slice(Ast.Node.Index) {
    377     return Slice(Ast.Node.Index).init(decl_fields_fallible(decl_index) catch @panic("OOM"));
    378 }
    379 
    380 export fn decl_params(decl_index: Decl.Index) Slice(Ast.Node.Index) {
    381     return Slice(Ast.Node.Index).init(decl_params_fallible(decl_index) catch @panic("OOM"));
    382 }
    383 
    384 fn decl_fields_fallible(decl_index: Decl.Index) ![]Ast.Node.Index {
    385     const decl = decl_index.get();
    386     const ast = decl.file.get_ast();
    387 
    388     switch (decl.categorize()) {
    389         .type_function => {
    390             // If the type function returns a reference to another type function, get the fields from there
    391             if (decl.get_type_fn_return_type_fn()) |function_decl| {
    392                 return decl_fields_fallible(function_decl);
    393             }
    394             // If the type function returns a container, such as a `struct`, read that container's fields
    395             if (decl.get_type_fn_return_expr()) |return_expr| {
    396                 switch (ast.nodeTag(return_expr)) {
    397                     .container_decl, .container_decl_trailing, .container_decl_two, .container_decl_two_trailing, .container_decl_arg, .container_decl_arg_trailing => {
    398                         return ast_decl_fields_fallible(ast, return_expr);
    399                     },
    400                     else => {},
    401                 }
    402             }
    403             return &.{};
    404         },
    405         else => {
    406             const value_node = decl.value_node() orelse return &.{};
    407             return ast_decl_fields_fallible(ast, value_node);
    408         },
    409     }
    410 }
    411 
    412 fn ast_decl_fields_fallible(ast: *Ast, ast_index: Ast.Node.Index) ![]Ast.Node.Index {
    413     const g = struct {
    414         var result: std.ArrayListUnmanaged(Ast.Node.Index) = .empty;
    415     };
    416     g.result.clearRetainingCapacity();
    417     var buf: [2]Ast.Node.Index = undefined;
    418     const container_decl = ast.fullContainerDecl(&buf, ast_index) orelse return &.{};
    419     for (container_decl.ast.members) |member_node| switch (ast.nodeTag(member_node)) {
    420         .container_field_init,
    421         .container_field_align,
    422         .container_field,
    423         => try g.result.append(gpa, member_node),
    424 
    425         else => continue,
    426     };
    427     return g.result.items;
    428 }
    429 
    430 fn decl_params_fallible(decl_index: Decl.Index) ![]Ast.Node.Index {
    431     const g = struct {
    432         var result: std.ArrayListUnmanaged(Ast.Node.Index) = .empty;
    433     };
    434     g.result.clearRetainingCapacity();
    435     const decl = decl_index.get();
    436     const ast = decl.file.get_ast();
    437     const value_node = decl.value_node() orelse return &.{};
    438     var buf: [1]Ast.Node.Index = undefined;
    439     const fn_proto = ast.fullFnProto(&buf, value_node) orelse return &.{};
    440     try g.result.appendSlice(gpa, fn_proto.ast.params);
    441     return g.result.items;
    442 }
    443 
    444 export fn error_html(base_decl: Decl.Index, error_identifier: ErrorIdentifier) String {
    445     string_result.clearRetainingCapacity();
    446     error_identifier.html(base_decl, &string_result) catch @panic("OOM");
    447     return String.init(string_result.items);
    448 }
    449 
    450 export fn decl_field_html(decl_index: Decl.Index, field_node: Ast.Node.Index) String {
    451     string_result.clearRetainingCapacity();
    452     decl_field_html_fallible(&string_result, decl_index, field_node) catch @panic("OOM");
    453     return String.init(string_result.items);
    454 }
    455 
    456 export fn decl_param_html(decl_index: Decl.Index, param_node: Ast.Node.Index) String {
    457     string_result.clearRetainingCapacity();
    458     decl_param_html_fallible(&string_result, decl_index, param_node) catch @panic("OOM");
    459     return String.init(string_result.items);
    460 }
    461 
    462 fn decl_field_html_fallible(
    463     out: *std.ArrayListUnmanaged(u8),
    464     decl_index: Decl.Index,
    465     field_node: Ast.Node.Index,
    466 ) !void {
    467     const decl = decl_index.get();
    468     const ast = decl.file.get_ast();
    469     try out.appendSlice(gpa, "<pre><code>");
    470     try fileSourceHtml(decl.file, out, field_node, .{});
    471     try out.appendSlice(gpa, "</code></pre>");
    472 
    473     const field = ast.fullContainerField(field_node).?;
    474 
    475     if (Decl.findFirstDocComment(ast, field.firstToken()).unwrap()) |first_doc_comment| {
    476         try out.appendSlice(gpa, "<div class=\"fieldDocs\">");
    477         try render_docs(out, decl_index, first_doc_comment, false);
    478         try out.appendSlice(gpa, "</div>");
    479     }
    480 }
    481 
    482 fn decl_param_html_fallible(
    483     out: *std.ArrayListUnmanaged(u8),
    484     decl_index: Decl.Index,
    485     param_node: Ast.Node.Index,
    486 ) !void {
    487     const decl = decl_index.get();
    488     const ast = decl.file.get_ast();
    489     const colon = ast.firstToken(param_node) - 1;
    490     const name_token = colon - 1;
    491     const first_doc_comment = f: {
    492         var it = ast.firstToken(param_node);
    493         while (it > 0) {
    494             it -= 1;
    495             switch (ast.tokenTag(it)) {
    496                 .doc_comment, .colon, .identifier, .keyword_comptime, .keyword_noalias => {},
    497                 else => break,
    498             }
    499         }
    500         break :f it + 1;
    501     };
    502     const name = ast.tokenSlice(name_token);
    503 
    504     try out.appendSlice(gpa, "<pre><code>");
    505     try appendEscaped(out, name);
    506     try out.appendSlice(gpa, ": ");
    507     try fileSourceHtml(decl.file, out, param_node, .{});
    508     try out.appendSlice(gpa, "</code></pre>");
    509 
    510     if (ast.tokenTag(first_doc_comment) == .doc_comment) {
    511         try out.appendSlice(gpa, "<div class=\"fieldDocs\">");
    512         try render_docs(out, decl_index, first_doc_comment, false);
    513         try out.appendSlice(gpa, "</div>");
    514     }
    515 }
    516 
    517 export fn decl_fn_proto_html(decl_index: Decl.Index, linkify_fn_name: bool) String {
    518     const decl = decl_index.get();
    519     const ast = decl.file.get_ast();
    520     const proto_node = switch (ast.nodeTag(decl.ast_node)) {
    521         .fn_decl => ast.nodeData(decl.ast_node).node_and_node[0],
    522 
    523         .fn_proto,
    524         .fn_proto_one,
    525         .fn_proto_simple,
    526         .fn_proto_multi,
    527         => decl.ast_node,
    528 
    529         else => unreachable,
    530     };
    531 
    532     string_result.clearRetainingCapacity();
    533     fileSourceHtml(decl.file, &string_result, proto_node, .{
    534         .skip_doc_comments = true,
    535         .skip_comments = true,
    536         .collapse_whitespace = true,
    537         .fn_link = if (linkify_fn_name) decl_index else .none,
    538     }) catch |err| {
    539         std.debug.panic("unable to render source: {s}", .{@errorName(err)});
    540     };
    541     return String.init(string_result.items);
    542 }
    543 
    544 export fn decl_source_html(decl_index: Decl.Index) String {
    545     const decl = decl_index.get();
    546 
    547     string_result.clearRetainingCapacity();
    548     fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
    549         std.debug.panic("unable to render source: {s}", .{@errorName(err)});
    550     };
    551     return String.init(string_result.items);
    552 }
    553 
    554 export fn decl_doctest_html(decl_index: Decl.Index) String {
    555     const decl = decl_index.get();
    556     const doctest_ast_node = decl.file.get().doctests.get(decl.ast_node) orelse
    557         return String.init("");
    558 
    559     string_result.clearRetainingCapacity();
    560     fileSourceHtml(decl.file, &string_result, doctest_ast_node, .{}) catch |err| {
    561         std.debug.panic("unable to render source: {s}", .{@errorName(err)});
    562     };
    563     return String.init(string_result.items);
    564 }
    565 
    566 export fn decl_fqn(decl_index: Decl.Index) String {
    567     const decl = decl_index.get();
    568     string_result.clearRetainingCapacity();
    569     decl.fqn(&string_result) catch @panic("OOM");
    570     return String.init(string_result.items);
    571 }
    572 
    573 export fn decl_parent(decl_index: Decl.Index) Decl.Index {
    574     const decl = decl_index.get();
    575     return decl.parent;
    576 }
    577 
    578 export fn fn_error_set(decl_index: Decl.Index) Ast.Node.OptionalIndex {
    579     const decl = decl_index.get();
    580     const ast = decl.file.get_ast();
    581     var buf: [1]Ast.Node.Index = undefined;
    582     const full = ast.fullFnProto(&buf, decl.ast_node).?;
    583     const return_type = full.ast.return_type.unwrap().?;
    584     return switch (ast.nodeTag(return_type)) {
    585         .error_set_decl => return_type.toOptional(),
    586         .error_union => ast.nodeData(return_type).node_and_node[0].toOptional(),
    587         else => .none,
    588     };
    589 }
    590 
    591 export fn decl_file_path(decl_index: Decl.Index) String {
    592     string_result.clearRetainingCapacity();
    593     string_result.appendSlice(gpa, decl_index.get().file.path()) catch @panic("OOM");
    594     return String.init(string_result.items);
    595 }
    596 
    597 export fn decl_category_name(decl_index: Decl.Index) String {
    598     const decl = decl_index.get();
    599     const ast = decl.file.get_ast();
    600     const name = switch (decl.categorize()) {
    601         .namespace, .container => |node| {
    602             if (ast.nodeTag(decl.ast_node) == .root)
    603                 return String.init("struct");
    604             string_result.clearRetainingCapacity();
    605             var buf: [2]Ast.Node.Index = undefined;
    606             const container_decl = ast.fullContainerDecl(&buf, node).?;
    607             if (container_decl.layout_token) |t| {
    608                 if (ast.tokenTag(t) == .keyword_extern) {
    609                     string_result.appendSlice(gpa, "extern ") catch @panic("OOM");
    610                 }
    611             }
    612             const main_token_tag = ast.tokenTag(container_decl.ast.main_token);
    613             string_result.appendSlice(gpa, main_token_tag.lexeme().?) catch @panic("OOM");
    614             return String.init(string_result.items);
    615         },
    616         .global_variable => "Global Variable",
    617         .function => "Function",
    618         .type_function => "Type Function",
    619         .type, .type_type => "Type",
    620         .error_set => "Error Set",
    621         .global_const => "Constant",
    622         .primitive => "Primitive Value",
    623         .alias => "Alias",
    624     };
    625     return String.init(name);
    626 }
    627 
    628 export fn decl_name(decl_index: Decl.Index) String {
    629     const decl = decl_index.get();
    630     string_result.clearRetainingCapacity();
    631     const name = n: {
    632         if (decl.parent == .none) {
    633             // Then it is the root struct of a file.
    634             break :n std.fs.path.stem(decl.file.path());
    635         }
    636         break :n decl.extra_info().name;
    637     };
    638     string_result.appendSlice(gpa, name) catch @panic("OOM");
    639     return String.init(string_result.items);
    640 }
    641 
    642 export fn decl_docs_html(decl_index: Decl.Index, short: bool) String {
    643     const decl = decl_index.get();
    644     string_result.clearRetainingCapacity();
    645     if (decl.extra_info().first_doc_comment.unwrap()) |first_doc_comment| {
    646         render_docs(&string_result, decl_index, first_doc_comment, short) catch @panic("OOM");
    647     }
    648     return String.init(string_result.items);
    649 }
    650 
    651 fn collect_docs(
    652     list: *std.ArrayListUnmanaged(u8),
    653     ast: *const Ast,
    654     first_doc_comment: Ast.TokenIndex,
    655 ) Oom!void {
    656     list.clearRetainingCapacity();
    657     var it = first_doc_comment;
    658     while (true) : (it += 1) switch (ast.tokenTag(it)) {
    659         .doc_comment, .container_doc_comment => {
    660             // It is tempting to trim this string but think carefully about how
    661             // that will affect the markdown parser.
    662             const line = ast.tokenSlice(it)[3..];
    663             try list.appendSlice(gpa, line);
    664         },
    665         else => break,
    666     };
    667 }
    668 
    669 fn render_docs(
    670     out: *std.ArrayListUnmanaged(u8),
    671     decl_index: Decl.Index,
    672     first_doc_comment: Ast.TokenIndex,
    673     short: bool,
    674 ) Oom!void {
    675     const decl = decl_index.get();
    676     const ast = decl.file.get_ast();
    677 
    678     var parser = try markdown.Parser.init(gpa);
    679     defer parser.deinit();
    680     var it = first_doc_comment;
    681     while (true) : (it += 1) switch (ast.tokenTag(it)) {
    682         .doc_comment, .container_doc_comment => {
    683             const line = ast.tokenSlice(it)[3..];
    684             if (short and line.len == 0) break;
    685             try parser.feedLine(line);
    686         },
    687         else => break,
    688     };
    689 
    690     var parsed_doc = try parser.endInput();
    691     defer parsed_doc.deinit(gpa);
    692 
    693     const g = struct {
    694         var link_buffer: std.ArrayListUnmanaged(u8) = .empty;
    695     };
    696 
    697     const Writer = std.ArrayListUnmanaged(u8).Writer;
    698     const Renderer = markdown.Renderer(Writer, Decl.Index);
    699     const renderer: Renderer = .{
    700         .context = decl_index,
    701         .renderFn = struct {
    702             fn render(
    703                 r: Renderer,
    704                 doc: markdown.Document,
    705                 node: markdown.Document.Node.Index,
    706                 writer: Writer,
    707             ) !void {
    708                 const data = doc.nodes.items(.data)[@intFromEnum(node)];
    709                 switch (doc.nodes.items(.tag)[@intFromEnum(node)]) {
    710                     .code_span => {
    711                         try writer.writeAll("<code>");
    712                         const content = doc.string(data.text.content);
    713                         if (resolve_decl_path(r.context, content)) |resolved_decl_index| {
    714                             g.link_buffer.clearRetainingCapacity();
    715                             try resolveDeclLink(resolved_decl_index, &g.link_buffer);
    716 
    717                             try writer.writeAll("<a href=\"#");
    718                             _ = missing_feature_url_escape;
    719                             try writer.writeAll(g.link_buffer.items);
    720                             try writer.print("\">{f}</a>", .{markdown.fmtHtml(content)});
    721                         } else {
    722                             try writer.print("{f}", .{markdown.fmtHtml(content)});
    723                         }
    724 
    725                         try writer.writeAll("</code>");
    726                     },
    727 
    728                     else => try Renderer.renderDefault(r, doc, node, writer),
    729                 }
    730             }
    731         }.render,
    732     };
    733     try renderer.render(parsed_doc, out.writer(gpa));
    734 }
    735 
    736 fn resolve_decl_path(decl_index: Decl.Index, path: []const u8) ?Decl.Index {
    737     var path_components = std.mem.splitScalar(u8, path, '.');
    738     var current_decl_index = decl_index.get().lookup(path_components.first()) orelse return null;
    739     while (path_components.next()) |component| {
    740         switch (current_decl_index.get().categorize()) {
    741             .alias => |aliasee| current_decl_index = aliasee,
    742             else => {},
    743         }
    744         current_decl_index = current_decl_index.get().get_child(component) orelse return null;
    745     }
    746     return current_decl_index;
    747 }
    748 
    749 export fn decl_type_html(decl_index: Decl.Index) String {
    750     const decl = decl_index.get();
    751     const ast = decl.file.get_ast();
    752     string_result.clearRetainingCapacity();
    753     t: {
    754         // If there is an explicit type, use it.
    755         if (ast.fullVarDecl(decl.ast_node)) |var_decl| {
    756             if (var_decl.ast.type_node.unwrap()) |type_node| {
    757                 string_result.appendSlice(gpa, "<code>") catch @panic("OOM");
    758                 fileSourceHtml(decl.file, &string_result, type_node, .{
    759                     .skip_comments = true,
    760                     .collapse_whitespace = true,
    761                 }) catch |e| {
    762                     std.debug.panic("unable to render html: {s}", .{@errorName(e)});
    763                 };
    764                 string_result.appendSlice(gpa, "</code>") catch @panic("OOM");
    765                 break :t;
    766             }
    767         }
    768     }
    769     return String.init(string_result.items);
    770 }
    771 
    772 const Oom = error{OutOfMemory};
    773 
    774 fn unpackInner(tar_bytes: []u8) !void {
    775     var reader: std.Io.Reader = .fixed(tar_bytes);
    776     var file_name_buffer: [1024]u8 = undefined;
    777     var link_name_buffer: [1024]u8 = undefined;
    778     var it: std.tar.Iterator = .init(&reader, .{
    779         .file_name_buffer = &file_name_buffer,
    780         .link_name_buffer = &link_name_buffer,
    781     });
    782     while (try it.next()) |tar_file| {
    783         switch (tar_file.kind) {
    784             .file => {
    785                 if (tar_file.size == 0 and tar_file.name.len == 0) break;
    786                 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
    787                     log.debug("found file: '{s}'", .{tar_file.name});
    788                     const file_name = try gpa.dupe(u8, tar_file.name);
    789                     if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
    790                         const pkg_name = file_name[0..pkg_name_end];
    791                         const gop = try Walk.modules.getOrPut(gpa, pkg_name);
    792                         const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
    793                         if (!gop.found_existing or
    794                             std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
    795                             std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
    796                         {
    797                             gop.value_ptr.* = file;
    798                         }
    799                         const file_bytes = tar_bytes[reader.seek..][0..@intCast(tar_file.size)];
    800                         assert(file == try Walk.add_file(file_name, file_bytes));
    801                     }
    802                 } else {
    803                     log.warn("skipping: '{s}' - the tar creation should have done that", .{
    804                         tar_file.name,
    805                     });
    806                 }
    807             },
    808             else => continue,
    809         }
    810     }
    811 }
    812 
    813 fn ascii_lower(bytes: []u8) void {
    814     for (bytes) |*b| b.* = std.ascii.toLower(b.*);
    815 }
    816 
    817 export fn module_name(index: u32) String {
    818     const names = Walk.modules.keys();
    819     return String.init(if (index >= names.len) "" else names[index]);
    820 }
    821 
    822 export fn find_module_root(pkg: Walk.ModuleIndex) Decl.Index {
    823     const root_file = Walk.modules.values()[@intFromEnum(pkg)];
    824     const result = root_file.findRootDecl();
    825     assert(result != .none);
    826     return result;
    827 }
    828 
    829 /// Set by `set_input_string`.
    830 var input_string: std.ArrayListUnmanaged(u8) = .empty;
    831 
    832 export fn set_input_string(len: usize) [*]u8 {
    833     input_string.resize(gpa, len) catch @panic("OOM");
    834     return input_string.items.ptr;
    835 }
    836 
    837 /// Looks up the root struct decl corresponding to a file by path.
    838 /// Uses `input_string`.
    839 export fn find_file_root() Decl.Index {
    840     const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
    841     return file.findRootDecl();
    842 }
    843 
    844 /// Uses `input_string`.
    845 /// Tries to look up the Decl component-wise but then falls back to a file path
    846 /// based scan.
    847 export fn find_decl() Decl.Index {
    848     const result = Decl.find(input_string.items);
    849     if (result != .none) return result;
    850 
    851     const g = struct {
    852         var match_fqn: std.ArrayListUnmanaged(u8) = .empty;
    853     };
    854     for (Walk.decls.items, 0..) |*decl, decl_index| {
    855         g.match_fqn.clearRetainingCapacity();
    856         decl.fqn(&g.match_fqn) catch @panic("OOM");
    857         if (std.mem.eql(u8, g.match_fqn.items, input_string.items)) {
    858             //const path = @as(Decl.Index, @enumFromInt(decl_index)).get().file.path();
    859             //log.debug("find_decl '{s}' found in {s}", .{ input_string.items, path });
    860             return @enumFromInt(decl_index);
    861         }
    862     }
    863     return .none;
    864 }
    865 
    866 /// Set only by `categorize_decl`; read only by `get_aliasee`, valid only
    867 /// when `categorize_decl` returns `.alias`.
    868 var global_aliasee: Decl.Index = .none;
    869 
    870 export fn get_aliasee() Decl.Index {
    871     return global_aliasee;
    872 }
    873 export fn categorize_decl(decl_index: Decl.Index, resolve_alias_count: usize) Walk.Category.Tag {
    874     global_aliasee = .none;
    875     var chase_alias_n = resolve_alias_count;
    876     var decl = decl_index.get();
    877     while (true) {
    878         const result = decl.categorize();
    879         switch (result) {
    880             .alias => |new_index| {
    881                 assert(new_index != .none);
    882                 global_aliasee = new_index;
    883                 if (chase_alias_n > 0) {
    884                     chase_alias_n -= 1;
    885                     decl = new_index.get();
    886                     continue;
    887                 }
    888             },
    889             else => {},
    890         }
    891         return result;
    892     }
    893 }
    894 
    895 export fn type_fn_members(parent: Decl.Index, include_private: bool) Slice(Decl.Index) {
    896     const decl = parent.get();
    897 
    898     // If the type function returns another type function, get the members of that function
    899     if (decl.get_type_fn_return_type_fn()) |function_decl| {
    900         return namespace_members(function_decl, include_private);
    901     }
    902 
    903     return namespace_members(parent, include_private);
    904 }
    905 
    906 export fn namespace_members(parent: Decl.Index, include_private: bool) Slice(Decl.Index) {
    907     const g = struct {
    908         var members: std.ArrayListUnmanaged(Decl.Index) = .empty;
    909     };
    910 
    911     g.members.clearRetainingCapacity();
    912 
    913     for (Walk.decls.items, 0..) |*decl, i| {
    914         if (decl.parent == parent) {
    915             if (include_private or decl.is_pub()) {
    916                 g.members.append(gpa, @enumFromInt(i)) catch @panic("OOM");
    917             }
    918         }
    919     }
    920 
    921     return Slice(Decl.Index).init(g.members.items);
    922 }
    923 
    924 fn count_scalar(haystack: []const u8, needle: u8) usize {
    925     var total: usize = 0;
    926     for (haystack) |elem| {
    927         if (elem == needle)
    928             total += 1;
    929     }
    930     return total;
    931 }