authorgravatar for goon.pri.low@gmail.comKendall Condon <goon.pri.low@gmail.com> 2026-06-28 15:54:11-04:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-10-09 10:13:40+02:00
logd9690017246ac82410da742109f84072b7b0c553
tree2a178075695573126c6dcb4e25f6d0361aa83adf
parent18d9b2850ecba8fca14b11bd18111c17f0415aa9

zig fmt: optimize array inits

Rewrites `renderArrayInit` to avoid rendering expressions twice in the common case of there being one line per expression. Also reduces memory usage with the use of a scratch list for array inits and only storing the widths of expressions in the current section. A lot of logic has also been deduplicated. A few bugs were also fixed.

2 files changed, 393 insertions(+), 109 deletions(-)

lib/std/zig/Ast/Render.zig+270-109
......@@ -14,6 +14,8 @@ gpa: Allocator,
1414ais: *AutoIndentingStream,
1515tree: Ast,
1616fixups: Fixups,
17checking_newlines: bool,
18scratch_array_init: std.ArrayList(usize),
1719
1820const indent_delta = 4;
1921const asm_indent_delta = 2;
......@@ -92,7 +94,10 @@ pub fn renderTree(gpa: Allocator, w: *Writer, tree: Ast, fixups: Fixups) Error!v
9294 .ais = &auto_indenting_stream,
9395 .tree = tree,
9496 .fixups = fixups,
97 .checking_newlines = false,
98 .scratch_array_init = .empty,
9599 };
100 defer r.scratch_array_init.deinit(gpa);
96101
97102 // Render all the line comments at the beginning of the file.
98103 const comment_end_loc = tree.tokenStart(0);
......@@ -983,6 +988,8 @@ fn rendersMultiline(r: *const Render, node: Ast.Node.Index) error{OutOfMemory}!b
983988 .ais = &sub_ais,
984989 .tree = r.tree,
985990 .fixups = r.fixups,
991 .checking_newlines = true,
992 .scratch_array_init = undefined, // unused since `checking_newlines` is `true`
986993 };
987994
988995 renderExpression(&sub_r, node, .none) catch |err| return switch (err) {
......@@ -2101,6 +2108,21 @@ fn renderStructInit(
21012108 return renderToken(r, rbrace, space);
21022109}
21032110
2111fn renderCommentsBetweenTokens(r: *Render, start: Ast.TokenIndex, end: Ast.TokenIndex) Error!void {
2112 const tree = r.tree;
2113
2114 var next_start = tree.tokenStart(start);
2115 for (start + 1..end) |token_i| {
2116 const token_start = next_start;
2117 next_start = tree.tokenStart(@intCast(token_i + 1));
2118 _ = try renderComments(
2119 r,
2120 token_start + tokenSliceForRender(tree, @intCast(token_i)).len,
2121 next_start,
2122 );
2123 }
2124}
2125
21042126fn renderArrayInit(
21052127 r: *Render,
21062128 array_init: Ast.full.ArrayInit,
......@@ -2157,137 +2179,276 @@ fn renderArrayInit(
21572179 }
21582180 }
21592181 return renderToken(r, last_elem_token + 1, space); // rbrace
2182 } else if (r.checking_newlines) {
2183 // To avoid taking quadratic time while rendering nested arrays, just write a newline
2184 // since it is already gauraunteed. However, comments still need to be written since they
2185 // may enable/disable zig fmt.
2186 try r.renderCommentsBetweenTokens(array_init.ast.lbrace, rbrace);
2187 return r.ais.insertNewline();
21602188 }
21612189
21622190 try ais.pushIndent(.normal);
21632191 try renderToken(r, array_init.ast.lbrace, .newline);
21642192 try ais.pushSpace(.comma);
21652193
2166 const expr_widths = try gpa.alloc(enum(usize) {
2167 /// The expression contains non-printable characters (e.g. unicode / newlines)
2168 /// or has formatting disabled at the start or end.
2169 nonprint = std.math.maxInt(usize),
2170 _,
2171 }, array_init.ast.elements.len);
2172 defer gpa.free(expr_widths);
2173 {
2174 var buf: Writer.Allocating = .init(gpa);
2175 defer buf.deinit();
2176 var sub_ais: AutoIndentingStream = .init(gpa, &buf.writer, indent_delta);
2177 sub_ais.disabled_offset = ais.disabled_offset;
2178 defer sub_ais.deinit();
2179 var sub_r: Render = .{
2180 .gpa = r.gpa,
2181 .ais = &sub_ais,
2182 .tree = r.tree,
2183 .fixups = r.fixups,
2184 };
2185 for (array_init.ast.elements, expr_widths) |e, *width| {
2186 const begin_disabled = sub_ais.disabled_offset != null;
2187 // `.skip` space so trailing commments aren't included
2188 try renderExpressionComma(&sub_r, e, .skip);
2189 if (!begin_disabled and sub_ais.disabled_offset == null) {
2190 const w = buf.written();
2191 width.* = for (w) |c| {
2192 if (!std.ascii.isPrint(c))
2193 break .nonprint;
2194 } else @fromBackingInt(@intCast(w.len - @intFromBool(w[w.len - 1] == ',')));
2195 } else {
2196 width.* = .nonprint;
2197 }
2194 const scratch = &r.scratch_array_init;
2195 const scratch_start = scratch.items.len;
2196 defer assert(scratch.items.len == scratch_start);
21982197
2199 // Write trailing comments since they may enable/disable zig fmt
2200 buf.clearRetainingCapacity();
2201 var after_expr = tree.lastToken(e);
2202 after_expr += @intFromBool(tree.tokenTag(after_expr + 1) == .comma);
2203 try renderSpace(&sub_r, after_expr, tokenSliceForRender(tree, after_expr).len, .none);
2204
2205 buf.clearRetainingCapacity();
2206 // The following are needed to make sure isLineOverIndented is not influenced by
2207 // the previous element.
2208 sub_ais.indent_count = 0;
2209 sub_ais.applied_indent = 0;
2210 }
2211 }
2198 var sub_buf: Writer.Allocating = .init(gpa);
2199 defer sub_buf.deinit();
2200 var sub_ais: AutoIndentingStream = .init(gpa, &sub_buf.writer, indent_delta);
2201 defer sub_ais.deinit();
2202 var sub_r: Render = .{
2203 .gpa = r.gpa,
2204 .ais = &sub_ais,
2205 .tree = r.tree,
2206 .fixups = r.fixups,
2207 .checking_newlines = true,
2208 .scratch_array_init = undefined, // unused since `checking_newlines` is `true`
2209 };
22122210
22132211 var remaining_exprs = array_init.ast.elements;
2214 var remaining_widths = expr_widths;
2215 while (remaining_exprs.len != 0) {
2216 var row_size: usize = 1;
2217 for (1.., remaining_exprs, remaining_widths) |len, e, w| {
2218 if (w == .nonprint) break;
2219 row_size = len;
2220
2221 var after_expr = tree.lastToken(e);
2222 after_expr += @intFromBool(tree.tokenTag(after_expr + 1) == .comma);
2223 assert(tree.tokenTag(after_expr) == .comma or after_expr + 1 == rbrace);
2224 if (!tree.tokensOnSameLine(after_expr, after_expr + 1))
2225 break;
2226 } else {
2227 // All the expressions are on the same line.
2228 // However, if there is a trailing comma, we put them each on their own line.
2229 if (tree.tokenTag(rbrace - 1) == .comma)
2230 row_size = 1;
2231 }
2232
2233 // Determine the size of this section
2234 const section_end = end: {
2235 var line_start = row_size; // Start after the first row to ignore comments on it
2236 break :end for (line_start.., remaining_exprs[line_start..]) |i, e| {
2237 const expr_first = tree.firstToken(e);
2238 // Any nonprint character terminates the line because they are always put on their
2239 // own line, so they will not end up on the same line as the trailing comment.
2240 if (expr_widths[i - 1] == .nonprint or !tree.tokensOnSameLine(expr_first - 1, expr_first)) {
2241 line_start = i;
2212 section: while (remaining_exprs.len != 0) {
2213 defer scratch.items.len = scratch_start;
2214 // May not already be equal due to a section end and when `sub_ais` is initialized.
2215 sub_ais.disabled_offset = ais.disabled_offset;
2216
2217 var n_section_exprs: usize = 0;
2218 var n_cols: usize = 0;
2219 var line_col: usize = 0;
2220 var first_line: bool = true;
2221 // `scratch` will contain the width of the first `n_section_exprs`;
2222 // zero indicates the expression is on its own line.
2223 while (true) {
2224 assert(scratch.items[scratch_start..].len == n_section_exprs);
2225
2226 const expr = remaining_exprs[n_section_exprs];
2227 const final_expr = n_section_exprs + 1 == remaining_exprs.len;
2228 var elem_last = tree.lastToken(expr);
2229 elem_last += @intFromBool(tree.tokenTag(elem_last + 1) == .comma);
2230 assert(tree.tokenTag(elem_last) == .comma or elem_last + 1 == rbrace);
2231
2232 const own_line = ends_line: {
2233 const ends_line = final_expr or !tree.tokensOnSameLine(elem_last, elem_last + 1);
2234 if (ends_line and n_cols == 0) {
2235 // Avoid doing extra work below since the width will not be required.
2236 break :ends_line false;
2237 } // Otherwise a correct value for `own_line` is required.
2238 // This is the case even for `n_cols == 1` since it may become `2` below.
2239
2240 if (sub_ais.disabled_offset != null) {
2241 if (n_cols >= 2) {
2242 // Comments still need rendered since they may enable/disable zig fmt
2243 // for later expressions.
2244 try renderCommentsBetweenTokens(&sub_r, tree.firstToken(expr), elem_last + 1);
2245 sub_buf.clearRetainingCapacity();
2246 }
2247 break :ends_line true;
2248 }
2249 // `.skip` space so trailing commments aren't included
2250 try renderExpressionComma(&sub_r, expr, .skip);
2251 var own_line = sub_ais.disabled_offset != null;
2252 for (sub_buf.written()) |c| {
2253 if (!std.ascii.isPrint(c)) {
2254 // The expression contained a newline or unicode character so it is
2255 // put on its own line since there is no reasonable way to align it.
2256 own_line = true;
2257 }
2258 }
2259 const width = sub_buf.written().len -
2260 @intFromBool(sub_buf.written()[sub_buf.written().len - 1] == ',');
2261 sub_buf.clearRetainingCapacity();
2262
2263 // Write trailing comments since they may enable/disable zig fmt.
2264 _ = try renderComments(
2265 &sub_r,
2266 tree.tokenStart(elem_last) + tokenSliceForRender(tree, elem_last).len,
2267 tree.tokenStart(elem_last + 1),
2268 );
2269 sub_buf.clearRetainingCapacity();
2270
2271 // Make sure isLineOverIndented is not influenced for future elements.
2272 sub_ais.indent_count = 0;
2273 sub_ais.applied_indent = 0;
2274
2275 if (own_line) {
2276 break :ends_line true;
22422277 }
22432278
2244 var after_expr = tree.lastToken(e);
2245 after_expr += @intFromBool(tree.tokenTag(after_expr + 1) == .comma);
2246 assert(tree.tokenTag(after_expr) == .comma or after_expr + 1 == rbrace);
2247 if (hasTrailingComment(tree, after_expr))
2248 break line_start;
2249 } else remaining_exprs.len;
2250 };
2251 const section_exprs = remaining_exprs[0..section_end];
2252 const section_widths = remaining_widths[0..section_end];
2253 remaining_exprs = remaining_exprs[section_end..];
2254 remaining_widths = remaining_widths[section_end..];
2279 // This is a valid element to be put on a line with others.
2280 assert(width != 0);
22552281
2256 // Determine the width of each column
2257 var col_widths = try gpa.alloc(usize, row_size);
2258 defer gpa.free(col_widths);
2259 @memset(col_widths, 0);
2282 try scratch.append(r.gpa, width);
2283 line_col += 1;
2284 n_cols += @intFromBool(first_line);
2285 n_section_exprs += 1;
2286
2287 if (ends_line) {
2288 break :ends_line false;
2289 }
22602290
2261 var col: usize = 0;
2262 for (section_widths) |w| {
2263 if (w == .nonprint) {
2264 col = 0;
22652291 continue;
2292 };
2293
2294 if (n_cols <= 1 and first_line) {
2295 // Since there is only one column, there is no need to render each expression to
2296 // find its width. However, the section ending still needs to be accounted for, so
2297 // below we find the number of expressions that can be rendered before the section
2298 // might end.
2299 n_section_exprs = 1;
2300 n_cols = 1;
2301 line_col = 0;
2302 scratch.items.len = scratch_start;
2303
2304 while (n_section_exprs != remaining_exprs.len) {
2305 const s_expr = remaining_exprs[n_section_exprs];
2306 var s_elem_last = tree.lastToken(s_expr);
2307 s_elem_last += @intFromBool(tree.tokenTag(s_elem_last + 1) == .comma);
2308 assert(tree.tokenTag(s_elem_last) == .comma or s_elem_last + 1 == rbrace);
2309
2310 // `line_col` here does not account for expressions being put on their own line.
2311 if (tree.tokensOnSameLine(s_elem_last, s_elem_last + 1)) {
2312 line_col += 1;
2313 } else if (hasTrailingComment(tree, s_elem_last)) {
2314 n_section_exprs -= line_col;
2315 break;
2316 } else {
2317 line_col = 0;
2318 }
2319 n_section_exprs += 1;
2320 } else {
2321 line_col = 0;
2322 }
2323
2324 for (remaining_exprs[0..n_section_exprs]) |s_expr| {
2325 if (s_expr != array_init.ast.elements[0]) {
2326 try renderExtraNewline(r, s_expr);
2327 }
2328 try renderExpression(r, s_expr, .comma);
2329 }
2330 remaining_exprs = remaining_exprs[n_section_exprs..];
2331
2332 if (line_col != 0) {
2333 assert(remaining_exprs.len != 0);
2334
2335 // Since the final expressions may be forced on their own line, and so still
2336 // part of this section, the final row is handled by the slower path.
2337 sub_ais.disabled_offset = ais.disabled_offset;
2338 n_section_exprs = 0;
2339 line_col = 0;
2340 first_line = false;
2341 continue;
2342 }
2343
2344 continue :section;
22662345 }
2267 col_widths[col] = @max(col_widths[col], @backingInt(w));
2268 col += 1;
2269 if (col == row_size) {
2270 col = 0;
2346
2347 first_line &= !own_line or n_section_exprs == 0;
2348
2349 if (!first_line and hasTrailingComment(tree, elem_last)) {
2350 // End of the section
2351 if (own_line) {
2352 line_col = 0;
2353 }
2354 n_section_exprs -= line_col;
2355 scratch.items.len -= line_col;
2356
2357 break;
22712358 }
2359
2360 if (own_line) {
2361 // The expression has no width, so it is not yet present in `scratch`.
2362 try scratch.append(r.gpa, 0);
2363 n_section_exprs += 1;
2364 }
2365
2366 if (final_expr) {
2367 break;
2368 }
2369
2370 line_col = 0;
2371 first_line = false;
22722372 }
2373 line_col = undefined;
22732374
2274 // Render each expression
2275 col = 0;
2276 for (0.., section_exprs, section_widths) |i, e, w| {
2277 if (i + 1 == section_end or col + 1 == row_size or
2278 w == .nonprint or section_widths[i + 1] == .nonprint)
2279 {
2280 try renderExpression(r, e, .comma);
2281 col = 0;
2282 if (i + 1 != section_end) {
2283 try renderExtraNewline(r, section_exprs[i + 1]);
2375 const section_exprs = remaining_exprs[0..n_section_exprs];
2376 remaining_exprs = remaining_exprs[n_section_exprs..];
2377
2378 if (n_cols == 1 or
2379 // If all the expressions are on the same line with a
2380 // trailing comma, they are each put on their own line.
2381 (n_cols == array_init.ast.elements.len and
2382 tree.tokenTag(rbrace - 1) == .comma and tree.tokensOnSameLine(rbrace - 1, rbrace)))
2383 {
2384 scratch.items.len = scratch_start;
2385 for (section_exprs) |expr| {
2386 if (expr != array_init.ast.elements[0]) {
2387 try renderExtraNewline(r, expr);
2388 } else {
2389 // Can only be the case if `n_cols` became `1` at the end (one line with a
2390 // trailing comma) since otherwise the first rows were already rendered.
2391 assert(n_section_exprs == array_init.ast.elements.len);
22842392 }
2285 } else {
2286 try renderExpression(r, e, .comma_space);
2287 try ais.splatByteAll(' ', col_widths[col] - @backingInt(w));
2393 try renderExpression(r, expr, .comma);
2394 }
2395 continue;
2396 }
2397
2398 {
2399 assert(scratch.items[scratch_start..].len == n_section_exprs);
2400 // Determine the width of each column. This cannot be done with the width calculations
2401 // above since the section may have ended, removing elements already iterated over.
2402 //
2403 // The first `n_section_exprs` of `scratch` will contain zero at each end-of-line.
2404 // The remaining `n_cols - 1` will contain the widths of each column.
2405 const col_widths = try scratch.addManyAsSlice(gpa, n_cols - 1);
2406
2407 @memcpy(col_widths, scratch.items[scratch_start..][0 .. n_cols - 1]);
2408 scratch.items[scratch_start + n_cols - 1] = 0;
2409
2410 var col: usize = 0;
2411 for (
2412 scratch_start + n_cols..,
2413 scratch.items[scratch_start..][n_cols..n_section_exprs],
2414 ) |i, width| {
2415 if (width == 0) {
2416 scratch.items[i - 1] = 0;
2417 col = 0;
2418 } else if (col + 1 == n_cols) {
2419 scratch.items[i] = 0;
2420 col = 0;
2421 } else {
2422 col_widths[col] = @max(col_widths[col], width);
2423 col += 1;
2424 }
2425 }
2426
2427 scratch.items[scratch_start + n_section_exprs - 1] = 0;
2428 }
2429
2430 var col: usize = 0;
2431 for (0.., section_exprs) |i, expr| {
2432 assert(col < n_cols);
2433
2434 if (col == 0 and expr != array_init.ast.elements[0]) {
2435 try renderExtraNewline(r, expr);
2436 }
2437
2438 const width = scratch.items[scratch_start + i];
2439 if (width != 0) {
2440 try renderExpression(r, expr, .comma_space);
2441
2442 const col_width = scratch.items[scratch_start + n_section_exprs + col];
2443 try ais.splatByteAll(' ', col_width - width);
2444
22882445 col += 1;
2446 } else {
2447 try renderExpression(r, expr, .comma);
2448 col = 0;
22892449 }
22902450 }
2451 assert(col == 0);
22912452 }
22922453
22932454 ais.popSpace();
lib/std/zig/parser_test.zig+123
......@@ -1937,6 +1937,12 @@ test "zig fmt: array literal with hint" {
19371937 \\ 5, 6, //
19381938 \\ 7, 8, //
19391939 \\};
1940 \\const a = .{
1941 \\ 1,
1942 \\ 2, 3, '±', //
1943 \\ 4, 5,
1944 \\ 6, 7, //
1945 \\};
19401946 ,
19411947 \\const a = []u8{
19421948 \\ 1, 2, //
......@@ -1973,6 +1979,15 @@ test "zig fmt: array literal with hint" {
19731979 \\ 5, 6, //
19741980 \\ 7, 8, //
19751981 \\};
1982 \\const a = .{
1983 \\ 1,
1984 \\ 2,
1985 \\ 3,
1986 \\ '±', //
1987 \\ 4,
1988 \\ 5,
1989 \\ 6, 7, //
1990 \\};
19761991 \\
19771992 );
19781993}
......@@ -5006,6 +5021,10 @@ test "zig fmt: single argument trailing commas in @builtins()" {
50065021test "zig fmt: trailing comma should force multiline 1 column" {
50075022 try testTransform(
50085023 \\pub const UUID_NULL: uuid_t = [16]u8{0,0,0,0,};
5024 \\pub const UUID_NULL: uuid_t = [16]u8{
5025 \\0,0,0,0, };
5026 \\pub const UUID_NULL: uuid_t = [16]u8{0,0,0,0,
5027 \\};
50095028 \\
50105029 ,
50115030 \\pub const UUID_NULL: uuid_t = [16]u8{
......@@ -5014,6 +5033,15 @@ test "zig fmt: trailing comma should force multiline 1 column" {
50145033 \\ 0,
50155034 \\ 0,
50165035 \\};
5036 \\pub const UUID_NULL: uuid_t = [16]u8{
5037 \\ 0,
5038 \\ 0,
5039 \\ 0,
5040 \\ 0,
5041 \\};
5042 \\pub const UUID_NULL: uuid_t = [16]u8{
5043 \\ 0, 0, 0, 0,
5044 \\};
50175045 \\
50185046 );
50195047}
......@@ -7051,6 +7079,101 @@ test "zig fmt: canonicalize @enumFromInt(x) to @fromBackingInt(@intCast(x))" {
70517079 );
70527080}
70537081
7082test "zig fmt: linear time for nested array inits" {
7083 const source = comptime source: {
7084 var source: []const u8 = "const a = .{\n";
7085 var indent: []const u8 = " ";
7086
7087 for (0..32) |_| {
7088 source = source ++ indent ++ ".{},\n" ++ indent ++ ".{\n";
7089 indent = indent ++ " ";
7090 }
7091 source = source ++ indent ++ ".{},\n";
7092 for (0..32) |_| {
7093 indent = indent[4..];
7094 source = source ++ indent ++ "},\n";
7095 }
7096
7097 break :source source ++ "};\n";
7098 };
7099 try testCanonical(source);
7100}
7101
7102test "zig fmt: on/off with nested array inits" {
7103 try testCanonical(
7104 \\const a = .{
7105 \\ .{
7106 \\ 0, // zig fmt: off
7107 \\ },
7108 \\ 1, 2, // zig fmt: on
7109 \\ 3,
7110 \\ 4,
7111 \\};
7112 \\const b = .{
7113 \\ // zig fmt: off
7114 \\ .{
7115 \\ 0, // zig fmt: on
7116 \\ },
7117 \\ 1, 2, //
7118 \\ 3, 4,
7119 \\};
7120 \\const c = .{
7121 \\ .{
7122 \\ // zig fmt: off
7123 \\ 0, // zig fmt: on
7124 \\ },
7125 \\ 1, 2, //
7126 \\ 3, 4,
7127 \\};
7128 \\
7129 );
7130}
7131
7132test "zig fmt: empty lines in array inits" {
7133 try testTransform(
7134 \\const a = .{
7135 \\ 1,
7136 \\
7137 \\ 2, //
7138 \\ 3,
7139 \\
7140 \\ 4, 5, //
7141 \\ 6,
7142 \\
7143 \\ 7,
7144 \\
7145 \\ 8,
7146 \\};
7147 \\
7148 ,
7149 \\const a = .{
7150 \\ 1,
7151 \\
7152 \\ 2, //
7153 \\ 3,
7154 \\
7155 \\ 4, 5, //
7156 \\ 6, 7,
7157 \\
7158 \\ 8,
7159 \\};
7160 \\
7161 );
7162}
7163
7164test "zig fmt: nested array init with asm" {
7165 try testCanonical(
7166 \\const a = .{
7167 \\ asm (.{
7168 \\ 0,
7169 \\ }
7170 \\ : [_] "" (_),
7171 \\ ),
7172 \\};
7173 \\
7174 );
7175}
7176
70547177test "recovery: top level" {
70557178 try testError(
70567179 \\test "" {inline}