| ... | @@ -0,0 +1,555 @@ |
| 1 | const std = @import("std"); |
| 2 | const Autodoc = @This(); |
| 3 | const Compilation = @import("Compilation.zig"); |
| 4 | const Module = @import("Module.zig"); |
| 5 | const Zir = @import("Zir.zig"); |
| 6 | |
| 7 | module: *Module, |
| 8 | doc_location: ?Compilation.EmitLoc, |
| 9 | |
| 10 | pub fn init(m: *Module, dl: ?Compilation.EmitLoc) Autodoc { |
| 11 | return .{ |
| 12 | .doc_location = dl, |
| 13 | .module = m, |
| 14 | }; |
| 15 | } |
| 16 | |
| 17 | pub fn generateZirData(self: Autodoc) !void { |
| 18 | const gpa = self.module.gpa; |
| 19 | std.debug.print("yay, you called me!\n", .{}); |
| 20 | if (self.doc_location) |loc| { |
| 21 | if (loc.directory) |dir| { |
| 22 | if (dir.path) |path| { |
| 23 | std.debug.print("path: {s}\n", .{path}); |
| 24 | } |
| 25 | } |
| 26 | std.debug.print("basename: {s}\n", .{loc.basename}); |
| 27 | } |
| 28 | |
| 29 | // const root_file_path = self.module.main_pkg.root_src_path; |
| 30 | const root_file_path = "/home/kristoff/test/test.zig"; |
| 31 | const zir = self.module.import_table.get(root_file_path).?.zir; |
| 32 | |
| 33 | var types = std.ArrayList(DocData.Type).init(gpa); |
| 34 | var decls = std.ArrayList(DocData.Decl).init(gpa); |
| 35 | var ast_nodes = std.ArrayList(DocData.AstNode).init(gpa); |
| 36 | |
| 37 | // var decl_map = std.AutoHashMap(Zir.Inst.Index, usize); // values are positions in the `decls` array |
| 38 | |
| 39 | try types.append(.{ |
| 40 | .kind = 0, |
| 41 | .name = "type", |
| 42 | }); |
| 43 | |
| 44 | var root_scope: Scope = .{ .parent = null }; |
| 45 | try ast_nodes.append(.{ .name = "(root)" }); |
| 46 | const main_type_index = try walkInstruction(zir, gpa, &root_scope, &types, &decls, &ast_nodes, Zir.main_struct_inst); |
| 47 | |
| 48 | var data = DocData{ |
| 49 | .files = &[1][]const u8{root_file_path}, |
| 50 | .types = types.items, |
| 51 | .decls = decls.items, |
| 52 | .astNodes = ast_nodes.items, |
| 53 | }; |
| 54 | |
| 55 | data.packages[0].main = main_type_index.type; |
| 56 | |
| 57 | const out = std.io.getStdOut().writer(); |
| 58 | out.print("zigAnalysis=", .{}) catch unreachable; |
| 59 | std.json.stringify( |
| 60 | data, |
| 61 | .{ |
| 62 | .whitespace = .{}, |
| 63 | .emit_null_optional_fields = false, |
| 64 | }, |
| 65 | out, |
| 66 | ) catch unreachable; |
| 67 | out.print(";", .{}) catch unreachable; |
| 68 | } |
| 69 | |
| 70 | const Scope = struct { |
| 71 | parent: ?*Scope, |
| 72 | map: std.AutoHashMapUnmanaged(u32, usize) = .{}, // index into `decls` |
| 73 | |
| 74 | /// Assumes all decls in present scope and upper scopes have already |
| 75 | /// been either fully resolved or at least reserved. |
| 76 | pub fn resolveDeclName(self: Scope, string_table_idx: u32) usize { |
| 77 | var cur: ?*const Scope = &self; |
| 78 | return while (cur) |s| : (cur = s.parent) { |
| 79 | break s.map.get(string_table_idx) orelse continue; |
| 80 | } else unreachable; |
| 81 | } |
| 82 | |
| 83 | pub fn insertDeclRef( |
| 84 | self: *Scope, |
| 85 | gpa: std.mem.Allocator, |
| 86 | decl_name_index: u32, // decl name |
| 87 | decls_slot_index: usize, |
| 88 | ) !void { |
| 89 | try self.map.put(gpa, decl_name_index, decls_slot_index); |
| 90 | } |
| 91 | }; |
| 92 | |
| 93 | const DocData = struct { |
| 94 | typeKinds: []const []const u8 = std.meta.fieldNames(std.builtin.TypeId), |
| 95 | rootPkg: u32 = 0, |
| 96 | params: struct { |
| 97 | zigId: []const u8 = "arst", |
| 98 | zigVersion: []const u8 = "arst", |
| 99 | target: []const u8 = "arst", |
| 100 | rootName: []const u8 = "arst", |
| 101 | builds: []const struct { target: []const u8 } = &.{ |
| 102 | .{ .target = "arst" }, |
| 103 | }, |
| 104 | } = .{}, |
| 105 | packages: [1]Package = .{.{}}, |
| 106 | fns: []struct {} = &.{}, |
| 107 | errors: []struct {} = &.{}, |
| 108 | calls: []struct {} = &.{}, |
| 109 | |
| 110 | // non-hardcoded stuff |
| 111 | astNodes: []AstNode, |
| 112 | files: []const []const u8, |
| 113 | types: []Type, |
| 114 | decls: []Decl, |
| 115 | |
| 116 | const Package = struct { |
| 117 | name: []const u8 = "root", |
| 118 | file: usize = 0, // index into files |
| 119 | main: usize = 0, // index into decls |
| 120 | table: struct { root: usize } = .{ |
| 121 | .root = 0, |
| 122 | }, |
| 123 | }; |
| 124 | |
| 125 | const Decl = struct { |
| 126 | name: []const u8, |
| 127 | kind: []const u8, // TODO: where do we find this info? |
| 128 | src: usize, // index into astNodes |
| 129 | type: usize, // index into types |
| 130 | value: usize, |
| 131 | }; |
| 132 | |
| 133 | const AstNode = struct { |
| 134 | file: usize = 0, // index into files |
| 135 | line: usize = 0, |
| 136 | col: usize = 0, |
| 137 | name: ?[]const u8 = null, |
| 138 | docs: ?[]const u8 = null, |
| 139 | fields: ?[]usize = null, // index into astNodes |
| 140 | }; |
| 141 | |
| 142 | const Type = struct { |
| 143 | kind: u32, // index into typeKinds |
| 144 | name: []const u8, |
| 145 | src: ?usize = null, // index into astNodes |
| 146 | privDecls: ?[]usize = null, // index into decls |
| 147 | pubDecls: ?[]usize = null, // index into decls |
| 148 | fields: ?[]WalkResult = null, // (use src->fields to find names) |
| 149 | }; |
| 150 | |
| 151 | const WalkResult = union(enum) { |
| 152 | failure: bool, |
| 153 | type: usize, // index in `types` |
| 154 | decl_ref: usize, // index in `decls` |
| 155 | |
| 156 | pub fn jsonStringify( |
| 157 | self: WalkResult, |
| 158 | _: std.json.StringifyOptions, |
| 159 | w: anytype, |
| 160 | ) !void { |
| 161 | switch (self) { |
| 162 | .failure => |v| { |
| 163 | try w.print( |
| 164 | \\{{ "failure":{} }} |
| 165 | , .{v}); |
| 166 | }, |
| 167 | .type, .decl_ref => |v| { |
| 168 | try w.print( |
| 169 | \\{{ "{s}":{} }} |
| 170 | , .{ @tagName(self), v }); |
| 171 | }, |
| 172 | |
| 173 | // .decl_ref => |v| { |
| 174 | // try w.print( |
| 175 | // \\{{ "{s}":"{s}" }} |
| 176 | // , .{ @tagName(self), v }); |
| 177 | // }, |
| 178 | } |
| 179 | } |
| 180 | }; |
| 181 | }; |
| 182 | |
| 183 | fn walkInstruction( |
| 184 | zir: Zir, |
| 185 | gpa: std.mem.Allocator, |
| 186 | parent_scope: *Scope, |
| 187 | types: *std.ArrayList(DocData.Type), |
| 188 | decls: *std.ArrayList(DocData.Decl), |
| 189 | ast_nodes: *std.ArrayList(DocData.AstNode), |
| 190 | inst_index: usize, |
| 191 | ) error{OutOfMemory}!DocData.WalkResult { |
| 192 | const tags = zir.instructions.items(.tag); |
| 193 | const data = zir.instructions.items(.data); |
| 194 | |
| 195 | // We assume that the topmost ast_node entry corresponds to our decl |
| 196 | const self_ast_node_index = ast_nodes.items.len - 1; |
| 197 | |
| 198 | switch (tags[inst_index]) { |
| 199 | else => { |
| 200 | std.debug.print( |
| 201 | "TODO: implement `walkInstruction` for {s}\n\n", |
| 202 | .{@tagName(tags[inst_index])}, |
| 203 | ); |
| 204 | return DocData.WalkResult{ .failure = true }; |
| 205 | }, |
| 206 | .decl_val => { |
| 207 | const str_tok = data[inst_index].str_tok; |
| 208 | const decls_slot_index = parent_scope.resolveDeclName(str_tok.start); |
| 209 | return DocData.WalkResult{ .decl_ref = decls_slot_index }; |
| 210 | }, |
| 211 | .int_type => { |
| 212 | const int_type = data[inst_index].int_type; |
| 213 | const sign = if (int_type.signedness == .unsigned) "u" else "i"; |
| 214 | const bits = int_type.bit_count; |
| 215 | const name = try std.fmt.allocPrint(gpa, "{s}{}", .{ sign, bits }); |
| 216 | |
| 217 | try types.append(.{ |
| 218 | .kind = @enumToInt(std.builtin.TypeId.Int), |
| 219 | .name = name, |
| 220 | }); |
| 221 | return DocData.WalkResult{ .type = types.items.len - 1 }; |
| 222 | }, |
| 223 | .block_inline => { |
| 224 | const pl_node = data[inst_index].pl_node; |
| 225 | const body_len = zir.extra[pl_node.payload_index]; |
| 226 | |
| 227 | std.debug.print("body len: {}\n", .{body_len}); |
| 228 | |
| 229 | const result_index = inst_index + body_len - 1; |
| 230 | return walkInstruction(zir, gpa, parent_scope, types, decls, ast_nodes, result_index); |
| 231 | }, |
| 232 | .extended => { |
| 233 | const extended = data[inst_index].extended; |
| 234 | switch (extended.opcode) { |
| 235 | else => { |
| 236 | std.debug.print( |
| 237 | "TODO: implement `walkInstruction` (inside .extended case) for {s}\n\n", |
| 238 | .{@tagName(extended.opcode)}, |
| 239 | ); |
| 240 | return DocData.WalkResult{ .failure = true }; |
| 241 | }, |
| 242 | .struct_decl => { |
| 243 | var scope: Scope = .{ .parent = parent_scope }; |
| 244 | |
| 245 | const small = @bitCast(Zir.Inst.StructDecl.Small, extended.small); |
| 246 | var extra_index: usize = extended.operand; |
| 247 | |
| 248 | const src_node: ?i32 = if (small.has_src_node) blk: { |
| 249 | const src_node = @bitCast(i32, zir.extra[extra_index]); |
| 250 | extra_index += 1; |
| 251 | break :blk src_node; |
| 252 | } else null; |
| 253 | _ = src_node; |
| 254 | |
| 255 | const body_len = if (small.has_body_len) blk: { |
| 256 | const body_len = zir.extra[extra_index]; |
| 257 | extra_index += 1; |
| 258 | break :blk body_len; |
| 259 | } else 0; |
| 260 | |
| 261 | const fields_len = if (small.has_fields_len) blk: { |
| 262 | const fields_len = zir.extra[extra_index]; |
| 263 | extra_index += 1; |
| 264 | break :blk fields_len; |
| 265 | } else 0; |
| 266 | _ = fields_len; |
| 267 | |
| 268 | const decls_len = if (small.has_decls_len) blk: { |
| 269 | const decls_len = zir.extra[extra_index]; |
| 270 | extra_index += 1; |
| 271 | break :blk decls_len; |
| 272 | } else 0; |
| 273 | |
| 274 | var decl_indexes = std.ArrayList(usize).init(gpa); |
| 275 | var priv_decl_indexes = std.ArrayList(usize).init(gpa); |
| 276 | |
| 277 | const decls_first_index = decls.items.len; |
| 278 | // Decl name lookahead for reserving slots in `scope` (and `decls`). |
| 279 | // Done to make sure that all decl refs can be resolved correctly, |
| 280 | // even if we haven't fully analyzed the decl yet. |
| 281 | { |
| 282 | var it = zir.declIterator(@intCast(u32, inst_index)); |
| 283 | try decls.resize(decls_first_index + it.decls_len); |
| 284 | var decls_slot_index = decls_first_index; |
| 285 | while (it.next()) |d| : (decls_slot_index += 1) { |
| 286 | const decl_name_index = zir.extra[d.sub_index + 5]; |
| 287 | try scope.insertDeclRef(gpa, decl_name_index, decls_slot_index); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | extra_index = try walkDecls( |
| 292 | zir, |
| 293 | gpa, |
| 294 | &scope, |
| 295 | decls, |
| 296 | decls_first_index, |
| 297 | decls_len, |
| 298 | &decl_indexes, |
| 299 | &priv_decl_indexes, |
| 300 | types, |
| 301 | ast_nodes, |
| 302 | extra_index, |
| 303 | ); |
| 304 | |
| 305 | // const body = zir.extra[extra_index..][0..body_len]; |
| 306 | extra_index += body_len; |
| 307 | |
| 308 | var field_type_indexes = std.ArrayList(DocData.WalkResult).init(gpa); |
| 309 | var field_name_indexes = std.ArrayList(usize).init(gpa); |
| 310 | try collectFieldInfo( |
| 311 | zir, |
| 312 | gpa, |
| 313 | &scope, |
| 314 | types, |
| 315 | decls, |
| 316 | fields_len, |
| 317 | &field_type_indexes, |
| 318 | &field_name_indexes, |
| 319 | ast_nodes, |
| 320 | extra_index, |
| 321 | ); |
| 322 | |
| 323 | ast_nodes.items[self_ast_node_index].fields = field_name_indexes.items; |
| 324 | |
| 325 | try types.append(.{ |
| 326 | .kind = @enumToInt(std.builtin.TypeId.Struct), |
| 327 | .name = "todo_name", |
| 328 | .src = self_ast_node_index, |
| 329 | .privDecls = priv_decl_indexes.items, |
| 330 | .pubDecls = decl_indexes.items, |
| 331 | .fields = field_type_indexes.items, |
| 332 | }); |
| 333 | |
| 334 | return DocData.WalkResult{ .type = types.items.len - 1 }; |
| 335 | }, |
| 336 | } |
| 337 | }, |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | fn walkDecls( |
| 342 | zir: Zir, |
| 343 | gpa: std.mem.Allocator, |
| 344 | scope: *Scope, |
| 345 | decls: *std.ArrayList(DocData.Decl), |
| 346 | decls_first_index: usize, |
| 347 | decls_len: u32, |
| 348 | decl_indexes: *std.ArrayList(usize), |
| 349 | priv_decl_indexes: *std.ArrayList(usize), |
| 350 | types: *std.ArrayList(DocData.Type), |
| 351 | ast_nodes: *std.ArrayList(DocData.AstNode), |
| 352 | extra_start: usize, |
| 353 | ) error{OutOfMemory}!usize { |
| 354 | const bit_bags_count = std.math.divCeil(usize, decls_len, 8) catch unreachable; |
| 355 | var extra_index = extra_start + bit_bags_count; |
| 356 | var bit_bag_index: usize = extra_start; |
| 357 | var cur_bit_bag: u32 = undefined; |
| 358 | var decl_i: u32 = 0; |
| 359 | |
| 360 | while (decl_i < decls_len) : (decl_i += 1) { |
| 361 | const decls_slot_index = decls_first_index + decl_i; |
| 362 | |
| 363 | if (decl_i % 8 == 0) { |
| 364 | cur_bit_bag = zir.extra[bit_bag_index]; |
| 365 | bit_bag_index += 1; |
| 366 | } |
| 367 | const is_pub = @truncate(u1, cur_bit_bag) != 0; |
| 368 | cur_bit_bag >>= 1; |
| 369 | const is_exported = @truncate(u1, cur_bit_bag) != 0; |
| 370 | cur_bit_bag >>= 1; |
| 371 | // const has_align = @truncate(u1, cur_bit_bag) != 0; |
| 372 | cur_bit_bag >>= 1; |
| 373 | // const has_section_or_addrspace = @truncate(u1, cur_bit_bag) != 0; |
| 374 | cur_bit_bag >>= 1; |
| 375 | |
| 376 | // const sub_index = extra_index; |
| 377 | |
| 378 | // const hash_u32s = zir.extra[extra_index..][0..4]; |
| 379 | extra_index += 4; |
| 380 | // const line = zir.extra[extra_index]; |
| 381 | extra_index += 1; |
| 382 | const decl_name_index = zir.extra[extra_index]; |
| 383 | extra_index += 1; |
| 384 | const decl_index = zir.extra[extra_index]; |
| 385 | extra_index += 1; |
| 386 | const doc_comment_index = zir.extra[extra_index]; |
| 387 | extra_index += 1; |
| 388 | |
| 389 | // const align_inst: Zir.Inst.Ref = if (!has_align) .none else inst: { |
| 390 | // const inst = @intToEnum(Zir.Inst.Ref, zir.extra[extra_index]); |
| 391 | // extra_index += 1; |
| 392 | // break :inst inst; |
| 393 | // }; |
| 394 | // const section_inst: Zir.Inst.Ref = if (!has_section_or_addrspace) .none else inst: { |
| 395 | // const inst = @intToEnum(Zir.Inst.Ref, zir.extra[extra_index]); |
| 396 | // extra_index += 1; |
| 397 | // break :inst inst; |
| 398 | // }; |
| 399 | // const addrspace_inst: Zir.Inst.Ref = if (!has_section_or_addrspace) .none else inst: { |
| 400 | // const inst = @intToEnum(Zir.Inst.Ref, zir.extra[extra_index]); |
| 401 | // extra_index += 1; |
| 402 | // break :inst inst; |
| 403 | // }; |
| 404 | |
| 405 | // const pub_str = if (is_pub) "pub " else ""; |
| 406 | // const hash_bytes = @bitCast([16]u8, hash_u32s.*); |
| 407 | |
| 408 | const name: []const u8 = blk: { |
| 409 | if (decl_name_index == 0) { |
| 410 | break :blk if (is_exported) "usingnamespace" else "comptime"; |
| 411 | } else if (decl_name_index == 1) { |
| 412 | break :blk "test"; |
| 413 | } else { |
| 414 | const raw_decl_name = zir.nullTerminatedString(decl_name_index); |
| 415 | if (raw_decl_name.len == 0) { |
| 416 | break :blk zir.nullTerminatedString(decl_name_index + 1); |
| 417 | } else { |
| 418 | break :blk raw_decl_name; |
| 419 | } |
| 420 | } |
| 421 | }; |
| 422 | |
| 423 | const doc_comment: ?[]const u8 = if (doc_comment_index != 0) |
| 424 | zir.nullTerminatedString(doc_comment_index) |
| 425 | else |
| 426 | null; |
| 427 | |
| 428 | // astnode |
| 429 | const ast_node_index = idx: { |
| 430 | const idx = ast_nodes.items.len; |
| 431 | try ast_nodes.append(.{ |
| 432 | .file = 0, |
| 433 | .line = 0, |
| 434 | .col = 0, |
| 435 | .docs = doc_comment, |
| 436 | .fields = null, // walkInstruction will fill `fields` if necessary |
| 437 | }); |
| 438 | break :idx idx; |
| 439 | }; |
| 440 | |
| 441 | const walk_result = try walkInstruction(zir, gpa, scope, types, decls, ast_nodes, decl_index); |
| 442 | const type_index = walk_result.type; |
| 443 | |
| 444 | if (is_pub) { |
| 445 | try decl_indexes.append(decls_slot_index); |
| 446 | } else { |
| 447 | try priv_decl_indexes.append(decls_slot_index); |
| 448 | } |
| 449 | |
| 450 | decls.items[decls_slot_index] = .{ |
| 451 | .name = name, |
| 452 | .src = ast_node_index, |
| 453 | .type = 0, |
| 454 | .value = type_index, |
| 455 | .kind = "const", // find where this information can be found |
| 456 | }; |
| 457 | } |
| 458 | |
| 459 | return extra_index; |
| 460 | } |
| 461 | |
| 462 | fn collectFieldInfo( |
| 463 | zir: Zir, |
| 464 | gpa: std.mem.Allocator, |
| 465 | scope: *Scope, |
| 466 | types: *std.ArrayList(DocData.Type), |
| 467 | decls: *std.ArrayList(DocData.Decl), |
| 468 | fields_len: usize, |
| 469 | field_type_indexes: *std.ArrayList(DocData.WalkResult), |
| 470 | field_name_indexes: *std.ArrayList(usize), |
| 471 | ast_nodes: *std.ArrayList(DocData.AstNode), |
| 472 | ei: usize, |
| 473 | ) !void { |
| 474 | if (fields_len == 0) return; |
| 475 | var extra_index = ei; |
| 476 | |
| 477 | const bits_per_field = 4; |
| 478 | const fields_per_u32 = 32 / bits_per_field; |
| 479 | const bit_bags_count = std.math.divCeil(usize, fields_len, fields_per_u32) catch unreachable; |
| 480 | var bit_bag_index: usize = extra_index; |
| 481 | extra_index += bit_bags_count; |
| 482 | |
| 483 | var cur_bit_bag: u32 = undefined; |
| 484 | var field_i: u32 = 0; |
| 485 | while (field_i < fields_len) : (field_i += 1) { |
| 486 | if (field_i % fields_per_u32 == 0) { |
| 487 | cur_bit_bag = zir.extra[bit_bag_index]; |
| 488 | bit_bag_index += 1; |
| 489 | } |
| 490 | // const has_align = @truncate(u1, cur_bit_bag) != 0; |
| 491 | cur_bit_bag >>= 1; |
| 492 | // const has_default = @truncate(u1, cur_bit_bag) != 0; |
| 493 | cur_bit_bag >>= 1; |
| 494 | // const is_comptime = @truncate(u1, cur_bit_bag) != 0; |
| 495 | cur_bit_bag >>= 1; |
| 496 | const unused = @truncate(u1, cur_bit_bag) != 0; |
| 497 | cur_bit_bag >>= 1; |
| 498 | _ = unused; |
| 499 | |
| 500 | const field_name = zir.nullTerminatedString(zir.extra[extra_index]); |
| 501 | extra_index += 1; |
| 502 | const field_type = @intToEnum(Zir.Inst.Ref, zir.extra[extra_index]); |
| 503 | extra_index += 1; |
| 504 | const doc_comment_index = zir.extra[extra_index]; |
| 505 | extra_index += 1; |
| 506 | |
| 507 | // type |
| 508 | { |
| 509 | switch (field_type) { |
| 510 | .void_type => { |
| 511 | try field_type_indexes.append(.{ .type = types.items.len }); |
| 512 | try types.append(.{ |
| 513 | .kind = @enumToInt(std.builtin.TypeId.Void), |
| 514 | .name = "void", |
| 515 | }); |
| 516 | }, |
| 517 | .usize_type => { |
| 518 | try field_type_indexes.append(.{ .type = types.items.len }); |
| 519 | try types.append(.{ |
| 520 | .kind = @enumToInt(std.builtin.TypeId.Int), |
| 521 | .name = "usize", |
| 522 | }); |
| 523 | }, |
| 524 | |
| 525 | else => { |
| 526 | const enum_value = @enumToInt(field_type); |
| 527 | if (enum_value < Zir.Inst.Ref.typed_value_map.len) { |
| 528 | std.debug.print( |
| 529 | "TODO: handle ref type: {s}", |
| 530 | .{@tagName(field_type)}, |
| 531 | ); |
| 532 | try field_type_indexes.append(DocData.WalkResult{ .failure = true }); |
| 533 | } else { |
| 534 | const zir_index = enum_value - Zir.Inst.Ref.typed_value_map.len; |
| 535 | const walk_result = try walkInstruction(zir, gpa, scope, types, decls, ast_nodes, zir_index); |
| 536 | try field_type_indexes.append(walk_result); |
| 537 | } |
| 538 | }, |
| 539 | } |
| 540 | } |
| 541 | |
| 542 | // ast node |
| 543 | { |
| 544 | try field_name_indexes.append(ast_nodes.items.len); |
| 545 | const doc_comment: ?[]const u8 = if (doc_comment_index != 0) |
| 546 | zir.nullTerminatedString(doc_comment_index) |
| 547 | else |
| 548 | null; |
| 549 | try ast_nodes.append(.{ |
| 550 | .name = field_name, |
| 551 | .docs = doc_comment, |
| 552 | }); |
| 553 | } |
| 554 | } |
| 555 | } |