Something went wrong. Try again.
A small configuration language for Zig: ZON with let, if and templates over a context, evaluated to a struct, always finishing.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Lays a program out the way `zig fmt` lays out Zig.//!//! The rules are Zig's, so that a struthio file sitting beside Zig looks like//! it belongs there://!//! * Four spaces of indent, single spaces around binary operators and after//! commas, and none inside parentheses or brackets.//! * A struct or list literal, a call's arguments and a switch's prongs go on//! one line -- `.{ 1, 2 }` -- unless the last of them is followed by a//! comma. Then each goes on a line of its own, followed by a comma. The//! trailing comma is how a person asks for the long form, as it is in Zig.//! So is a comment inside, or anything inside that cannot be on one line.//! A switch prong's cases follow the same rule.//! * A line break written after a binary operator is kept, and what follows//! it is indented once more. One written anywhere else is not.//! * A multiline string starts on a line of its own, one indent in from the//! line it belongs to, and what follows it starts on the next.//! * Comments stay where they were: at the end of the line they were on, or//! on a line of their own. A run of blank lines becomes one, and a blank//! line just inside a bracket goes.//!//! Strings, numbers and names are copied as they are written, and so is//! everything inside a `${ }`. Formatting changes only the space between//! tokens and the trailing commas, and never what a program means.//!//! It parses with a parser of its own rather than `Parser.zig`'s, because//! that one resolves names, reads included files and keeps no record of the//! layout. This one keeps every token, looks nothing up, and accepts any//! function name, so a file can be formatted without the host that runs it.//! It accepts what that one accepts, in the same shapes; a little more, too,//! since it checks no names, arities or duplicates.
const std = @import("std");const Allocator = std.mem.Allocator;
const Diagnostic = @import("Diagnostic.zig");const Tokenizer = @import("Tokenizer.zig");const Token = Tokenizer.Token;const Tag = Tokenizer.Tag;
pub const Error = error{ OutOfMemory, /// The source does not parse; the diagnostic says where. Syntax,};
pub const Options = struct { /// How deeply expressions may nest, as `Limits.max_depth`. max_depth: u32 = 64,};
/// `source`, formatted. The caller owns the result.pub fn format(gpa: Allocator, source: []const u8, options: Options, diag: *Diagnostic) Error![]u8 { var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator();
if (source.len >= std.math.maxInt(u32)) { diag.set(.{}, "program too large", .{}); return error.Syntax; } var p: Parser = .{ .arena = arena, .source = source, .toks = try tokenize(arena, source, diag), .diag = diag, .max_depth = options.max_depth, }; const file = try p.parseFile();
var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); var r: Renderer = .{ .gpa = gpa, .source = source, .toks = p.toks, .out = &out }; try r.file(file); return out.toOwnedSlice(gpa);}
fn tokenize(arena: Allocator, source: []const u8, diag: *Diagnostic) Error![]const Token { var toks: std.ArrayList(Token) = .empty; var t: Tokenizer = .init(source); while (true) { const tok = t.next(); if (tok.tag == .invalid) return invalidToken(source, tok, diag); try toks.append(arena, tok); if (tok.tag == .eof) return toks.items; }}
fn invalidToken(source: []const u8, tok: Token, diag: *Diagnostic) error{Syntax} { const c = source[tok.start]; const pos: Diagnostic.Pos = .of(source, tok.start); switch (c) { '"' => diag.set(pos, "unterminated string", .{}), '\'' => diag.set(pos, "unterminated character literal", .{}), '@' => diag.set(pos, "expected a builtin name or a quoted identifier after '@'", .{}), else => if (std.ascii.isPrint(c)) diag.set(pos, "unexpected character '{c}'", .{c}) else diag.set(pos, "unexpected byte 0x{x:0>2}", .{c}), } return error.Syntax;}
// -- the tree -----------------------------------------------------------------
/// A piece of the program, by the tokens it spans. Most of its punctuation/// is not recorded, because the grammar says exactly where it is: an `if`'s/// `(` is the token after its first, and its `)` the token after its/// condition's last.const Node = struct { first: u32, last: u32, data: Data, /// The `${ }`s inside a string or a multiline string, in order. interpolations: []const Interpolation = &.{},};
/// What is inside one `${ }`. Its tokens are a list of their own, which the/// renderer never sees: it copies the string they are in as it is written.const Interpolation = struct { toks: []const Token, node: *const Node,};
const Data = union(enum) { /// One token: a number, a character, a string, a name, `true`, `false` /// or `null`. token, /// One or more `\\` lines, which are one string. multiline, /// `.name` enum_literal, /// `(inner)` paren: *const Node, /// `!x` or `-x`, the operator being `first`. unary: *const Node, binary: struct { lhs: *const Node, op: u32, rhs: *const Node }, /// `object.name`, the name being `last`. field: *const Node, /// `object.?` unwrap: *const Node, /// `object[index]` index: struct { object: *const Node, index: *const Node }, /// `@name(args)`: `(` is the token after the name, and `)` is `last`. call: []const *const Node, /// `.{ items }`, an item being an expression or a `field_init`. literal: []const *const Node, /// `.name = value` field_init: *const Node, @"if": struct { cond: *const Node, /// The name in `|name|`. capture: ?u32, then: *const Node, @"else": ?*const Node, }, /// `switch (subject) { prongs }` @"switch": struct { subject: *const Node, prongs: []const *const Node }, /// `cases => |capture| value`, or `else => value` with no cases. prong: struct { cases: []const *const Node, arrow: u32, capture: ?u32, value: *const Node }, /// `lo...hi` range: struct { lo: *const Node, hi: *const Node },};
const File = struct { lets: []const Let, root: *const Node, eof: u32,};
/// `let name = value;`: `let` is `value.first - 3`.const Let = struct { value: *const Node };
// -- parsing ------------------------------------------------------------------
/// The same descent as `Parser.zig`, level for level, recording tokens/// instead of meaning.const Parser = struct { arena: Allocator, source: []const u8, toks: []const Token, i: u32 = 0, diag: *Diagnostic, depth: u32 = 0, max_depth: u32,
fn tag(p: *const Parser) Tag { return p.toks[p.i].tag; }
fn fail(p: *Parser, at: u32, comptime fmt: []const u8, args: anytype) error{Syntax} { p.diag.set(.of(p.source, p.toks[at].start), fmt, args); return error.Syntax; }
fn unexpected(p: *Parser, comptime wanted: []const u8) error{Syntax} { return p.fail(p.i, "expected " ++ wanted ++ ", found {s}", .{p.tag().describe()}); }
fn advance(p: *Parser) void { if (p.tag() != .eof) p.i += 1; }
fn expect(p: *Parser, t: Tag) Error!u32 { if (p.tag() != t) return p.fail(p.i, "expected {s}, found {s}", .{ t.describe(), p.tag().describe() }); const at = p.i; p.advance(); return at; }
fn expectName(p: *Parser) Error!u32 { return switch (p.tag()) { .identifier, .quoted_identifier => blk: { const at = p.i; p.advance(); break :blk at; }, else => p.unexpected("an identifier"), }; }
/// A node from `first` to the token just consumed. fn node(p: *Parser, first: u32, data: Data) Error!*Node { const n = try p.arena.create(Node); n.* = .{ .first = first, .last = p.i - 1, .data = data }; return n; }
fn enter(p: *Parser) error{Syntax}!void { p.depth += 1; if (p.depth > p.max_depth) return p.fail(p.i, "expression nested too deeply", .{}); }
fn leave(p: *Parser) void { p.depth -= 1; }
fn parseFile(p: *Parser) Error!File { var lets: std.ArrayList(Let) = .empty; while (p.tag() == .keyword_let) { p.advance(); _ = try p.expectName(); _ = try p.expect(.equal); const value = try p.parseExpr(); _ = try p.expect(.semicolon); try lets.append(p.arena, .{ .value = value }); } const root = try p.parseExpr(); if (p.tag() != .eof) return p.unexpected("an operator or the end of the program"); return .{ .lets = lets.items, .root = root, .eof = p.i }; }
fn parseExpr(p: *Parser) Error!*const Node { try p.enter(); defer p.leave(); return p.parseOr(); }
fn binary(p: *Parser, lhs: *const Node, comptime next: fn (*Parser) Error!*const Node) Error!*const Node { const op = p.i; p.advance(); const rhs = try next(p); return p.node(lhs.first, .{ .binary = .{ .lhs = lhs, .op = op, .rhs = rhs } }); }
fn parseOr(p: *Parser) Error!*const Node { var lhs = try p.parseAnd(); while (p.tag() == .keyword_or) lhs = try p.binary(lhs, parseAnd); return lhs; }
fn parseAnd(p: *Parser) Error!*const Node { var lhs = try p.parseCompare(); while (p.tag() == .keyword_and) lhs = try p.binary(lhs, parseCompare); return lhs; }
fn isCompare(t: Tag) bool { return switch (t) { .equal_equal, .bang_equal, .angle_bracket_left, .angle_bracket_left_equal, .angle_bracket_right, .angle_bracket_right_equal, => true, else => false, }; }
fn parseCompare(p: *Parser) Error!*const Node { const lhs = try p.parseOrelse(); if (!isCompare(p.tag())) return lhs; const n = try p.binary(lhs, parseOrelse); if (isCompare(p.tag())) return p.fail(p.i, "comparisons cannot be chained", .{}); return n; }
fn parseOrelse(p: *Parser) Error!*const Node { var lhs = try p.parseAdditive(); while (p.tag() == .keyword_orelse) lhs = try p.binary(lhs, parseAdditive); return lhs; }
fn parseAdditive(p: *Parser) Error!*const Node { var lhs = try p.parseMultiplicative(); while (p.tag() == .plus or p.tag() == .minus or p.tag() == .plus_plus) lhs = try p.binary(lhs, parseMultiplicative); return lhs; }
fn parseMultiplicative(p: *Parser) Error!*const Node { var lhs = try p.parseUnary(); while (p.tag() == .asterisk or p.tag() == .slash or p.tag() == .percent) lhs = try p.binary(lhs, parseUnary); return lhs; }
/// `-5.?` is `(-5).?` to the real parser, which reads a negative number /// as one literal, and `-(x.?)` otherwise. Either is the same tokens in /// the same order, which is all that matters here. fn parseUnary(p: *Parser) Error!*const Node { if (p.tag() != .bang and p.tag() != .minus) return p.parsePostfix(); try p.enter(); defer p.leave(); const first = p.i; p.advance(); const operand = try p.parseUnary(); return p.node(first, .{ .unary = operand }); }
fn parsePostfix(p: *Parser) Error!*const Node { var n = try p.parsePrimary(); while (true) { switch (p.tag()) { .period => { p.advance(); _ = try p.expectName(); n = try p.node(n.first, .{ .field = n }); }, .period_question => { p.advance(); n = try p.node(n.first, .{ .unwrap = n }); }, .l_bracket => { p.advance(); const index = try p.parseExpr(); _ = try p.expect(.r_bracket); n = try p.node(n.first, .{ .index = .{ .object = n, .index = index } }); }, else => return n, } } }
fn parsePrimary(p: *Parser) Error!*const Node { const first = p.i; switch (p.tag()) { .number, .char_literal, .keyword_true, .keyword_false, .keyword_null, .identifier, .quoted_identifier, => { p.advance(); return p.node(first, .token); }, .string => { p.advance(); const n = try p.node(first, .token); const t = p.toks[first]; n.interpolations = try p.interpolations(t.start + 1, t.end - 1, true); return n; }, .multiline_string_line => { var found: std.ArrayList(Interpolation) = .empty; while (p.tag() == .multiline_string_line) { const t = p.toks[p.i]; try found.appendSlice(p.arena, try p.interpolations(t.start + 2, t.end, false)); p.advance(); } const n = try p.node(first, .multiline); n.interpolations = found.items; return n; }, .builtin => return p.parseCall(), .period => { p.advance(); _ = try p.expectName(); return p.node(first, .enum_literal); }, .period_brace => return p.parseLiteral(), .l_paren => { p.advance(); const inner = try p.parseExpr(); _ = try p.expect(.r_paren); return p.node(first, .{ .paren = inner }); }, .keyword_if => return p.parseIf(), .keyword_switch => return p.parseSwitch(), else => return p.unexpected("an expression"), } }
/// An `else` is optional everywhere here. The real parser allows leaving /// it out only of a field's value or a list's item, but that is a rule of /// meaning, and where it is allowed an `else` binds to the nearest `if`, /// which is what this does everywhere. fn parseIf(p: *Parser) Error!*const Node { try p.enter(); defer p.leave(); const first = p.i; p.advance(); _ = try p.expect(.l_paren); const cond = try p.parseExpr(); _ = try p.expect(.r_paren); var capture: ?u32 = null; if (p.tag() == .pipe) { p.advance(); capture = try p.expectName(); _ = try p.expect(.pipe); } const then = try p.parseBranch(); var otherwise: ?*const Node = null; if (p.tag() == .keyword_else) { p.advance(); otherwise = try p.parseBranch(); } return p.node(first, .{ .@"if" = .{ .cond = cond, .capture = capture, .then = then, .@"else" = otherwise } }); }
fn parseBranch(p: *Parser) Error!*const Node { if (p.tag() == .keyword_if) return p.parseIf(); return p.parseExpr(); }
fn parseSwitch(p: *Parser) Error!*const Node { try p.enter(); defer p.leave(); const first = p.i; p.advance(); _ = try p.expect(.l_paren); const subject = try p.parseExpr(); _ = try p.expect(.r_paren); _ = try p.expect(.l_brace);
var prongs: std.ArrayList(*const Node) = .empty; while (p.tag() != .r_brace) { const prong_first = p.i; var cases: std.ArrayList(*const Node) = .empty; if (p.tag() == .keyword_else) { p.advance(); } else while (true) { const lo = try p.parseExpr(); if (p.tag() == .ellipsis3) { p.advance(); const hi = try p.parseExpr(); try cases.append(p.arena, try p.node(lo.first, .{ .range = .{ .lo = lo, .hi = hi } })); } else { try cases.append(p.arena, lo); } if (p.tag() != .comma) break; p.advance(); if (p.tag() == .equal_angle_bracket_right) break; } const arrow = try p.expect(.equal_angle_bracket_right); var capture: ?u32 = null; if (p.tag() == .pipe) { p.advance(); capture = try p.expectName(); _ = try p.expect(.pipe); } const value = try p.parseExpr(); try prongs.append(p.arena, try p.node(prong_first, .{ .prong = .{ .cases = cases.items, .arrow = arrow, .capture = capture, .value = value, } })); if (p.tag() == .comma) { p.advance(); } else if (p.tag() != .r_brace) { return p.unexpected("',' or '}}'"); } } p.advance(); if (prongs.items.len == 0) return p.fail(first, "a switch needs at least one prong", .{}); return p.node(first, .{ .@"switch" = .{ .subject = subject, .prongs = prongs.items } }); }
fn parseItem(p: *Parser) Error!*const Node { if (p.tag() == .keyword_if) return p.parseIf(); return p.parseExpr(); }
fn parseLiteral(p: *Parser) Error!*const Node { try p.enter(); defer p.leave(); const first = p.i; p.advance(); var items: std.ArrayList(*const Node) = .empty; if (p.tag() == .r_brace) { p.advance(); return p.node(first, .{ .literal = items.items }); } const is_record = p.tag() == .period and (p.toks[p.i + 1].tag == .identifier or p.toks[p.i + 1].tag == .quoted_identifier) and p.toks[p.i + 2].tag == .equal; while (true) { if (is_record) { const field_first = p.i; if (p.tag() != .period) return p.unexpected("'.name = value' or '}}'"); p.advance(); _ = try p.expectName(); _ = try p.expect(.equal); const value = try p.parseItem(); try items.append(p.arena, try p.node(field_first, .{ .field_init = value })); } else { try items.append(p.arena, try p.parseItem()); } switch (p.tag()) { .comma => { p.advance(); if (p.tag() == .r_brace) break; }, .r_brace => break, else => return p.unexpected("',' or '}}'"), } } p.advance(); return p.node(first, .{ .literal = items.items }); }
/// Each `${ }` in the string content `source[start..end]`, parsed as /// `Parser.zig` parses it, by pointing a second tokenizer at its inside. /// `escapes` is whether a `\` escapes the byte after it, as it does in /// a string and not in a multiline string's line. The tokenizer has /// already checked that every `${` is closed, and how deeply strings nest. fn interpolations(p: *Parser, start: u32, end: u32, escapes: bool) Error![]const Interpolation { var found: std.ArrayList(Interpolation) = .empty; var i = start; while (i < end) { if (p.source[i] == '\\' and escapes) { i += 2; } else if (p.source[i] == '$' and i + 1 < end and p.source[i + 1] == '{') { const close = Tokenizer.skipInterpolation(p.source, i + 2, end, 0) orelse { p.diag.set(.of(p.source, i), "unterminated interpolation", .{}); return error.Syntax; }; try found.append(p.arena, try p.interpolation(i, close)); i = close + 1; } else i += 1; } return found.items; }
fn interpolation(p: *Parser, dollar: u32, close: u32) Error!Interpolation { var toks: std.ArrayList(Token) = .empty; var t: Tokenizer = .{ .source = p.source, .index = dollar + 2, .limit = close }; while (true) { const tok = t.next(); if (tok.tag == .invalid) return invalidToken(p.source, tok, p.diag); try toks.append(p.arena, tok); if (tok.tag == .eof) break; } if (toks.items.len == 1) { p.diag.set(.of(p.source, dollar), "empty interpolation", .{}); return error.Syntax; } const saved_toks = p.toks; const saved_i = p.i; defer { p.toks = saved_toks; p.i = saved_i; } p.toks = toks.items; p.i = 0; const n = try p.parseExpr(); if (p.tag() != .eof) return p.unexpected("'}}'"); return .{ .toks = toks.items, .node = n }; }
fn parseCall(p: *Parser) Error!*const Node { try p.enter(); defer p.leave(); const first = p.i; p.advance(); _ = try p.expect(.l_paren); var args: std.ArrayList(*const Node) = .empty; while (p.tag() != .r_paren) { try args.append(p.arena, try p.parseExpr()); if (p.tag() == .comma) { p.advance(); } else if (p.tag() != .r_paren) { return p.unexpected("',' or ')'"); } } p.advance(); return p.node(first, .{ .call = args.items }); }};
// -- outline ------------------------------------------------------------------
/// What a `Span` is: the shape of the source it covers.pub const SpanKind = enum { /// The whole program, from its first token to its last. file, /// `let name = value;` let, /// `.{ ... }`, a struct or list literal. literal, /// `.name = value`, inside a struct literal. field, /// `@name(args)` call, @"switch", /// `cases => value`, inside a switch. prong, @"if", /// One or more `\\` lines. multiline_string, /// A name used as a value: `ctx`, a `let`, a capture, `inf` or `nan`. name, /// Any other expression: a number, a string, an operator and its /// operands, `x.name`, `x[i]`, `(x)`, `lo...hi`. expression,};
/// Where a name is written, by byte offsets into the source.pub const NameSpan = struct { start: u32, end: u32 };
/// A piece of a program, by where it starts and ends in the source.pub const Span = struct { /// Byte offsets: `source[start..end]` is the span, comments within it /// included and comments around it not. start: u32, end: u32, kind: SpanKind, /// The index of the span this one is inside, which comes before it, or /// null for the `file`. parent: ?u32, /// The name the span binds or calls, which is not a span of its own: a /// `let`'s name, an `if`'s or a prong's `|name|`, a field's `.name` /// without its `.`, or a call's `@name` with its `@`. name: ?NameSpan = null,};
/// The program in `source` as a tree of spans, for an editor to fold or to/// grow a selection by. Every span comes before the spans inside it, and/// after any that end before it starts, so the spans holding an offset are/// a chain whose innermost is the last of them. The caller frees the/// result.////// It parses as `format` does: names are not resolved, nor files included,/// and any function name is accepted. What is inside a string's `${ }` is/// outlined too, inside the string's span.pub fn outline(gpa: Allocator, source: []const u8, options: Options, diag: *Diagnostic) Error![]Span { var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator();
if (source.len >= std.math.maxInt(u32)) { diag.set(.{}, "program too large", .{}); return error.Syntax; } var p: Parser = .{ .arena = arena, .source = source, .toks = try tokenize(arena, source, diag), .diag = diag, .max_depth = options.max_depth, }; const file = try p.parseFile();
var o: Outliner = .{ .gpa = gpa, .toks = p.toks }; errdefer o.spans.deinit(gpa); const root = try o.add(0, file.eof - 1, .file, null); for (file.lets) |let| { const index = try o.add(let.value.first - 3, let.value.last + 1, .let, root); o.named(index, let.value.first - 2); try o.node(let.value, index); } try o.node(file.root, root); return o.spans.toOwnedSlice(gpa);}
const Outliner = struct { gpa: Allocator, toks: []const Token, spans: std.ArrayList(Span) = .empty,
/// A span from token `first` to token `last`, both included. fn add(o: *Outliner, first: u32, last: u32, kind: SpanKind, parent: ?u32) Allocator.Error!u32 { const index: u32 = @intCast(o.spans.items.len); try o.spans.append(o.gpa, .{ .start = o.toks[first].start, .end = o.toks[last].end, .kind = kind, .parent = parent, }); return index; }
/// Give span `index` the name that is token `tok`. fn named(o: *Outliner, index: u32, tok: u32) void { o.spans.items[index].name = .{ .start = o.toks[tok].start, .end = o.toks[tok].end }; }
/// `n` and everything inside it. The parser bounds how deeply nodes /// nest, which bounds how deeply this recurses. fn node(o: *Outliner, n: *const Node, parent: u32) Allocator.Error!void { const kind: SpanKind = switch (n.data) { .token => switch (o.toks[n.first].tag) { .identifier, .quoted_identifier => .name, else => .expression, }, .multiline => .multiline_string, .call => .call, .literal => .literal, .field_init => .field, .@"if" => .@"if", .@"switch" => .@"switch", .prong => .prong, else => .expression, }; const index = try o.add(n.first, n.last, kind, parent); switch (n.data) { .call => o.named(index, n.first), .field_init => o.named(index, n.first + 1), .@"if" => |x| if (x.capture) |c| o.named(index, c), .prong => |x| if (x.capture) |c| o.named(index, c), else => {}, } if (n.interpolations.len > 0) { const saved = o.toks; defer o.toks = saved; for (n.interpolations) |x| { o.toks = x.toks; try o.node(x.node, index); } } switch (n.data) { .token, .multiline, .enum_literal => {}, .paren, .unary, .field, .unwrap, .field_init => |inner| try o.node(inner, index), .binary => |b| { try o.node(b.lhs, index); try o.node(b.rhs, index); }, .index => |x| { try o.node(x.object, index); try o.node(x.index, index); }, .call, .literal => |items| for (items) |item| try o.node(item, index), .@"if" => |x| { try o.node(x.cond, index); try o.node(x.then, index); if (x.@"else") |e| try o.node(e, index); }, .@"switch" => |x| { try o.node(x.subject, index); for (x.prongs) |prong| try o.node(prong, index); }, .prong => |x| { for (x.cases) |case| try o.node(case, index); try o.node(x.value, index); }, .range => |x| { try o.node(x.lo, index); try o.node(x.hi, index); }, } }};
// -- rendering ----------------------------------------------------------------
/// Writes tokens in order, with the comments between them.////// Space and line breaks are asked for rather than written, and only written/// when the next thing is: so a space asked for before a line break is never/// written, and a comment that was at the end of a line can still be put/// there after the break before the next token has been asked for.const Renderer = struct { gpa: Allocator, source: []const u8, toks: []const Token, out: *std.ArrayList(u8), indent: u32 = 0, /// Comments have been written from the gaps before every token below /// this one. gaps_done: u32 = 0, /// Where the last token or comment written ends in the source. last_end: u32 = 0, /// The last token written, or null if a comment came after it. last_tag: ?Tag = null, space: bool = false, /// Line breaks asked for: one, or two for a blank line. newlines: u2 = 0,
const indent_width = 4;
fn file(r: *Renderer, f: File) Error!void { for (f.lets) |let| { const v = let.value; try r.token(v.first - 3); // let r.space = true; try r.token(v.first - 2); // name r.space = true; try r.token(v.first - 1); // = r.space = true; try r.expr(v); try r.token(v.last + 1); // ; r.lineBreak(); } try r.expr(f.root); try r.comments(f.eof); try r.out.append(r.gpa, '\n'); }
fn lineBreak(r: *Renderer) void { r.newlines = @max(r.newlines, 1); }
fn atLineStart(r: *const Renderer) bool { return r.out.items.len == 0 or r.newlines > 0; }
/// Write the space and the line breaks asked for, if anything has been /// written yet: a file starts with what is in it. fn begin(r: *Renderer) Error!void { if (r.out.items.len == 0) { r.newlines = 0; r.space = false; return; } if (r.newlines > 0) { try r.out.appendNTimes(r.gpa, '\n', r.newlines); try r.out.appendNTimes(r.gpa, ' ', r.indent); } else if (r.space) { try r.out.append(r.gpa, ' '); } r.newlines = 0; r.space = false; }
/// Ask for a blank line before what starts at `start`, if the source had /// one there and a line break has been asked for anyway. fn keepBlankLine(r: *Renderer, start: u32, next: ?Tag) void { if (r.newlines == 0 or r.out.items.len == 0) return; if (r.last_tag) |t| if (opens(t)) return; if (next) |t| if (closes(t)) return; if (r.last_tag == .multiline_string_line and next == .multiline_string_line) return; if (std.mem.count(u8, r.source[r.last_end..start], "\n") >= 2) r.newlines = 2; }
/// Write token `i`, after the comments before it. fn token(r: *Renderer, i: u32) Error!void { try r.comments(i); const t = r.toks[i]; r.keepBlankLine(t.start, t.tag); try r.begin(); var bytes = r.source[t.start..t.end]; // A line ending in `\r\n` is written with `\n`. The parser drops one // `\r` from the end of a line and keeps any others as content, so the // `\r` goes only if what is left does not end with another -- which // would then be the one dropped, and lost. if (t.tag == .multiline_string_line and std.mem.endsWith(u8, bytes, "\r") and !std.mem.endsWith(u8, bytes[0 .. bytes.len - 1], "\r")) { bytes = bytes[0 .. bytes.len - 1]; } try r.out.appendSlice(r.gpa, bytes); r.last_end = t.end; r.last_tag = t.tag; }
/// Write text that is not in the source: a trailing comma. fn text(r: *Renderer, s: []const u8) Error!void { try r.begin(); try r.out.appendSlice(r.gpa, s); }
/// Write the comments in the gaps before every token up to `i`. fn comments(r: *Renderer, i: u32) Error!void { while (r.gaps_done <= i) : (r.gaps_done += 1) { const k = r.gaps_done; var pos: u32 = if (k == 0) 0 else r.toks[k - 1].end; const end = r.toks[k].start; while (pos < end) { if (r.source[pos] == '/') { const stop: u32 = @intCast(std.mem.findScalarPos(u8, r.source[0..end], pos, '\n') orelse end); try r.comment(pos, stop); pos = stop; } else pos += 1; } } }
fn comment(r: *Renderer, start: u32, end: u32) Error!void { const body = std.mem.trimEnd(u8, r.source[start..end], " \t\r"); const same_line = r.out.items.len != 0 and std.mem.findScalar(u8, r.source[r.last_end..start], '\n') == null; if (same_line) { // Where it was: at the end of the line, even if a line break has // been asked for since. try r.out.append(r.gpa, ' '); try r.out.appendSlice(r.gpa, body); } else { r.lineBreak(); r.keepBlankLine(start, null); try r.begin(); try r.out.appendSlice(r.gpa, body); } r.space = false; r.lineBreak(); r.last_end = end; r.last_tag = null; }
fn opens(t: Tag) bool { return t == .period_brace or t == .l_brace or t == .l_paren or t == .l_bracket; }
fn closes(t: Tag) bool { return t == .r_brace or t == .r_paren or t == .r_bracket; }
// -- what breaks ----------------------------------------------------------
/// Whether there is a comment in a gap before any of the tokens after /// `first`, up to and including `last`. fn hasComment(r: *const Renderer, first: u32, last: u32) bool { var k = first + 1; while (k <= last) : (k += 1) { const gap = r.source[r.toks[k - 1].end..r.toks[k].start]; if (std.mem.find(u8, gap, "//") != null) return true; } return false; }
fn newlineAfter(r: *const Renderer, i: u32) bool { return std.mem.findScalar(u8, r.source[r.toks[i].end..r.toks[i + 1].start], '\n') != null; }
/// Whether `n` cannot be written on one line: what it renders has a line /// break in it. fn breaks(r: *const Renderer, n: *const Node) bool { if (r.hasComment(n.first, n.last)) return true; return switch (n.data) { .token, .enum_literal => false, .multiline => true, .paren, .unary, .field, .unwrap, .field_init => |x| r.breaks(x), .binary => |b| r.newlineAfter(b.op) or r.breaks(b.lhs) or r.breaks(b.rhs), .index => |x| r.breaks(x.object) or r.breaks(x.index), .call => |args| r.tall(n.first + 1, args, n.last), .literal => |items| r.tall(n.first, items, n.last), .@"if" => |x| r.breaks(x.cond) or r.breaks(x.then) or (if (x.@"else") |e| r.breaks(e) else false), .@"switch" => |x| r.breaks(x.subject) or r.tall(x.subject.last + 2, x.prongs, n.last), .prong => |x| r.tallCases(x.cases, x.arrow) or r.breaks(x.value), .range => |x| r.breaks(x.lo) or r.breaks(x.hi), }; }
/// Whether the items between `open` and `close` go one to a line. fn tall(r: *const Renderer, open: u32, items: []const *const Node, close: u32) bool { if (r.hasComment(open, close)) return true; if (items.len == 0) return false; if (r.toks[items[items.len - 1].last + 1].tag == .comma) return true; for (items) |item| if (r.breaks(item)) return true; return false; }
fn tallCases(r: *const Renderer, cases: []const *const Node, arrow: u32) bool { if (cases.len == 0) return false; if (r.hasComment(cases[0].first, arrow)) return true; if (r.toks[arrow - 1].tag == .comma) return true; for (cases) |c| if (r.breaks(c)) return true; return false; }
// -- expressions ----------------------------------------------------------
fn expr(r: *Renderer, n: *const Node) Error!void { switch (n.data) { .token => try r.token(n.first), .multiline => try r.multiline(n), .enum_literal => { try r.token(n.first); try r.token(n.first + 1); }, .paren => |inner| { try r.token(n.first); try r.expr(inner); try r.token(n.last); }, .unary => |operand| { try r.token(n.first); try r.expr(operand); }, .binary => |b| { try r.expr(b.lhs); r.space = true; try r.token(b.op); if (r.newlineAfter(b.op)) { r.indent += indent_width; r.lineBreak(); try r.expr(b.rhs); r.indent -= indent_width; } else { r.space = true; try r.expr(b.rhs); } }, .field => |object| { try r.expr(object); // `0x1 .a` written as `0x1.a` would be one hex number. if (r.toks[object.last].tag == .number) r.space = true; try r.token(n.last - 1); try r.token(n.last); }, .unwrap => |object| { try r.expr(object); try r.token(n.last); }, .index => |x| { try r.expr(x.object); try r.token(x.index.first - 1); try r.expr(x.index); try r.token(n.last); }, .call => |args| { try r.token(n.first); try r.list(n.first + 1, args, n.last, false); }, .literal => |items| try r.list(n.first, items, n.last, true), .field_init => |value| { try r.token(n.first); try r.token(n.first + 1); r.space = true; try r.token(n.first + 2); r.space = true; try r.expr(value); }, .@"if" => |x| { try r.token(n.first); r.space = true; try r.token(n.first + 1); try r.expr(x.cond); try r.token(x.cond.last + 1); if (x.capture) |name| try r.capture(name); r.space = true; try r.expr(x.then); if (x.@"else") |otherwise| { r.space = true; try r.token(x.then.last + 1); r.space = true; try r.expr(otherwise); } }, .@"switch" => |x| { try r.token(n.first); r.space = true; try r.token(n.first + 1); try r.expr(x.subject); try r.token(x.subject.last + 1); r.space = true; try r.list(x.subject.last + 2, x.prongs, n.last, true); }, .prong => |x| { if (x.cases.len == 0) { try r.token(n.first); // else r.space = true; } else if (r.tallCases(x.cases, x.arrow)) { for (x.cases, 0..) |c, i| { if (i > 0) r.lineBreak(); try r.expr(c); try r.comma(c); } r.lineBreak(); } else { for (x.cases, 0..) |c, i| { if (i > 0) { try r.token(x.cases[i - 1].last + 1); r.space = true; } try r.expr(c); } r.space = true; } try r.token(x.arrow); if (x.capture) |name| try r.capture(name); r.space = true; try r.expr(x.value); }, .range => |x| { try r.expr(x.lo); try r.token(x.lo.last + 1); try r.expr(x.hi); }, } }
/// ` |name|`, given the name. fn capture(r: *Renderer, name: u32) Error!void { r.space = true; try r.token(name - 1); try r.token(name); try r.token(name + 1); }
/// The comma after `item`: its own, or a new one. fn comma(r: *Renderer, item: *const Node) Error!void { if (r.toks[item.last + 1].tag == .comma) return r.token(item.last + 1); return r.text(","); }
/// `open items close`, on one line or one item to a line. `padded` puts /// a space inside the brackets when they are on one line, as Zig does for /// `.{ 1, 2 }` and not for a call's `(1, 2)`. fn list(r: *Renderer, open: u32, items: []const *const Node, close: u32, padded: bool) Error!void { const is_tall = r.tall(open, items, close); try r.token(open); if (is_tall) { r.indent += indent_width; for (items) |item| { r.lineBreak(); try r.expr(item); try r.comma(item); } // What is left before the bracket -- comments after the last // item -- belongs to the items, at their indent. try r.comments(close); r.indent -= indent_width; r.lineBreak(); } else if (items.len != 0) { r.space = padded; for (items, 0..) |item, i| { if (i > 0) { try r.token(items[i - 1].last + 1); r.space = true; } try r.expr(item); } r.space = padded; } try r.token(close); }
/// Each `\\` line on a line of its own: one indent in, if the string does /// not already start a line. Whatever follows starts on the next line. fn multiline(r: *Renderer, n: *const Node) Error!void { const extra: u32 = if (r.atLineStart()) 0 else indent_width; r.indent += extra; var i = n.first; while (i <= n.last) : (i += 1) { r.lineBreak(); try r.token(i); } r.indent -= extra; r.lineBreak(); }};
// -- tests --------------------------------------------------------------------
const testing = std.testing;
fn expectFormat(source: []const u8, expected: []const u8) !void { var d: Diagnostic = .{}; const got = format(testing.allocator, source, .{}, &d) catch |err| { std.debug.print("{f}\n", .{&d}); return err; }; defer testing.allocator.free(got); try testing.expectEqualStrings(expected, got); // Formatting what is formatted changes nothing. const again = try format(testing.allocator, got, .{}, &d); defer testing.allocator.free(again); try testing.expectEqualStrings(got, again);}
test "spacing" { try expectFormat("let a=1 ;let b =a+ -2*3;b", "let a = 1;\nlet b = a + -2 * 3;\nb\n"); try expectFormat("! ctx . a .? or ctx.xs[ 0 ]orelse(1)", "!ctx.a.? or ctx.xs[0] orelse (1)\n"); try expectFormat("@max( 1 , 2 )", "@max(1, 2)\n"); try expectFormat("if(ctx.a)|v|v else if (true) 2 else 3", "if (ctx.a) |v| v else if (true) 2 else 3\n"); try expectFormat(".@\"two words\"", ".@\"two words\"\n");}
test "a literal is one line unless it ends with a comma" { try expectFormat(".{.a=1,.b=.{1,2}}", ".{ .a = 1, .b = .{ 1, 2 } }\n"); try expectFormat(".{ }", ".{}\n"); try expectFormat(".{.a=1,.b=2,}", \\.{ \\ .a = 1, \\ .b = 2, \\} \\ ); // A tall literal inside makes the one around it tall too. try expectFormat(".{ .a = .{ 1, }, .b = 2 }", \\.{ \\ .a = .{ \\ 1, \\ }, \\ .b = 2, \\} \\ ); try expectFormat("@join(.{ \"a\", \"b\" }, \",\",)", \\@join( \\ .{ "a", "b" }, \\ ",", \\) \\ );}
test "comments stay where they were" { try expectFormat( \\// a file \\ \\ \\let a = 1; // one \\ \\// the value \\.{ \\ \\ .a = a, // a \\ // b next \\ .b = 2 \\ // the end \\ \\} , \\// a file \\ \\let a = 1; // one \\ \\// the value \\.{ \\ .a = a, // a \\ // b next \\ .b = 2, \\ // the end \\} \\ ); // A comment makes a literal tall, and is kept even in an empty one. One // after the opening bracket stays there, as it does in Zig. try expectFormat(".{ 1, 2 // two\n}", ".{\n 1,\n 2, // two\n}\n"); try expectFormat(".{\n// nothing\n}", ".{\n // nothing\n}\n"); try expectFormat(".{ // first\n1 }", ".{ // first\n 1,\n}\n");}
test "a line break after an operator is kept" { try expectFormat("ctx.a and\nctx.b and\n ctx.c", "ctx.a and\n ctx.b and\n ctx.c\n"); try expectFormat("ctx.a\nand ctx.b", "ctx.a and ctx.b\n"); try expectFormat("1 + // one\n2", "1 + // one\n 2\n");}
test "multiline strings are lines of their own" { // The spaces after `${x}` are part of the string, and stay. try expectFormat( ".{ .a = \\\\one\n \\\\two ${x} \n, .b = 1 }", ".{\n .a =\n \\\\one\n \\\\two ${x} \n ,\n .b = 1,\n}\n", ); try expectFormat("let s = \\\\x\n; s", "let s =\n \\\\x\n;\ns\n"); try expectFormat("\\\\a\r\n\\\\b\r\n", "\\\\a\n\\\\b\n");}
test "switch" { try expectFormat("switch(ctx.n){1...4=>8,5,6=>|v|v,else=>0}", "switch (ctx.n) { 1...4 => 8, 5, 6 => |v| v, else => 0 }\n"); try expectFormat("switch (ctx.n) { 1 => 2, else => 3, }", \\switch (ctx.n) { \\ 1 => 2, \\ else => 3, \\} \\ ); try expectFormat("switch (ctx.n) { .a, .b, => 1 }", \\switch (ctx.n) { \\ .a, \\ .b, \\ => 1, \\} \\ );}
test "a number keeps its distance from a field" { try expectFormat("0x1 .a", "0x1 .a\n");}
test "a program that does not parse is an error with a position" { var d: Diagnostic = .{}; try testing.expectError(error.Syntax, format(testing.allocator, ".{ .a = }", .{}, &d)); try testing.expectEqual(Diagnostic.Pos{ .line = 1, .column = 9 }, d.pos); try testing.expectError(error.Syntax, format(testing.allocator, "\"abc", .{}, &d)); try testing.expectEqualStrings("unterminated string", d.message());}
test "an outline is the program's spans, outermost first" { const source = \\let a = 1; \\.{ .b = @max(a, 2) } ; var d: Diagnostic = .{}; const spans = try outline(testing.allocator, source, .{}, &d); defer testing.allocator.free(spans);
var got: std.Io.Writer.Allocating = .init(testing.allocator); defer got.deinit(); for (spans) |span| { try got.writer.print("{t} {?d} '{s}'", .{ span.kind, span.parent, source[span.start..span.end] }); if (span.name) |n| try got.writer.print(" named '{s}'", .{source[n.start..n.end]}); try got.writer.writeByte('\n'); } try testing.expectEqualStrings( \\file null 'let a = 1; \\.{ .b = @max(a, 2) }' \\let 0 'let a = 1;' named 'a' \\expression 1 '1' \\literal 0 '.{ .b = @max(a, 2) }' \\field 3 '.b = @max(a, 2)' named 'b' \\call 4 '@max(a, 2)' named '@max' \\name 5 'a' \\expression 5 '2' \\ , got.written());}
test "an outline names captures" { const source = "if (ctx.a) |x| switch (x) { else => |y| y } else 0"; var d: Diagnostic = .{}; const spans = try outline(testing.allocator, source, .{}, &d); defer testing.allocator.free(spans); var names: std.ArrayList(u8) = .empty; defer names.deinit(testing.allocator); for (spans) |span| if (span.name) |n| { try names.print(testing.allocator, "{t}:{s} ", .{ span.kind, source[n.start..n.end] }); }; try testing.expectEqualStrings("if:x prong:y ", names.items);}
test "an outline looks inside a string's interpolations" { const source = "let a = 1;\n.{ \"x${a}y${\"${@f(a)}\"}\\${a}\",\n\\\\${a + 1}\n}"; var d: Diagnostic = .{}; const spans = try outline(testing.allocator, source, .{}, &d); defer testing.allocator.free(spans); var got: std.ArrayList(u8) = .empty; defer got.deinit(testing.allocator); for (spans[4..]) |span| { try got.print(testing.allocator, "{t} {?d} '{s}'\n", .{ span.kind, span.parent, source[span.start..span.end] }); } try testing.expectEqualStrings( \\expression 3 '"x${a}y${"${@f(a)}"}\${a}"' \\name 4 'a' \\expression 4 '"${@f(a)}"' \\call 6 '@f(a)' \\name 7 'a' \\multiline_string 3 '\\${a + 1}' \\expression 9 'a + 1' \\name 10 'a' \\expression 10 '1' \\ , got.items);}
test "an interpolation that does not parse is an error with a position" { var d: Diagnostic = .{}; try testing.expectError(error.Syntax, outline(testing.allocator, "\"ab${}\"", .{}, &d)); try testing.expectEqual(Diagnostic.Pos{ .line = 1, .column = 4 }, d.pos); try testing.expectEqualStrings("empty interpolation", d.message()); try testing.expectError(error.Syntax, format(testing.allocator, "\"${1 +}\"", .{}, &d)); try testing.expectEqual(Diagnostic.Pos{ .line = 1, .column = 7 }, d.pos);}
test "an outline of a program that does not parse is an error with a position" { var d: Diagnostic = .{}; try testing.expectError(error.Syntax, outline(testing.allocator, ".{ .a = }", .{}, &d)); try testing.expectEqual(Diagnostic.Pos{ .line = 1, .column = 9 }, d.pos);}
// -- formatting never changes what a program means ----------------------------
const ast = @import("ast.zig");const RealParser = @import("Parser.zig");
/// Whether two trees mean the same, wherever they were written.fn sameNode(a: *const ast.Node, b: *const ast.Node) bool { if (std.meta.activeTag(a.data) != std.meta.activeTag(b.data)) return false; return switch (a.data) { .null, .ctx => true, .bool => |x| x == b.data.bool, .int => |x| x == b.data.int, // By bits, so that a NaN is the same as itself. .float => |x| @as(u64, @bitCast(x)) == @as(u64, @bitCast(b.data.float)), .string => |x| std.mem.eql(u8, x, b.data.string), .enum_literal => |x| std.mem.eql(u8, x, b.data.enum_literal), .slot => |x| x == b.data.slot, .template => |x| x.len == b.data.template.len and for (x, b.data.template) |p, q| { if (std.meta.activeTag(p) != std.meta.activeTag(q)) break false; if (!switch (p) { .text => |t| std.mem.eql(u8, t, q.text), .expr => |e| sameNode(e, q.expr), }) break false; } else true, .record => |x| x.len == b.data.record.len and for (x, b.data.record) |f, g| { if (!std.mem.eql(u8, f.name, g.name) or !sameNode(f.value, g.value)) break false; } else true, .list => |x| sameNodes(x, b.data.list), .field => |x| std.mem.eql(u8, x.name, b.data.field.name) and sameNode(x.object, b.data.field.object), .index => |x| sameNode(x.object, b.data.index.object) and sameNode(x.index, b.data.index.index), .unwrap => |x| sameNode(x, b.data.unwrap), .unary => |x| x.op == b.data.unary.op and sameNode(x.operand, b.data.unary.operand), .binary => |x| x.op == b.data.binary.op and sameNode(x.lhs, b.data.binary.lhs) and sameNode(x.rhs, b.data.binary.rhs), .@"if" => |x| { const y = b.data.@"if"; return x.capture == y.capture and sameNode(x.cond, y.cond) and sameNode(x.then, y.then) and sameOptional(x.@"else", y.@"else"); }, .@"switch" => |x| { const y = b.data.@"switch"; if (!sameNode(x.subject, y.subject) or x.prongs.len != y.prongs.len) return false; for (x.prongs, y.prongs) |p, q| if (!sameProng(p, q)) return false; if ((x.@"else" == null) != (y.@"else" == null)) return false; return x.@"else" == null or sameProng(x.@"else".?, y.@"else".?); }, .call => |x| std.meta.eql(x.func, b.data.call.func) and sameNodes(x.args, b.data.call.args), };}
fn sameNodes(a: []const *const ast.Node, b: []const *const ast.Node) bool { if (a.len != b.len) return false; for (a, b) |x, y| if (!sameNode(x, y)) return false; return true;}
fn sameOptional(a: ?*const ast.Node, b: ?*const ast.Node) bool { if (a == null or b == null) return a == null and b == null; return sameNode(a.?, b.?);}
fn sameProng(a: ast.Prong, b: ast.Prong) bool { if (a.capture != b.capture or a.cases.len != b.cases.len or !sameNode(a.value, b.value)) return false; for (a.cases, b.cases) |c, d| { if (std.meta.activeTag(c) != std.meta.activeTag(d)) return false; if (!switch (c) { .value => |v| sameNode(v, d.value), .range => |r| sameNode(r.lo, d.range.lo) and sameNode(r.hi, d.range.hi), }) return false; } return true;}
/// For any program the real parser accepts: it formats, what it formats to/// parses to the same tree, and formatting that changes nothing.fn checkFormat(gpa: Allocator, src: []const u8) !void { var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); var d: Diagnostic = .{}; const before = RealParser.parse(arena, src, .{}, &d) catch |err| switch (err) { error.OutOfMemory => return err, else => return, }; const out = format(gpa, src, .{}, &d) catch |err| { std.debug.print("{s}\nformat: {f}\n", .{ src, &d }); return err; }; defer gpa.free(out); const after = RealParser.parse(arena, out, .{}, &d) catch |err| { std.debug.print("{s}\nformatted to\n{s}\nwhich does not parse: {f}\n", .{ src, out, &d }); return err; }; const same = sameNode(before.root, after.root) and before.slots.len == after.slots.len and for (before.slots, after.slots) |x, y| { if (!sameOptional(x, y)) break false; } else true; if (!same) { std.debug.print("{s}\nformatted to\n{s}\nwhich means something else\n", .{ src, out }); return error.FormattingChangedTheMeaning; } const again = try format(gpa, out, .{}, &d); defer gpa.free(again); if (!std.mem.eql(u8, out, again)) { std.debug.print("{s}\nformatted to\n{s}\nand then to\n{s}\n", .{ src, out, again }); return error.FormattingIsNotIdempotent; }}
const corpus = [_][]const u8{ "1", "let a = 1; let b = a + 1; .{ .x = b }", ".{ .a = 1, .b = \"two\", .c = .three, .d = .{ 4, 5 }, .e = null, .f = 'x', }", "if (ctx.b) |x| x else if (false) 2 else 3", "\"a${ctx.s}b${\"c${1}\"}d\\${e}\"", "\\\\one ${ctx.n}\n\\\\two\n", ".{ \\\\one\n// between\n\\\\two\n, 1 }", "@join(@split(\"a,b\", \",\"), \"-\") ++ @lower(\"X\")", "-9223372036854775808 + 0x7fff_ffff * 1.5e3 / 2 % 3", "ctx.m.b.c[0] == 100 and !(ctx.n < 2) or ctx.none orelse 1 != 2", ".{ 1, if (true) 2, if (false) 3 else if (true) 4 }", ".{ .@\"if\" = inf, .b = -nan }", "switch (ctx.n) { 1...5, 9 => |v| v, -3 => 0, else => 1 }", "switch (ctx.n) { 1...5, // small\n 9, => |v| v,\n\n\n -3 => 0, else => 1, }", "-5.? + - -1 - !!true", "0x1 .a + 1 .b + 0x1.8p1 .c", "// only a comment before\n1 // and after\n// and below", "let a = 1 + // why\n 2;\n\n\n// and\nlet b = a\n* 3;\n.{ .x = a and\nb, .y = .{ // first\n}, }", "@max(1, \\\\x\n, 3)", "\\\\a\n\n\\\\b\n++ \"c\" ++ \\\\d\n", // A string that ends in a carriage return, written with and without a // line ending of its own. "\\\\\r\r", "\\\\\r\r\n", "\\\\a\r\n\\\\b\r\r\r\n", "let s = \\\\x\n; switch (s) { \\\\x\n => \\\\y\n, else => .{} }", "if (ctx.a) .{ 1, } else .{ .a = .{} }", "1 + if (ctx.a) 2 else 3 * 4",};
test "formatting keeps the meaning, and formatting twice changes nothing" { for (corpus) |src| try checkFormat(testing.allocator, src);}
fn fuzzFormat(_: void, smith: *std.testing.Smith) anyerror!void { var buf: [4096]u8 = undefined; try checkFormat(testing.allocator, buf[0..smith.slice(&buf)]);}
test "fuzz format" { try testing.fuzz({}, fuzzFormat, .{});}