From 4caea5d62f1a37cf5661afdfbd47458fe6e38b5f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Oct 2026 13:21:04 -0700 Subject: [PATCH 1/4] zig fmt: change --complexity to --stats preparing to add more stats --- src/fmt.zig | 72 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/src/fmt.zig b/src/fmt.zig index 8bb5582cbea0e506e66a6b1c52f381b81222bc2e..d0a06bf53db66ac0a0a873c30a4df63e4b523dc3 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -6,6 +6,7 @@ const process = std.process; const Allocator = std.mem.Allocator; const Color = std.zig.Color; const fatal = std.process.fatal; +const Serializer = std.zon.Serializer; const usage_fmt = \\Usage: zig fmt [file]... @@ -23,7 +24,7 @@ const usage_fmt = \\ --ast-check Run zig ast-check on every file \\ --exclude [file] Exclude file or directory from formatting \\ --zon Treat all input files as ZON, regardless of file extension - \\ --complexity Print a complexity report for each file as well as total + \\ --stats Print aggregated statistics in ZON format to stdout \\ \\ ; @@ -39,10 +40,16 @@ const Fmt = struct { io: Io, out_buffer: std.Io.Writer.Allocating, stdout_writer: *Io.File.Writer, + serializer: Serializer, + root_struct: Serializer.Struct, - complexity: bool, + stats: bool, total_tokens: u64, total_nodes: u64, + /// Count per token tag index + token_stats: []u64, + /// Count per node tag index + node_stats: []u64, const SeenMap = std.AutoHashMap(Io.File.INode, void); }; @@ -53,7 +60,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! var check_flag = false; var check_ast_flag = false; var force_zon = false; - var complexity = false; + var stats = false; var input_files = std.array_list.Managed([]const u8).init(gpa); defer input_files.deinit(); @@ -84,8 +91,8 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! check_flag = true; } else if (mem.eql(u8, arg, "--ast-check")) { check_ast_flag = true; - } else if (mem.eql(u8, arg, "--complexity")) { - complexity = true; + } else if (mem.eql(u8, arg, "--stats")) { + stats = true; } else if (mem.eql(u8, arg, "--exclude")) { if (i + 1 >= args.len) { fatal("expected parameter after --exclude", .{}); @@ -185,13 +192,23 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! .color = color, .out_buffer = .init(gpa), .stdout_writer = &stdout_writer, - .complexity = complexity, + .stats = stats, .total_tokens = 0, .total_nodes = 0, + .serializer = .{ .writer = &stdout_writer.interface }, + .root_struct = undefined, + .token_stats = undefined, + .node_stats = undefined, }; defer fmt.seen.deinit(); defer fmt.out_buffer.deinit(); + if (stats) { + fmt.root_struct = try fmt.serializer.beginStruct(.{}); + fmt.token_stats = try arena.alloc(u64, @typeInfo(std.zig.Token.Tag).@"enum".field_names.len); + fmt.node_stats = try arena.alloc(u64, @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names.len); + } + // Mark any excluded files/directories as already seen, // so that they are skipped later during actual processing for (excluded_files.items) |file_path| { @@ -212,8 +229,10 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path); } - if (complexity) { - std.log.info("total: tokens={d} nodes={d}", .{ fmt.total_tokens, fmt.total_nodes }); + if (stats) { + try fmt.root_struct.field("tokens", fmt.total_tokens, .{}); + try fmt.root_struct.field("nodes", fmt.total_nodes, .{}); + try fmt.root_struct.end(); } try fmt.stdout_writer.flush(); @@ -357,8 +376,13 @@ fn fmtPathFile( } } - if (fmt.complexity) { - std.log.info("{s}: tokens={d} nodes={d}", .{ file_path, tree.tokens.len, tree.nodes.len }); + var zon_struct: Serializer.Struct = undefined; + + if (fmt.stats) { + zon_struct = try fmt.root_struct.beginStructField(file_path, .{}); + try zon_struct.field("tokens", tree.tokens.len, .{}); + try zon_struct.field("nodes", tree.nodes.len, .{}); + fmt.total_tokens += tree.tokens.len; fmt.total_nodes += tree.nodes.len; } @@ -370,20 +394,24 @@ fn fmtPathFile( tree.render(gpa, &fmt.out_buffer.writer, .{}) catch |err| switch (err) { error.WriteFailed, error.OutOfMemory => return error.OutOfMemory, }; - if (mem.eql(u8, fmt.out_buffer.written(), source_code)) - return; + if (!mem.eql(u8, fmt.out_buffer.written(), source_code)) { + if (check_mode) { + fmt.any_error = true; + } else { + var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true }); + defer af.deinit(io); - if (check_mode) { - try fmt.stdout_writer.interface.print("{s}\n", .{file_path}); - fmt.any_error = true; - } else { - var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true }); - defer af.deinit(io); - - try af.file.writeStreamingAll(io, fmt.out_buffer.written()); - try af.replace(io); - try fmt.stdout_writer.interface.print("{s}\n", .{file_path}); + try af.file.writeStreamingAll(io, fmt.out_buffer.written()); + try af.replace(io); + } + if (fmt.stats) { + try zon_struct.field("dirty", true, .{}); + } else { + try fmt.stdout_writer.interface.print("{s}\n", .{file_path}); + } } + + if (fmt.stats) try zon_struct.end(); } /// Provided for debugging/testing purposes; unused by the compiler. -- 2.54.0 From 2f2224c19a5328de4cdc962192e763f6301bfd72 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Oct 2026 13:55:11 -0700 Subject: [PATCH 2/4] zig fmt: individual node and token stats per file --- src/fmt.zig | 131 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 30 deletions(-) diff --git a/src/fmt.zig b/src/fmt.zig index d0a06bf53db66ac0a0a873c30a4df63e4b523dc3..00562de0182bc56896a4d20713cb4eb3fd2be21d 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -40,18 +40,22 @@ const Fmt = struct { io: Io, out_buffer: std.Io.Writer.Allocating, stdout_writer: *Io.File.Writer, + stats: ?*Stats, + + const SeenMap = std.AutoHashMap(Io.File.INode, void); +}; + +const Stats = struct { serializer: Serializer, root_struct: Serializer.Struct, - stats: bool, - total_tokens: u64, - total_nodes: u64, + files: u32, + tokens: u64, + nodes: u64, /// Count per token tag index token_stats: []u64, /// Count per node tag index node_stats: []u64, - - const SeenMap = std.AutoHashMap(Io.File.INode, void); }; pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void { @@ -60,7 +64,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! var check_flag = false; var check_ast_flag = false; var force_zon = false; - var stats = false; + var enable_stats = false; var input_files = std.array_list.Managed([]const u8).init(gpa); defer input_files.deinit(); @@ -92,7 +96,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! } else if (mem.eql(u8, arg, "--ast-check")) { check_ast_flag = true; } else if (mem.eql(u8, arg, "--stats")) { - stats = true; + enable_stats = true; } else if (mem.eql(u8, arg, "--exclude")) { if (i + 1 >= args.len) { fatal("expected parameter after --exclude", .{}); @@ -192,21 +196,25 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! .color = color, .out_buffer = .init(gpa), .stdout_writer = &stdout_writer, - .stats = stats, - .total_tokens = 0, - .total_nodes = 0, - .serializer = .{ .writer = &stdout_writer.interface }, - .root_struct = undefined, - .token_stats = undefined, - .node_stats = undefined, + .stats = null, }; defer fmt.seen.deinit(); defer fmt.out_buffer.deinit(); - if (stats) { - fmt.root_struct = try fmt.serializer.beginStruct(.{}); - fmt.token_stats = try arena.alloc(u64, @typeInfo(std.zig.Token.Tag).@"enum".field_names.len); - fmt.node_stats = try arena.alloc(u64, @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names.len); + if (enable_stats) { + const stats = try arena.create(Stats); + fmt.stats = stats; + stats.* = .{ + .files = 0, + .tokens = 0, + .nodes = 0, + .serializer = .{ .writer = &stdout_writer.interface }, + .root_struct = try stats.serializer.beginStruct(.{}), + .token_stats = try arena.alloc(u64, @typeInfo(std.zig.Token.Tag).@"enum".field_names.len), + .node_stats = try arena.alloc(u64, @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names.len), + }; + @memset(stats.token_stats, 0); + @memset(stats.node_stats, 0); } // Mark any excluded files/directories as already seen, @@ -229,10 +237,13 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path); } - if (stats) { - try fmt.root_struct.field("tokens", fmt.total_tokens, .{}); - try fmt.root_struct.field("nodes", fmt.total_nodes, .{}); - try fmt.root_struct.end(); + if (fmt.stats) |stats| { + if (stats.files > 1) { + try stats.root_struct.field("files", stats.files, .{}); + try stats.root_struct.field("tokens", stats.tokens, .{}); + try stats.root_struct.field("nodes", stats.nodes, .{}); + } + try stats.root_struct.end(); } try fmt.stdout_writer.flush(); @@ -378,13 +389,73 @@ fn fmtPathFile( var zon_struct: Serializer.Struct = undefined; - if (fmt.stats) { - zon_struct = try fmt.root_struct.beginStructField(file_path, .{}); - try zon_struct.field("tokens", tree.tokens.len, .{}); - try zon_struct.field("nodes", tree.nodes.len, .{}); + if (fmt.stats) |stats| { + zon_struct = try stats.root_struct.beginStructField(file_path, .{}); - fmt.total_tokens += tree.tokens.len; - fmt.total_nodes += tree.nodes.len; + try zon_struct.field("token_count", tree.tokens.len, .{}); + try zon_struct.field("node_count", tree.nodes.len, .{}); + + const token_names = @typeInfo(std.zig.Token.Tag).@"enum".field_names; + const node_names = @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names; + + var token_stats: [token_names.len]u64 = @splat(0); + var token_order: [token_names.len]usize = undefined; + var node_stats: [node_names.len]u64 = @splat(0); + var node_order: [node_names.len]usize = undefined; + + for (tree.tokens.items(.tag)) |tag| { + stats.token_stats[@backingInt(tag)] += 1; + token_stats[@backingInt(tag)] += 1; + } + + for (tree.nodes.items(.tag)) |tag| { + stats.node_stats[@backingInt(tag)] += 1; + node_stats[@backingInt(tag)] += 1; + } + + const SortContext = struct { + stats: []const u64, + order: []usize, + + pub fn lessThan(this: @This(), a_index: usize, b_index: usize) bool { + return this.stats[this.order[b_index]] < this.stats[this.order[a_index]]; + } + + pub fn swap(this: @This(), a_index: usize, b_index: usize) void { + std.mem.swap(usize, &this.order[a_index], &this.order[b_index]); + } + }; + + { + for (&token_order, 0..) |*elem, i| elem.* = i; + std.mem.sortContext(0, token_names.len, @as(SortContext, .{ + .stats = &token_stats, + .order = &token_order, + })); + var s = try zon_struct.beginStructField("tokens", .{}); + for (&token_order) |i| { + const n = token_stats[i]; + if (n != 0) try zon_struct.field(token_names[i], n, .{}); + } + try s.end(); + } + + { + for (&node_order, 0..) |*elem, i| elem.* = i; + std.mem.sortContext(0, node_names.len, @as(SortContext, .{ + .stats = &node_stats, + .order = &node_order, + })); + var s = try zon_struct.beginStructField("nodes", .{}); + for (&node_order) |i| { + const n = node_stats[i]; + if (n != 0) try zon_struct.field(node_names[i], n, .{}); + } + try s.end(); + } + + stats.tokens += tree.tokens.len; + stats.nodes += tree.nodes.len; } // As a heuristic, we make enough capacity for the same as the input source. @@ -404,14 +475,14 @@ fn fmtPathFile( try af.file.writeStreamingAll(io, fmt.out_buffer.written()); try af.replace(io); } - if (fmt.stats) { + if (fmt.stats != null) { try zon_struct.field("dirty", true, .{}); } else { try fmt.stdout_writer.interface.print("{s}\n", .{file_path}); } } - if (fmt.stats) try zon_struct.end(); + if (fmt.stats != null) try zon_struct.end(); } /// Provided for debugging/testing purposes; unused by the compiler. -- 2.54.0 From e51bf822dd0a79bc946f956f0fcb7a7f9abde389 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Oct 2026 14:02:57 -0700 Subject: [PATCH 3/4] zig fmt: add aggregate stats --- src/fmt.zig | 73 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/src/fmt.zig b/src/fmt.zig index 00562de0182bc56896a4d20713cb4eb3fd2be21d..2f892dffc0ad733e2d34f26ef6121fef1f1058bd 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -239,11 +239,47 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! if (fmt.stats) |stats| { if (stats.files > 1) { - try stats.root_struct.field("files", stats.files, .{}); - try stats.root_struct.field("tokens", stats.tokens, .{}); - try stats.root_struct.field("nodes", stats.nodes, .{}); + var agg_struct = try stats.root_struct.beginStructField("aggregate", .{}); + try agg_struct.field("file_count", stats.files, .{}); + try agg_struct.field("token_count", stats.tokens, .{}); + try agg_struct.field("node_count", stats.nodes, .{}); + + const token_names = @typeInfo(std.zig.Token.Tag).@"enum".field_names; + const node_names = @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names; + + var token_order: [token_names.len]usize = undefined; + var node_order: [node_names.len]usize = undefined; + { + for (&token_order, 0..) |*elem, i| elem.* = i; + std.mem.sortContext(0, token_names.len, @as(SortContext, .{ + .stats = stats.token_stats, + .order = &token_order, + })); + var tokens_struct = try agg_struct.beginStructField("tokens", .{}); + for (&token_order) |i| { + const n = stats.token_stats[i]; + if (n != 0) try tokens_struct.field(token_names[i], n, .{}); + } + try tokens_struct.end(); + } + { + for (&node_order, 0..) |*elem, i| elem.* = i; + std.mem.sortContext(0, node_names.len, @as(SortContext, .{ + .stats = stats.node_stats, + .order = &node_order, + })); + var nodes_struct = try agg_struct.beginStructField("nodes", .{}); + for (&node_order) |i| { + const n = stats.node_stats[i]; + if (n != 0) try nodes_struct.field(node_names[i], n, .{}); + } + try nodes_struct.end(); + } + + try agg_struct.end(); } try stats.root_struct.end(); + try fmt.stdout_writer.interface.writeByte('\n'); } try fmt.stdout_writer.flush(); @@ -413,19 +449,6 @@ fn fmtPathFile( node_stats[@backingInt(tag)] += 1; } - const SortContext = struct { - stats: []const u64, - order: []usize, - - pub fn lessThan(this: @This(), a_index: usize, b_index: usize) bool { - return this.stats[this.order[b_index]] < this.stats[this.order[a_index]]; - } - - pub fn swap(this: @This(), a_index: usize, b_index: usize) void { - std.mem.swap(usize, &this.order[a_index], &this.order[b_index]); - } - }; - { for (&token_order, 0..) |*elem, i| elem.* = i; std.mem.sortContext(0, token_names.len, @as(SortContext, .{ @@ -435,7 +458,7 @@ fn fmtPathFile( var s = try zon_struct.beginStructField("tokens", .{}); for (&token_order) |i| { const n = token_stats[i]; - if (n != 0) try zon_struct.field(token_names[i], n, .{}); + if (n != 0) try s.field(token_names[i], n, .{}); } try s.end(); } @@ -449,13 +472,14 @@ fn fmtPathFile( var s = try zon_struct.beginStructField("nodes", .{}); for (&node_order) |i| { const n = node_stats[i]; - if (n != 0) try zon_struct.field(node_names[i], n, .{}); + if (n != 0) try s.field(node_names[i], n, .{}); } try s.end(); } stats.tokens += tree.tokens.len; stats.nodes += tree.nodes.len; + stats.files += 1; } // As a heuristic, we make enough capacity for the same as the input source. @@ -485,6 +509,19 @@ fn fmtPathFile( if (fmt.stats != null) try zon_struct.end(); } +const SortContext = struct { + stats: []const u64, + order: []usize, + + pub fn lessThan(this: @This(), a_index: usize, b_index: usize) bool { + return this.stats[this.order[b_index]] < this.stats[this.order[a_index]]; + } + + pub fn swap(this: @This(), a_index: usize, b_index: usize) void { + std.mem.swap(usize, &this.order[a_index], &this.order[b_index]); + } +}; + /// Provided for debugging/testing purposes; unused by the compiler. pub fn main(init: process.Init) !void { const args = try init.minimal.args.toSlice(init.arena.allocator()); -- 2.54.0 From cd00ebe2a7913927687336a80febaffa6beeb246 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Oct 2026 14:16:36 -0700 Subject: [PATCH 4/4] zig fmt --stats: add source size --- src/fmt.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fmt.zig b/src/fmt.zig index 2f892dffc0ad733e2d34f26ef6121fef1f1058bd..c12eadd95b5a3f25b57e0d497da887dcd7437616 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -52,6 +52,7 @@ const Stats = struct { files: u32, tokens: u64, nodes: u64, + source_size: u64, /// Count per token tag index token_stats: []u64, /// Count per node tag index @@ -208,6 +209,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! .files = 0, .tokens = 0, .nodes = 0, + .source_size = 0, .serializer = .{ .writer = &stdout_writer.interface }, .root_struct = try stats.serializer.beginStruct(.{}), .token_stats = try arena.alloc(u64, @typeInfo(std.zig.Token.Tag).@"enum".field_names.len), @@ -243,6 +245,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! try agg_struct.field("file_count", stats.files, .{}); try agg_struct.field("token_count", stats.tokens, .{}); try agg_struct.field("node_count", stats.nodes, .{}); + try agg_struct.field("source_size", stats.source_size, .{}); const token_names = @typeInfo(std.zig.Token.Tag).@"enum".field_names; const node_names = @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names; @@ -430,6 +433,7 @@ fn fmtPathFile( try zon_struct.field("token_count", tree.tokens.len, .{}); try zon_struct.field("node_count", tree.nodes.len, .{}); + try zon_struct.field("source_size", stat.size, .{}); const token_names = @typeInfo(std.zig.Token.Tag).@"enum".field_names; const node_names = @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names; @@ -480,6 +484,7 @@ fn fmtPathFile( stats.tokens += tree.tokens.len; stats.nodes += tree.nodes.len; stats.files += 1; + stats.source_size += stat.size; } // As a heuristic, we make enough capacity for the same as the input source. -- 2.54.0