diff --git a/lib/std/zig/string_literal.zig b/lib/std/zig/string_literal.zig index a795e8df7418..26cd9126684f 100644 --- a/lib/std/zig/string_literal.zig +++ b/lib/std/zig/string_literal.zig @@ -298,3 +298,143 @@ test "parse" { try expect(eql(u8, "foo", try parseAlloc(alloc, "\"f\x6f\x6f\""))); try expect(eql(u8, "f💯", try parseAlloc(alloc, "\"f\u{1f4af}\""))); } + +/// Parses one line at a time of a multiline Zig string literal to a std.io.Writer type. Does not append a null terminator. +pub fn MultilineParser(comptime Writer: type) type { + return struct { + writer: Writer, + first_line: bool, + + pub fn init(writer: Writer) @This() { + return .{ + .writer = writer, + .first_line = true, + }; + } + + /// Parse one line of a multiline string, writing the result to the writer prepending a newline if necessary. + /// + /// Asserts bytes begins with "\\". The line may be terminated with '\n' or "\r\n", but may not contain any interior newlines. + /// contain any interior newlines. + pub fn line(self: *@This(), bytes: []const u8) Writer.Error!void { + assert(bytes.len >= 2 and bytes[0] == '\\' and bytes[1] == '\\'); + if (self.first_line) { + self.first_line = false; + } else { + try self.writer.writeByte('\n'); + } + const carriage_return_ending: usize = if (bytes[bytes.len - 2] == '\r') 2 else if (bytes[bytes.len - 1] == '\n') 1 else 0; + const line_bytes = bytes[2 .. bytes.len - carriage_return_ending]; + try self.writer.writeAll(line_bytes); + } + }; +} + +pub fn multilineParser(writer: anytype) MultilineParser(@TypeOf(writer)) { + return MultilineParser(@TypeOf(writer)).init(writer); +} + +test "parse multiline" { + // Varying newlines + { + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\foo"); + try std.testing.expectEqualStrings("foo", parsed.items); + try parser.line("\\\\bar"); + try std.testing.expectEqualStrings("foo\nbar", parsed.items); + } + + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\foo"); + try std.testing.expectEqualStrings("foo", parsed.items); + try parser.line("\\\\bar\n"); + try std.testing.expectEqualStrings("foo\nbar", parsed.items); + } + + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\foo"); + try std.testing.expectEqualStrings("foo", parsed.items); + try parser.line("\\\\bar\r\n"); + try std.testing.expectEqualStrings("foo\nbar", parsed.items); + } + + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\foo\n"); + try std.testing.expectEqualStrings("foo", parsed.items); + try parser.line("\\\\bar"); + try std.testing.expectEqualStrings("foo\nbar", parsed.items); + } + + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\foo\r\n"); + try std.testing.expectEqualStrings("foo", parsed.items); + try parser.line("\\\\bar"); + try std.testing.expectEqualStrings("foo\nbar", parsed.items); + } + } + + // Empty lines + { + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\"); + try std.testing.expectEqualStrings("", parsed.items); + try parser.line("\\\\"); + try std.testing.expectEqualStrings("\n", parsed.items); + try parser.line("\\\\foo"); + try std.testing.expectEqualStrings("\n\nfoo", parsed.items); + try parser.line("\\\\bar"); + try std.testing.expectEqualStrings("\n\nfoo\nbar", parsed.items); + } + + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\foo"); + try std.testing.expectEqualStrings("foo", parsed.items); + try parser.line("\\\\"); + try std.testing.expectEqualStrings("foo\n", parsed.items); + try parser.line("\\\\bar"); + try std.testing.expectEqualStrings("foo\n\nbar", parsed.items); + try parser.line("\\\\"); + try std.testing.expectEqualStrings("foo\n\nbar\n", parsed.items); + } + } + + // No escapes + { + var parsed = std.ArrayList(u8).init(std.testing.allocator); + defer parsed.deinit(); + const writer = parsed.writer(); + var parser = multilineParser(writer); + try parser.line("\\\\no \\n escape"); + try std.testing.expectEqualStrings("no \\n escape", parsed.items); + try parser.line("\\\\still no \\n escape"); + try std.testing.expectEqualStrings("no \\n escape\nstill no \\n escape", parsed.items); + } +} diff --git a/lib/std/zon.zig b/lib/std/zon.zig index e04fa0930a9a..b87ad86ac5ca 100644 --- a/lib/std/zon.zig +++ b/lib/std/zon.zig @@ -22,7 +22,7 @@ pub const ParseOptions = struct { // TODO: support max_value_len too? }; -pub const Error = error{ OutOfMemory, Type }; +const Error = error{ OutOfMemory, Type }; // TODO: make a render errors function...handle underlines as well as point of error like zig? How? pub const Status = union(enum) { @@ -70,7 +70,49 @@ pub const Status = union(enum) { }, }; -pub fn parseFromAst(comptime T: type, gpa: Allocator, ast: *const Ast, err: ?*Status, options: ParseOptions) Error!T { +fn requiresAllocator(comptime T: type) bool { + return switch (@typeInfo(T)) { + .Pointer => true, + .Array => |Array| requiresAllocator(Array.child), + .Struct => |Struct| inline for (Struct.fields) |field| { + if (requiresAllocator(field.type)) { + break true; + } + } else false, + .Union => |Union| inline for (Union.fields) |field| { + if (requiresAllocator(field.type)) { + break true; + } + } else false, + .Optional => |Optional| requiresAllocator(Optional.child), + else => false, + }; +} + +test "requiresAllocator" { + try std.testing.expect(!requiresAllocator(u8)); + try std.testing.expect(!requiresAllocator(f32)); + try std.testing.expect(!requiresAllocator(enum { foo })); + try std.testing.expect(!requiresAllocator(@TypeOf(.foo))); + try std.testing.expect(!requiresAllocator(struct { f32 })); + try std.testing.expect(!requiresAllocator(struct { x: f32 })); + try std.testing.expect(!requiresAllocator([2]u8)); + try std.testing.expect(!requiresAllocator(union { x: f32, y: f32 })); + try std.testing.expect(!requiresAllocator(union(enum) { x: f32, y: f32 })); + try std.testing.expect(!requiresAllocator(?f32)); + try std.testing.expect(!requiresAllocator(void)); + try std.testing.expect(!requiresAllocator(@TypeOf(null))); + + try std.testing.expect(requiresAllocator([]u8)); + try std.testing.expect(requiresAllocator(*struct{ u8, u8 })); + try std.testing.expect(requiresAllocator([1][]const u8)); + try std.testing.expect(requiresAllocator(struct { x: i32, y: []u8 })); + try std.testing.expect(requiresAllocator(union { x: i32, y: []u8 })); + try std.testing.expect(requiresAllocator(union(enum) { x: i32, y: []u8 })); + try std.testing.expect(requiresAllocator(?[]u8)); +} + +pub fn parseFromAst(comptime T: type, gpa: Allocator, ast: *const Ast, err: ?*Status, options: ParseOptions) error{ OutOfMemory, Type }!T { var parser = Parser{ .gpa = gpa, .ast = ast, @@ -83,6 +125,39 @@ pub fn parseFromAst(comptime T: type, gpa: Allocator, ast: *const Ast, err: ?*St return parser.parseExpr(T, root); } +pub fn parseFromAstNoAlloc(comptime T: type, ast: *const Ast, err: ?*Status, options: ParseOptions) error { Type }!T { + if (comptime requiresAllocator(T)) { + @compileError(@typeName(T) ++ ": requires allocator"); + } + var buffer: [0]u8 = .{}; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + return parseFromAst(T, fba.allocator(), ast, err, options) catch |e| switch (e) { + error.OutOfMemory => unreachable, + else => |other| return other, + }; +} + +test "parseFromAstNoAlloc" { + var ast = try std.zig.Ast.parse(std.testing.allocator, ".{ .x = 1.5, .y = 2.5 }", .zon); + defer ast.deinit(std.testing.allocator); + try std.testing.expectEqual(ast.errors.len, 0); + + const S = struct { x: f32, y: f32 }; + const found = try parseFromAstNoAlloc(S, &ast, null, .{}); + try std.testing.expectEqual(S{ .x = 1.5, .y = 2.5}, found); +} + +pub fn parseFromSlice(comptime T: type, gpa: Allocator, source: [:0]const u8, options: ParseOptions) error{ OutOfMemory, Type, Syntax }!T { + var ast = try std.zig.Ast.parse(gpa, source, .zon); + defer ast.deinit(gpa); + if (ast.errors.len != 0) return error.Syntax; + return parseFromAst(T, gpa, &ast, null, options); +} + +test "syntax error" { + try std.testing.expectError(error.Syntax, parseFromSlice(u8, std.testing.allocator, ".{", .{})); +} + test "error literals" { // TODO: can't return error!error, i think, so we need to use an out param, or not support this... // const gpa = std.testing.allocator; @@ -90,12 +165,6 @@ test "error literals" { // try std.testing.expectEqual(error.Foo, parsed); } -pub fn parseFromSlice(comptime T: type, gpa: Allocator, source: [:0]const u8, options: ParseOptions) Error!T { - var ast = try std.zig.Ast.parse(gpa, source, .zon); - defer ast.deinit(gpa); - assert(ast.errors.len == 0); - return parseFromAst(T, gpa, &ast, null, options); -} pub fn parseFree(gpa: Allocator, value: anytype) void { const Value = @TypeOf(value); @@ -104,7 +173,7 @@ pub fn parseFree(gpa: Allocator, value: anytype) void { .Bool, .Int, .Float, .Enum => {}, .Pointer => |Pointer| { switch (Pointer.size) { - .One, .Many, .C => failFreeType(Value), + .One, .Many, .C => @compileError(@typeName(Value) ++ ": parseFree cannot free non slice pointers"), .Slice => for (value) |item| { parseFree(gpa, item); }, @@ -117,7 +186,9 @@ pub fn parseFree(gpa: Allocator, value: anytype) void { .Struct => |Struct| inline for (Struct.fields) |field| { parseFree(gpa, @field(value, field.name)); }, - .Union => switch (value) { + .Union => |Union| if (Union.tag_type == null) { + @compileError(@typeName(Value) ++ ": parseFree cannot free untagged unions"); + } else switch (value) { inline else => |_, tag| { parseFree(gpa, @field(value, @tagName(tag))); }, @@ -127,13 +198,12 @@ pub fn parseFree(gpa: Allocator, value: anytype) void { }, .Void => {}, .Null => {}, - // TODO: ... - else => failFreeType(Value), + else => @compileError(@typeName(Value) ++ ": parseFree cannot free this type"), } } fn parseExpr(self: *Parser, comptime T: type, node: NodeIndex) Error!T { - // TODO: keep in sync with parseFree + // TODO: keep in sync with parseFree, stringify, and requiresAllocator switch (@typeInfo(T)) { // TODO: better errors for this? .Bool => return self.parseBool(node), @@ -149,9 +219,9 @@ fn parseExpr(self: *Parser, comptime T: type, node: NodeIndex) Error!T { .Union => return self.parseUnion(T, node), .Optional => return self.parseOptional(T, node), .Void => return self.parseVoid(node), - // .Null => return self.parseNull(node), + .Null => return self.parseNull(node), - else => failToParseType(T), + else => @compileError(@typeName(T) ++ ": cannot parse this type"), } } @@ -169,22 +239,6 @@ fn parseVoid(self: *Parser, node: NodeIndex) Error!void { } } -fn parseNull(self: *Parser, node: NodeIndex) error{Type}!void { - const tags = self.ast.nodes.items(.tag); - const main_tokens = self.ast.nodes.items(.main_token); - const token = main_tokens[node]; - switch (tags[node]) { - .identifier => { - const bytes = self.ast.tokenSlice(token); - if (std.mem.eql(u8, bytes, "void")) { - return true; - } - }, - else => {}, - } - return self.failExpectedType(void, node); -} - test "void" { const gpa = std.testing.allocator; @@ -214,8 +268,7 @@ test "void" { }, location); } - // XXX: will this fail properly comptime? - // Brackets around values (will eventually be parser error) + // Brackets around values { var ast = try std.zig.Ast.parse(gpa, "{1}", .zon); defer ast.deinit(gpa); @@ -235,7 +288,22 @@ test "void" { } } -// XXX: what's going on here? +// fn parseNull(self: *Parser, node: NodeIndex) error{Type}!void { +// const tags = self.ast.nodes.items(.tag); +// const main_tokens = self.ast.nodes.items(.main_token); +// const token = main_tokens[node]; +// switch (tags[node]) { +// .identifier => { +// const bytes = self.ast.tokenSlice(token); +// if (std.mem.eql(u8, bytes, "null")) { +// return true; +// } +// }, +// else => {}, +// } +// return self.failExpectedType(void, node); +// } + // TODO: see https://github.com/MasonRemaley/WIP-ZON/issues/3 // test "null" { // const gpa = std.testing.allocator; @@ -311,7 +379,7 @@ fn parseUnion(self: *Parser, comptime T: type, node: NodeIndex) Error!T { const field_infos = Union.fields; if (field_infos.len == 0) { - failToParseType(T); + @compileError(@typeName(T) ++ ": cannot parse unions with no fields"); } // Gather info on the fields @@ -1298,6 +1366,7 @@ fn parsePointer(self: *Parser, comptime T: type, node: NodeIndex) Error!T { const data = self.ast.nodes.items(.data); return switch (tags[node]) { .string_literal => try self.parseStringLiteral(T, node), + .multiline_string_literal => try self.parseMultilineStringLiteral(T, node), .address_of => try self.parseAddressOf(T, data[node].lhs), else => self.failExpectedType(T, node), }; @@ -1309,7 +1378,7 @@ fn parseAddressOf(self: *Parser, comptime T: type, node: NodeIndex) Error!T { // zig does, so it's consistent. Not gonna bother for now though can revisit later and decide. // Make sure we're working with a slice switch (Ptr.size) { - .One, .Many, .C => failToParseType(T), + .One, .Many, .C => @compileError(@typeName(T) ++ ": cannot parse pointers that are not slices"), .Slice => {}, } @@ -1338,7 +1407,7 @@ fn parseStringLiteral(self: *Parser, comptime T: type, node: NodeIndex) !T { switch (@typeInfo(T)) { .Pointer => |Pointer| { if (Pointer.size != .Slice) { - failToParseType(T); + @compileError(@typeName(T) ++ ": cannot parse pointers that are not slices"); } const main_tokens = self.ast.nodes.items(.main_token); @@ -1360,6 +1429,7 @@ fn parseStringLiteral(self: *Parser, comptime T: type, node: NodeIndex) !T { return self.failExpectedType(T, node); } + // TODO: see multiline string too // TODO: why couldn't I use from owned slice for this before when it was getting converted // back and forth? // var temp = std.ArrayListUnmanaged(u8).fromOwnedSlice(result); @@ -1375,38 +1445,40 @@ fn parseStringLiteral(self: *Parser, comptime T: type, node: NodeIndex) !T { return try buf.toOwnedSlice(self.gpa); }, - .Array => |Array| { - if (Array.sentinel) |sentinel| { - if (@as(*const u8, @ptrCast(sentinel)).* != 0) { - return self.failExpectedType(T, node); - } + else => unreachable, + } +} + +fn parseMultilineStringLiteral(self: *Parser, comptime T: type, node: NodeIndex) !T { + switch (@typeInfo(T)) { + .Pointer => |Pointer| { + if (Pointer.size != .Slice) { + @compileError(@typeName(T) ++ ": cannot parse pointers that are not slices"); } - if (Array.child != u8) { + if (Pointer.child != u8 or !Pointer.is_const or Pointer.alignment != 1) { return self.failExpectedType(T, node); } - const data = self.ast.nodes.items(.data); - const literal = data[node].lhs; - const main_tokens = self.ast.nodes.items(.main_token); - const token = main_tokens[literal]; - const raw = self.ast.tokenSlice(token); + var buf = std.ArrayListUnmanaged(u8){}; + defer buf.deinit(self.gpa); + const writer = buf.writer(self.gpa); - // TODO: are undefined zero terminated arrays still terminated? - var result: T = undefined; - var fsw = std.io.fixedBufferStream(&result); - const status = std.zig.string_literal.parseWrite(fsw.writer(), raw) catch |e| switch (e) { - error.NoSpaceLeft => return self.failExpectedType(T, node), - }; - switch (status) { - .success => {}, - .failure => |reason| return self.failInvalidStringLiteral(literal, reason), - } - if (Array.len != fsw.pos) { - return self.failExpectedType(T, node); + var parser = std.zig.string_literal.multilineParser(writer); + const data = self.ast.nodes.items(.data); + var tok_i = data[node].lhs; + while (tok_i <= data[node].rhs) : (tok_i += 1) { + try parser.line(self.ast.tokenSlice(tok_i)); } - return result; + if (Pointer.sentinel) |sentinel| { + if (@as(*const u8, @ptrCast(sentinel)).* != 0) { + return self.failExpectedType(T, node); + } + return buf.toOwnedSliceSentinel(self.gpa, 0); + } else { + return buf.toOwnedSlice(self.gpa); + } }, else => unreachable, } @@ -1419,60 +1491,148 @@ test "string literal" { { const parsed = try parseFromSlice([]const u8, gpa, "\"abc\"", .{}); defer parseFree(gpa, parsed); - try std.testing.expectEqualSlices(u8, @as([]const u8, "abc"), parsed); + try std.testing.expectEqualStrings(@as([]const u8, "abc"), parsed); } // String literal with escape characters { const parsed = try parseFromSlice([]const u8, gpa, "\"ab\\nc\"", .{}); defer parseFree(gpa, parsed); - try std.testing.expectEqualSlices(u8, @as([]const u8, "ab\nc"), parsed); + try std.testing.expectEqualStrings(@as([]const u8, "ab\nc"), parsed); } // Passing string literal to a mutable slice { - var ast = try std.zig.Ast.parse(gpa, "\"abcd\"", .zon); - defer ast.deinit(gpa); - var status: Status = .success; - try std.testing.expectError(error.Type, parseFromAst([]u8, gpa, &ast, &status, .{})); - try std.testing.expectEqualStrings(@typeName([]u8), status.expected_type.name); - const node = status.expected_type.node; - const main_tokens = ast.nodes.items(.main_token); - const token = main_tokens[node]; - const location = ast.tokenLocation(0, token); - try std.testing.expectEqual(Ast.Location{ - .line = 0, - .column = 0, - .line_start = 0, - .line_end = 6, - }, location); + { + var ast = try std.zig.Ast.parse(gpa, "\"abcd\"", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([]u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([]u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 6, + }, location); + } + + { + var ast = try std.zig.Ast.parse(gpa, "\\\\abcd", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([]u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([]u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 6, + }, location); + } + } + + // Passing string literal to a array + { + { + var ast = try std.zig.Ast.parse(gpa, "\"abcd\"", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([4:0]u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([4:0]u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 6, + }, location); + } + + { + var ast = try std.zig.Ast.parse(gpa, "\\\\abcd", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([4:0]u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([4:0]u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 6, + }, location); + } } // Zero termianted slices { - const parsed: [:0]const u8 = try parseFromSlice([:0]const u8, gpa, "\"abc\"", .{}); - defer parseFree(gpa, parsed); - try std.testing.expectEqualSlices(u8, "abc", parsed); - try std.testing.expectEqual(@as(u8, 0), parsed[3]); + { + const parsed: [:0]const u8 = try parseFromSlice([:0]const u8, gpa, "\"abc\"", .{}); + defer parseFree(gpa, parsed); + try std.testing.expectEqualStrings("abc", parsed); + try std.testing.expectEqual(@as(u8, 0), parsed[3]); + } + + { + const parsed: [:0]const u8 = try parseFromSlice([:0]const u8, gpa, "\\\\abc", .{}); + defer parseFree(gpa, parsed); + try std.testing.expectEqualStrings("abc", parsed); + try std.testing.expectEqual(@as(u8, 0), parsed[3]); + } } // Other value terminated slices { - var ast = try std.zig.Ast.parse(gpa, "\"foo\"", .zon); - defer ast.deinit(gpa); - var status: Status = .success; - try std.testing.expectError(error.Type, parseFromAst([:1]const u8, gpa, &ast, &status, .{})); - try std.testing.expectEqualStrings(@typeName([:1]const u8), status.expected_type.name); - const node = status.expected_type.node; - const main_tokens = ast.nodes.items(.main_token); - const token = main_tokens[node]; - const location = ast.tokenLocation(0, token); - try std.testing.expectEqual(Ast.Location{ - .line = 0, - .column = 0, - .line_start = 0, - .line_end = 5, - }, location); + { + var ast = try std.zig.Ast.parse(gpa, "\"foo\"", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([:1]const u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([:1]const u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 5, + }, location); + } + + { + var ast = try std.zig.Ast.parse(gpa, "\\\\foo", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([:1]const u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([:1]const u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 5, + }, location); + } } // Invalid string literal @@ -1495,49 +1655,115 @@ test "string literal" { // Slice wrong child type { - var ast = try std.zig.Ast.parse(gpa, "\"a\"", .zon); - defer ast.deinit(gpa); - var status: Status = .success; - try std.testing.expectError(error.Type, parseFromAst([]const i8, gpa, &ast, &status, .{})); - try std.testing.expectEqualStrings(@typeName([]const i8), status.expected_type.name); - const node = status.expected_type.node; - const main_tokens = ast.nodes.items(.main_token); - const token = main_tokens[node]; - const location = ast.tokenLocation(0, token); - try std.testing.expectEqual(Ast.Location{ - .line = 0, - .column = 0, - .line_start = 0, - .line_end = 3, - }, location); + { + var ast = try std.zig.Ast.parse(gpa, "\"a\"", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([]const i8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([]const i8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 3, + }, location); + } + + { + var ast = try std.zig.Ast.parse(gpa, "\\\\a", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([]const i8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([]const i8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 3, + }, location); + } } // Bad alignment { - var ast = try std.zig.Ast.parse(gpa, "\"abc\"", .zon); - defer ast.deinit(gpa); - var status: Status = .success; - try std.testing.expectError(error.Type, parseFromAst([]align(2) const u8, gpa, &ast, &status, .{})); - try std.testing.expectEqualStrings(@typeName([]align(2) const u8), status.expected_type.name); - const node = status.expected_type.node; - const main_tokens = ast.nodes.items(.main_token); - const token = main_tokens[node]; - const location = ast.tokenLocation(0, token); - try std.testing.expectEqual(Ast.Location{ - .line = 0, - .column = 0, - .line_start = 0, - .line_end = 5, - }, location); - } + { + var ast = try std.zig.Ast.parse(gpa, "\"abc\"", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([]align(2) const u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([]align(2) const u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 5, + }, location); + } - // TODO: ... - // // Multi line strins - // { - // const parsed = try parseFromSlice([]const u8, gpa, "\\foo\\bar", .{}); - // defer parseFree(gpa, parsed); - // try std.testing.expectEqualSlices(u8, "foo\nbar", parsed); - // } + { + var ast = try std.zig.Ast.parse(gpa, "\\\\abc", .zon); + defer ast.deinit(gpa); + var status: Status = .success; + try std.testing.expectError(error.Type, parseFromAst([]align(2) const u8, gpa, &ast, &status, .{})); + try std.testing.expectEqualStrings(@typeName([]align(2) const u8), status.expected_type.name); + const node = status.expected_type.node; + const main_tokens = ast.nodes.items(.main_token); + const token = main_tokens[node]; + const location = ast.tokenLocation(0, token); + try std.testing.expectEqual(Ast.Location{ + .line = 0, + .column = 0, + .line_start = 0, + .line_end = 5, + }, location); + } + } + + // Multi line strings + inline for (.{[]const u8, [:0]const u8}) |String| { + // Nested + { + const S = struct { + message: String, + message2: String, + message3: String, + }; + const parsed = try parseFromSlice(S, gpa, + \\.{ + \\ .message = + \\ \\hello, world! + \\ + \\ \\this is a multiline string! + \\ \\ + \\ \\... + \\ + \\ , + \\ .message2 = + \\ \\this too...sort of. + \\ , + \\ .message3 = + \\ \\ + \\ \\and this. + \\} + , .{}); + defer parseFree(gpa, parsed); + try std.testing.expectEqualStrings("hello, world!\nthis is a multiline string!\n\n...", parsed.message); + try std.testing.expectEqualStrings("this too...sort of.", parsed.message2); + try std.testing.expectEqualStrings("\nand this.", parsed.message3); + } + } } // TODO: cannot represent not quite right error for unknown field right? @@ -1638,7 +1864,7 @@ test "enum literals" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(Enum, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(Enum)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(Enum)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -1702,7 +1928,7 @@ test "@enumFromInt" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(Enum, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(Enum)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(Enum)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -1721,7 +1947,7 @@ test "@enumFromInt" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(Enum, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(Enum)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(Enum)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -1740,7 +1966,7 @@ test "@enumFromInt" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(Enum, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(Enum)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(Enum)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -1759,7 +1985,7 @@ test "@enumFromInt" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(Enum, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(Enum)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(Enum)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -1778,7 +2004,7 @@ test "@enumFromInt" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(Enum, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.unsupported_builtin.name, "@fooBarBaz"); + try std.testing.expectEqualStrings(status.unsupported_builtin.name, "@fooBarBaz"); const node = status.unsupported_builtin.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -1919,14 +2145,6 @@ fn failBadArgCount(self: *Parser, node: NodeIndex, expected: u8) error{Type} { } }); } -fn failToParseType(comptime T: type) noreturn { - @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"); -} - -fn failFreeType(comptime T: type) noreturn { - @compileError("Unable to free type '" ++ @typeName(T) ++ "'"); -} - fn failTypeExpr(self: *Parser, node: NodeIndex) error{Type} { @setCold(true); return self.fail(.{ .type_expr = .{ @@ -2239,7 +2457,7 @@ test "parse int" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(i66, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(i66)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(i66)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -2256,7 +2474,7 @@ test "parse int" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(i66, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(i66)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(i66)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -2384,7 +2602,7 @@ test "parse int" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(u8, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(u8)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(u8)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -2403,7 +2621,7 @@ test "parse int" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(i8, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(i8)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(i8)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -2422,7 +2640,7 @@ test "parse int" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(u8, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(u8)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(u8)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -2441,7 +2659,7 @@ test "parse int" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(u8, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(u8)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(u8)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -2460,7 +2678,7 @@ test "parse int" { defer ast.deinit(gpa); var status: Status = .success; try std.testing.expectError(error.Type, parseFromAst(u8, gpa, &ast, &status, .{})); - try std.testing.expectEqualSlices(u8, status.cannot_represent.name, @typeName(u8)); + try std.testing.expectEqualStrings(status.cannot_represent.name, @typeName(u8)); const node = status.cannot_represent.node; const main_tokens = ast.nodes.items(.main_token); const token = main_tokens[node]; @@ -2534,3 +2752,1604 @@ test "parse float" { // const parsed = try std.fmt.parseFloat(f32, "0xffffffffffffffff.0p0"); // try std.testing.expectEqual(float, parsed); // } + +pub const StringifierOptions = struct { + whitespace: bool = true, +}; + +pub const StringifyValueOptions = struct { + emit_utf8_codepoints: bool = false, + emit_strings_as_containers: bool = false, + emit_default_optional_fields: bool = true, +}; + +pub const StringifyOptions = struct { + whitespace: bool = true, + emit_utf8_codepoints: bool = false, + emit_strings_as_containers: bool = false, + emit_default_optional_fields: bool = true, +}; + +pub const StringifyContainerOptions = struct { + whitespace_style: union(enum) { + wrap: bool, + fields: usize, + } = .{ .wrap = true }, + + fn shouldWrap(self: StringifyContainerOptions) bool { + return switch (self.whitespace_style) { + .wrap => |wrap| wrap, + .fields => |fields| fields > 2, + }; + } +}; + +pub fn stringify(val: anytype, comptime options: StringifyOptions, writer: anytype) @TypeOf(writer).Error!void { + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{ + .whitespace = options.whitespace, + }); + try stringifier.value(val, .{ + .emit_utf8_codepoints = options.emit_utf8_codepoints, + .emit_strings_as_containers = options.emit_strings_as_containers, + .emit_default_optional_fields = options.emit_default_optional_fields, + }); +} + +pub fn stringifyMaxDepth(val: anytype, comptime options: StringifyOptions, writer: anytype, depth: usize) Stringifier(@TypeOf(writer)).MaxDepthError!void { + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{ + .whitespace = options.whitespace, + }); + try stringifier.valueMaxDepth(val, .{ + .emit_utf8_codepoints = options.emit_utf8_codepoints, + .emit_strings_as_containers = options.emit_strings_as_containers, + .emit_default_optional_fields = options.emit_default_optional_fields, + }, depth); +} + +pub fn stringifyUnchecked(val: anytype, comptime options: StringifyOptions, writer: anytype) @TypeOf(writer).Error!void { + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{ + .whitespace = options.whitespace, + }); + try stringifier.valueUnchecked(val, .{ + .emit_utf8_codepoints = options.emit_utf8_codepoints, + .emit_strings_as_containers = options.emit_strings_as_containers, + .emit_default_optional_fields = options.emit_default_optional_fields, + }); +} + +const RecursiveTypeBuffer = [32]type; + +fn typeIsRecursive(comptime T: type) bool { + comptime var buf: RecursiveTypeBuffer = undefined; + return typeIsRecursiveImpl(T, buf[0..0]); +} + +fn typeIsRecursiveImpl(comptime T: type, comptime visited_arg: []type) bool { + comptime var visited = visited_arg; + + // Check if we've already seen this type + inline for (visited) |found| { + if (T == found) { + return true; + } + } + + // Add this type to the stack + if (visited.len >= @typeInfo(RecursiveTypeBuffer).Array.len) { + @compileError("recursion limit"); + } + visited.ptr[visited.len] = T; + visited.len += 1; + + // Recurse + switch (@typeInfo(T)) { + .Pointer => |Pointer| return typeIsRecursiveImpl(Pointer.child, visited), + .Array => |Array| return typeIsRecursiveImpl(Array.child, visited), + .Struct => |Struct| inline for (Struct.fields) |field| { + if (typeIsRecursiveImpl(field.type, visited)) { + return true; + } + }, + .Union => |Union| inline for (Union.fields) |field| { + if (typeIsRecursiveImpl(field.type, visited)) { + return true; + } + }, + .Optional => |Optional| return typeIsRecursiveImpl(Optional.child, visited), + else => {}, + } + return false; +} + +test "typeIsRecursive" { + try std.testing.expect(!typeIsRecursive(bool)); + try std.testing.expect(!typeIsRecursive(struct { x: i32, y: i32 })); + try std.testing.expect(!typeIsRecursive(struct { i32, i32 })); + try std.testing.expect(typeIsRecursive(struct { x: i32, y: i32, z: *@This() })); + try std.testing.expect(typeIsRecursive(struct { + a: struct { + const A = @This(); + b: struct { + c: *struct { + a: ?A, + }, + }, + }, + })); + try std.testing.expect(typeIsRecursive(struct { + a: [3]*@This(), + })); + try std.testing.expect(typeIsRecursive(struct { + a: union { a: i32, b: *@This() }, + })); +} + +fn checkValueDepth(val: anytype, depth: usize) error { MaxDepth }!void { + if (depth == 0) return error.MaxDepth; + const child_depth = depth - 1; + + switch (@typeInfo(@TypeOf(val))) { + .Pointer => |Pointer| switch (Pointer.size) { + .One => try checkValueDepth(val.*, child_depth), + .Slice => for (val) |item| { + try checkValueDepth(item, child_depth); + }, + .C, .Many => {}, + }, + .Array => for (val) |item| { + try checkValueDepth(item, child_depth); + }, + .Struct => |Struct| inline for (Struct.fields) |field_info| { + try checkValueDepth(@field(val, field_info.name), child_depth); + }, + .Union => |Union| if (Union.tag_type == null) { + return; + } else switch (val) { + inline else => |payload| { + return checkValueDepth(payload, child_depth); + }, + }, + .Optional => if (val) |inner| try checkValueDepth(inner, child_depth), + else => {}, + } +} + +fn expectValueDepthEquals(expected: usize, value: anytype) !void { + try checkValueDepth(value, expected); + try std.testing.expectError(error.MaxDepth, checkValueDepth(value, expected - 1)); +} + +test "checkValueDepth" { + try expectValueDepthEquals(1, 10); + try expectValueDepthEquals(2, .{ .x = 1, .y = 2 }); + try expectValueDepthEquals(2, .{ 1, 2 }); + try expectValueDepthEquals(3, .{ 1, .{ 2, 3 } }); + try expectValueDepthEquals(3, .{ .{ 1, 2 }, 3 }); + try expectValueDepthEquals(3, .{ .x = 0, .y = 1, .z = .{ .x = 3 } }); + try expectValueDepthEquals(3, .{ .x = 0, .y = .{ .x = 1 }, .z = 2 }); + try expectValueDepthEquals(3, .{ .x = .{ .x = 0 }, .y = 1, .z = 2 }); + try expectValueDepthEquals(2, @as(?u32, 1)); + try expectValueDepthEquals(1, @as(?u32, null)); + try expectValueDepthEquals(1, null); + try expectValueDepthEquals(2, &1); + try expectValueDepthEquals(3, &@as(?u32, 1)); + + const Union = union(enum) { + x: u32, + y: struct { x: u32 }, + }; + try expectValueDepthEquals(2, Union{.x = 1}); + try expectValueDepthEquals(3, Union{.y = .{.x = 1 }}); + + const Recurse = struct { r: ?*const @This() }; + try expectValueDepthEquals(2, Recurse { .r = null }); + try expectValueDepthEquals(5, Recurse { .r = &Recurse { .r = null } }); + try expectValueDepthEquals(8, Recurse { .r = &Recurse { .r = &Recurse { .r = null }} }); + + try expectValueDepthEquals(2, @as([]const u8, &.{1, 2, 3})); + try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{1, 2, 3}})); +} + +pub fn Stringifier(comptime Writer: type) type { + return struct { + const Self = @This(); + + pub const MaxDepthError = error { MaxDepth } || Writer.Error; + + options: StringifierOptions, + indent_level: u8, + writer: Writer, + + pub fn init(writer: Writer, options: StringifierOptions) Self { + return .{ + .options = options, + .writer = writer, + .indent_level = 0, + }; + } + + pub fn value(self: *Self, val: anytype, options: StringifyValueOptions) Writer.Error!void { + comptimeAssertNoRecursion(@TypeOf(val)); + return self.valueUnchecked(val, options); + } + + pub fn valueMaxDepth(self: *Self, val: anytype, options: StringifyValueOptions, depth: usize) MaxDepthError!void { + try checkValueDepth(val, depth); + return self.valueUnchecked(val, options); + } + + pub fn valueUnchecked(self: *Self, val: anytype, options: StringifyValueOptions) Writer.Error!void { + switch (@typeInfo(@TypeOf(val))) { + .Int => |Int| if (options.emit_utf8_codepoints and + Int.signedness == .unsigned and + Int.bits <= 21 and std.unicode.utf8ValidCodepoint(val)) + { + self.utf8Codepoint(val) catch |err| switch (err) { + error.InvalidCodepoint => unreachable, + else => |e| return e, + }; + } else { + try self.int(val); + }, + .ComptimeInt => if (options.emit_utf8_codepoints and + val > 0 and + val <= std.math.maxInt(u21) and + std.unicode.utf8ValidCodepoint(val)) + { + self.utf8Codepoint(val) catch |err| switch (err) { + error.InvalidCodepoint => unreachable, + else => |e| return e, + }; + } else { + try self.int(val); + }, + .Float, .ComptimeFloat => try self.float(val), + .Bool, .Null => try std.fmt.format(self.writer, "{}", .{val}), + .EnumLiteral => { + try self.writer.writeByte('.'); + try self.ident(@tagName(val)); + }, + .Enum => |Enum| if (std.enums.tagName(@TypeOf(val), val)) |name| { + try self.writer.writeByte('.'); + try self.ident(name); + } else { + try self.int(@as(Enum.tag_type, @intFromEnum(val))); + }, + .Void => try self.writer.writeAll("{}"), + .Pointer => |Pointer| { + const child_type = switch (@typeInfo(Pointer.child)) { + .Array => |Array| Array.child, + else => if (Pointer.size != .Slice) @compileError(@typeName(@TypeOf(val)) ++ ": cannot stringify pointer to this type") else Pointer.child, + }; + if (child_type == u8 and !options.emit_strings_as_containers) { + try self.string(val); + } else { + try self.sliceImpl(val, options); + } + }, + .Array => { + var container = try self.startTuple(.{ .whitespace_style = .{ .fields = val.len } }); + for (val) |item_val| { + try container.fieldUnchecked(item_val, options); + } + try container.finish(); + }, + .Struct => |StructInfo| if (StructInfo.is_tuple) { + var container = try self.startTuple(.{ .whitespace_style = .{ .fields = StructInfo.fields.len } }); + inline for (val) |field_value| { + try container.fieldUnchecked(field_value, options); + } + try container.finish(); + } else { + // Decide which fields to emit + const fields, const skipped = if (options.emit_default_optional_fields) b: { + break :b .{ StructInfo.fields.len, [1]bool{false} ** StructInfo.fields.len}; + } else b: { + var fields = StructInfo.fields.len; + var skipped = [1]bool {false} ** StructInfo.fields.len; + inline for (StructInfo.fields, &skipped) |field_info, *skip| { + if (field_info.default_value) |default_field_value_opaque| { + const field_value = @field(val, field_info.name); + const default_field_value: *const @TypeOf(field_value) = @ptrCast(@alignCast(default_field_value_opaque)); + if (std.meta.eql(field_value, default_field_value.*)) { + skip.* = true; + fields -= 1; + } + } + } + break :b .{ fields, skipped }; + }; + + // Emit those fields + var container = try self.startStruct(.{ .whitespace_style = .{ .fields = fields } }); + inline for (StructInfo.fields, skipped) |field_info, skip| { + if (!skip) { + try container.fieldUnchecked(field_info.name, @field(val, field_info.name), options); + } + } + try container.finish(); + }, + .Union => |Union| if (Union.tag_type == null) { + @compileError(@typeName(@TypeOf(val)) ++ ": cannot stringify untagged unions"); + } else { + var container = try self.startStruct(.{ .whitespace_style = .{ .fields = 1 } }); + switch (val) { + inline else => |pl, tag| try container.fieldUnchecked(@tagName(tag), pl, options), + } + try container.finish(); + }, + .Optional => if (val) |inner| { + try self.valueUnchecked(inner, options); + } else { + try self.writer.writeAll("null"); + }, + + else => @compileError(@typeName(@TypeOf(val)) ++ ": cannot stringify this type"), + } + } + + pub fn int(self: *const Self, val: anytype) Writer.Error!void { + try std.fmt.formatInt(val, 10, .lower, .{}, self.writer); + } + + pub fn float(self: *const Self, val: anytype) Writer.Error!void { + try std.fmt.formatFloatDecimal(val, .{}, self.writer); + } + + fn identNeedsEscape(name: []const u8) bool { + std.debug.assert(name.len != 0); + for (name, 0..) |c, i| { + switch (c) { + 'A'...'Z', 'a'...'z', '_' => {}, + '0'...'9' => if (i == 0) return true, + else => return true, + } + } + return std.zig.Token.keywords.has(name); + } + + pub fn ident(self: *const Self, name: []const u8) Writer.Error!void { + if (identNeedsEscape(name)) { + try self.writer.writeAll("@\""); + try self.writer.writeAll(name); + try self.writer.writeByte('"'); + } else { + try self.writer.writeAll(name); + } + } + + pub fn utf8Codepoint(self: *const Self, val: u21) (Writer.Error || error{InvalidCodepoint})!void { + var buf: [8]u8 = undefined; + const len = std.unicode.utf8Encode(val, &buf) catch return error.InvalidCodepoint; + const str = buf[0..len]; + try std.fmt.format(self.writer, "'{'}'", .{std.zig.fmtEscapes(str)}); + } + + pub fn slice(self: *Self, val: anytype, options: StringifyValueOptions) Writer.Error!void { + comptimeAssertNoRecursion(@TypeOf(val)); + try self.sliceImpl(val, options); + } + + pub fn sliceDepthLimit(self: *Self, val: anytype, options: StringifyValueOptions, depth: usize) MaxDepthError!void { + try checkValueDepth(val, depth); + try self.sliceImpl(val, options, depth); + } + + fn sliceImpl(self: *Self, val: anytype, options: StringifyValueOptions) Writer.Error!void { + var container = try self.startSlice(.{ .whitespace_style = .{ .fields = val.len } }); + for (val) |item_val| { + try container.itemUnchecked(item_val, options); + } + try container.finish(); + } + + pub fn string(self: *const Self, val: []const u8) Writer.Error!void { + try std.fmt.format(self.writer, "\"{}\"", .{std.zig.fmtEscapes(val)}); + } + + pub fn startStruct(self: *Self, options: StringifyContainerOptions) Writer.Error!Struct { + return Struct.start(self, options); + } + + pub fn startTuple(self: *Self, options: StringifyContainerOptions) Writer.Error!Tuple { + return Tuple.start(self, options); + } + + pub fn startSlice(self: *Self, options: StringifyContainerOptions) Writer.Error!Slice { + return Slice.start(self, options); + } + + fn indent(self: *const Self) Writer.Error!void { + if (self.options.whitespace) { + try self.writer.writeByteNTimes(' ', 4 * self.indent_level); + } + } + + fn newline(self: *const Self) Writer.Error!void { + if (self.options.whitespace) { + try self.writer.writeByte('\n'); + } + } + + fn newlineOrSpace(self: *const Self, len: usize) Writer.Error!void { + if (self.containerShouldWrap(len)) { + try self.newline(); + } else { + try self.space(); + } + } + + fn space(self: *const Self) Writer.Error!void { + if (self.options.whitespace) { + try self.writer.writeByte(' '); + } + } + + pub const Tuple = struct { + container: Container, + + pub fn start(stringifier: *Self, options: StringifyContainerOptions) Writer.Error!Tuple { + return .{ + .container = try Container.start(stringifier, .anon, options), + }; + } + + pub fn finish(self: *Tuple) Writer.Error!void { + try self.container.finish(); + self.* = undefined; + } + + pub fn fieldPrefix(self: *Tuple) Writer.Error!void { + try self.container.fieldPrefix(null); + } + + pub fn field(self: *Tuple, val: anytype, options: StringifyValueOptions) Writer.Error!void { + try self.container.field(null, val, options); + } + + pub fn fieldMaxDepth(self: *Tuple, val: anytype, options: StringifyValueOptions, depth: usize) MaxDepthError!void { + try self.container.fieldMaxDepth(null, val, options, depth); + } + + pub fn fieldUnchecked(self: *Tuple, val: anytype, options: StringifyValueOptions) Writer.Error!void { + try self.container.fieldUnchecked(null, val, options); + } + }; + + pub const Struct = struct { + container: Container, + + pub fn start(stringifier: *Self, options: StringifyContainerOptions) Writer.Error!Struct { + return .{ + .container = try Container.start(stringifier, .named, options), + }; + } + + pub fn finish(self: *Struct) Writer.Error!void { + try self.container.finish(); + self.* = undefined; + } + + pub fn fieldPrefix(self: *Struct, name: []const u8) Writer.Error!void { + try self.container.fieldPrefix(name); + } + + pub fn field(self: *Struct, name: []const u8, val: anytype, options: StringifyValueOptions) Writer.Error!void { + try self.container.field(name, val, options); + } + + pub fn fieldMaxDepth(self: *Struct, name: []const u8, val: anytype, options: StringifyValueOptions, depth: usize) MaxDepthError!void { + try self.container.fieldMaxDepth(name, val, options, depth); + } + + pub fn fieldUnchecked(self: *Struct, name: []const u8, val: anytype, options: StringifyValueOptions) Writer.Error!void { + try self.container.fieldUnchecked(name, val, options); + } + }; + + pub const Slice = struct { + container: Container, + + pub fn start(stringifier: *Self, options: StringifyContainerOptions) Writer.Error!Slice { + try stringifier.writer.writeByte('&'); + return .{ + .container = try Container.start(stringifier, .anon, options), + }; + } + + pub fn finish(self: *Slice) Writer.Error!void { + try self.container.finish(); + self.* = undefined; + } + + pub fn itemPrefix(self: *Slice) Writer.Error!void { + try self.container.fieldPrefix(null); + } + + pub fn item(self: *Slice, val: anytype, options: StringifyValueOptions) Writer.Error!void { + try self.container.field(null, val, options); + } + + pub fn itemMaxDepth(self: *Slice, val: anytype, options: StringifyValueOptions, depth: usize) MaxDepthError!void { + try self.container.fieldMaxDepth(null, val, options, depth); + } + + pub fn itemUnchecked(self: *Slice, val: anytype, options: StringifyValueOptions) Writer.Error!void { + try self.container.fieldUnchecked(null, val, options); + } + }; + + const Container = struct { + const FieldStyle = enum { named, anon }; + + stringifier: *Self, + field_style: FieldStyle, + options: StringifyContainerOptions, + empty: bool, + + fn start(stringifier: *Self, field_style: FieldStyle, options: StringifyContainerOptions) Writer.Error!Container { + if (options.shouldWrap()) stringifier.indent_level +|= 1; + try stringifier.writer.writeAll(".{"); + return .{ + .stringifier = stringifier, + .field_style = field_style, + .options = options, + .empty = true, + }; + } + + fn finish(self: *Container) Writer.Error!void { + if (self.options.shouldWrap()) self.stringifier.indent_level -|= 1; + if (!self.empty) { + if (self.options.shouldWrap()) { + if (self.stringifier.options.whitespace) { + try self.stringifier.writer.writeByte(','); + } + try self.stringifier.newline(); + try self.stringifier.indent(); + } else if (!self.shouldElideSpaces()) { + try self.stringifier.space(); + } + } + try self.stringifier.writer.writeByte('}'); + self.* = undefined; + } + + fn fieldPrefix(self: *Container, name: ?[]const u8) Writer.Error!void { + if (!self.empty) { + try self.stringifier.writer.writeByte(','); + } + self.empty = false; + if (self.options.shouldWrap()) { + try self.stringifier.newline(); + } else if (!self.shouldElideSpaces()) { + try self.stringifier.space(); + } + if (self.options.shouldWrap()) try self.stringifier.indent(); + if (name) |n| { + try self.stringifier.writer.writeByte('.'); + try self.stringifier.ident(n); + try self.stringifier.space(); + try self.stringifier.writer.writeByte('='); + try self.stringifier.space(); + } + } + + fn field(self: *Container, name: ?[]const u8, val: anytype, options: StringifyValueOptions) Writer.Error!void { + comptimeAssertNoRecursion(@TypeOf(val)); + try self.fieldUnchecked(name, val, options); + } + + fn fieldMaxDepth(self: *Container, name: ?[]const u8, val: anytype, options: StringifyValueOptions, depth: usize) MaxDepthError!void { + try checkValueDepth(val, depth); + try self.fieldUnchecked(name, val, options); + } + + fn fieldUnchecked(self: *Container, name: ?[]const u8, val: anytype, options: StringifyValueOptions) Writer.Error!void { + try self.fieldPrefix(name); + try self.stringifier.valueUnchecked(val, options); + } + + fn shouldElideSpaces(self: *const Container) bool { + return switch (self.options.whitespace_style) { + .fields => |fields| self.field_style != .named and fields == 1, + else => false, + }; + } + }; + + fn comptimeAssertNoRecursion(comptime T: type) void { + if (comptime typeIsRecursive(T)) { + @compileError(@typeName(T) ++ ": recursive type stringified without depth limit"); + } + } + }; +} + +fn expectStringifyEqual(expected: []const u8, value: anytype, comptime options: StringifyOptions) !void { + var buf = std.ArrayList(u8).init(std.testing.allocator); + defer buf.deinit(); + try stringify(value, options, buf.writer()); + try std.testing.expectEqualStrings(expected, buf.items); +} + +test "stringify whitespace, high level API" { + try expectStringifyEqual(".{}", .{}, .{}); + try expectStringifyEqual(".{}", .{}, .{ .whitespace = false }); + + try expectStringifyEqual(".{1}", .{1}, .{}); + try expectStringifyEqual(".{1}", .{1}, .{ .whitespace = false }); + + try expectStringifyEqual(".{1}", @as([1]u32, .{1}), .{}); + try expectStringifyEqual(".{1}", @as([1]u32, .{1}), .{ .whitespace = false }); + + try expectStringifyEqual("&.{1}", @as([]const u32, &.{1}), .{}); + try expectStringifyEqual("&.{1}", @as([]const u32, &.{1}), .{ .whitespace = false }); + + try expectStringifyEqual(".{ .x = 1 }", .{ .x = 1 }, .{}); + try expectStringifyEqual(".{.x=1}", .{ .x = 1 }, .{ .whitespace = false }); + + try expectStringifyEqual(".{ 1, 2 }", .{ 1, 2 }, .{}); + try expectStringifyEqual(".{1,2}", .{ 1, 2 }, .{ .whitespace = false }); + + try expectStringifyEqual(".{ 1, 2 }", @as([2]u32, .{ 1, 2 }), .{}); + try expectStringifyEqual(".{1,2}", @as([2]u32, .{ 1, 2 }), .{ .whitespace = false }); + + try expectStringifyEqual("&.{ 1, 2 }", @as([]const u32, &.{ 1, 2 }), .{}); + try expectStringifyEqual("&.{1,2}", @as([]const u32, &.{ 1, 2 }), .{ .whitespace = false }); + + try expectStringifyEqual(".{ .x = 1, .y = 2 }", .{ .x = 1, .y = 2 }, .{}); + try expectStringifyEqual(".{.x=1,.y=2}", .{ .x = 1, .y = 2 }, .{ .whitespace = false }); + + try expectStringifyEqual( + \\.{ + \\ 1, + \\ 2, + \\ 3, + \\} + , .{ 1, 2, 3 }, .{}); + try expectStringifyEqual(".{1,2,3}", .{ 1, 2, 3 }, .{ .whitespace = false }); + + try expectStringifyEqual( + \\.{ + \\ 1, + \\ 2, + \\ 3, + \\} + , @as([3]u32, .{ 1, 2, 3 }), .{}); + try expectStringifyEqual(".{1,2,3}", @as([3]u32, .{ 1, 2, 3 }), .{ .whitespace = false }); + + try expectStringifyEqual( + \\&.{ + \\ 1, + \\ 2, + \\ 3, + \\} + , @as([]const u32, &.{ 1, 2, 3 }), .{}); + try expectStringifyEqual("&.{1,2,3}", @as([]const u32, &.{ 1, 2, 3 }), .{ .whitespace = false }); + + try expectStringifyEqual( + \\.{ + \\ .x = 1, + \\ .y = 2, + \\ .z = 3, + \\} + , .{ .x = 1, .y = 2, .z = 3 }, .{}); + try expectStringifyEqual(".{.x=1,.y=2,.z=3}", .{ .x = 1, .y = 2, .z = 3 }, .{ .whitespace = false }); + + const Union = union(enum) { a: bool, b: i32, c: u8 }; + + try expectStringifyEqual(".{ .b = 1 }", Union{ .b = 1 }, .{}); + try expectStringifyEqual(".{.b=1}", Union{ .b = 1 }, .{ .whitespace = false }); + + // Nested indentation where outer object doesn't wrap + try expectStringifyEqual( + \\.{ .inner = .{ + \\ 1, + \\ 2, + \\ 3, + \\} } + , .{ .inner = .{ 1, 2, 3 } }, .{}); +} + +test "stringify whitespace, low level API" { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + const writer = buffer.writer(); + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{}); + + inline for (.{ true, false }) |whitespace| { + stringifier.options = .{ .whitespace = whitespace }; + + // Empty containers + { + var container = try stringifier.startStruct(.{}); + try container.finish(); + try std.testing.expectEqualStrings(".{}", buffer.items); + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{}); + try container.finish(); + try std.testing.expectEqualStrings(".{}", buffer.items); + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .wrap = false } }); + try container.finish(); + try std.testing.expectEqualStrings(".{}", buffer.items); + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .wrap = false } }); + try container.finish(); + try std.testing.expectEqualStrings(".{}", buffer.items); + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .fields = 0 } }); + try container.finish(); + try std.testing.expectEqualStrings(".{}", buffer.items); + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .fields = 0 } }); + try container.finish(); + try std.testing.expectEqualStrings(".{}", buffer.items); + buffer.clearRetainingCapacity(); + } + + // Size 1 + { + var container = try stringifier.startStruct(.{}); + try container.field("a", 1, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ .a = 1, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{}); + try container.field(1, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ 1, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{1}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .wrap = false } }); + try container.field("a", 1, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ .a = 1 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + // We get extra spaces here, since we didn't know up front that there would only be one + // field. + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .wrap = false } }); + try container.field(1, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ 1 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{1}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .fields = 1 } }); + try container.field("a", 1, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ .a = 1 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .fields = 1 } }); + try container.field(1, .{}); + try container.finish(); + try std.testing.expectEqualStrings(".{1}", buffer.items); + buffer.clearRetainingCapacity(); + } + + // Size 2 + { + var container = try stringifier.startStruct(.{}); + try container.field("a", 1, .{}); + try container.field("b", 2, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ .a = 1, + \\ .b = 2, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1,.b=2}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{}); + try container.field(1, .{}); + try container.field(2, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ 1, + \\ 2, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{1,2}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .wrap = false } }); + try container.field("a", 1, .{}); + try container.field("b", 2, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ .a = 1, .b = 2 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1,.b=2}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .wrap = false } }); + try container.field(1, .{}); + try container.field(2, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ 1, 2 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{1,2}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .fields = 2 } }); + try container.field("a", 1, .{}); + try container.field("b", 2, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ .a = 1, .b = 2 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1,.b=2}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .fields = 2 } }); + try container.field(1, .{}); + try container.field(2, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ 1, 2 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{1,2}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + // Size 3 + { + var container = try stringifier.startStruct(.{}); + try container.field("a", 1, .{}); + try container.field("b", 2, .{}); + try container.field("c", 3, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ .a = 1, + \\ .b = 2, + \\ .c = 3, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{}); + try container.field(1, .{}); + try container.field(2, .{}); + try container.field(3, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ 1, + \\ 2, + \\ 3, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{1,2,3}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .wrap = false } }); + try container.field("a", 1, .{}); + try container.field("b", 2, .{}); + try container.field("c", 3, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ .a = 1, .b = 2, .c = 3 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .wrap = false } }); + try container.field(1, .{}); + try container.field(2, .{}); + try container.field(3, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings(".{ 1, 2, 3 }", buffer.items); + } else { + try std.testing.expectEqualStrings(".{1,2,3}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .fields = 3 } }); + try container.field("a", 1, .{}); + try container.field("b", 2, .{}); + try container.field("c", 3, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ .a = 1, + \\ .b = 2, + \\ .c = 3, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + { + var container = try stringifier.startTuple(.{ .whitespace_style = .{ .fields = 3 } }); + try container.field(1, .{}); + try container.field(2, .{}); + try container.field(3, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ + \\ 1, + \\ 2, + \\ 3, + \\} + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{1,2,3}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + + // Nested objects where the outer container doesn't wrap but the inner containers do + { + var container = try stringifier.startStruct(.{ .whitespace_style = .{ .wrap = false } }); + try container.field("first", .{ 1, 2, 3 }, .{}); + try container.field("second", .{ 4, 5, 6 }, .{}); + try container.finish(); + if (whitespace) { + try std.testing.expectEqualStrings( + \\.{ .first = .{ + \\ 1, + \\ 2, + \\ 3, + \\}, .second = .{ + \\ 4, + \\ 5, + \\ 6, + \\} } + , buffer.items); + } else { + try std.testing.expectEqualStrings(".{.first=.{1,2,3},.second=.{4,5,6}}", buffer.items); + } + buffer.clearRetainingCapacity(); + } + } +} + +test "stringify utf8 codepoints" { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + const writer = buffer.writer(); + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{}); + + // Minimal case + try stringifier.utf8Codepoint('a'); + try std.testing.expectEqualStrings("'a'", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.int('a'); + try std.testing.expectEqualStrings("97", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value('a', .{ .emit_utf8_codepoints = true }); + try std.testing.expectEqualStrings("'a'", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value('a', .{ .emit_utf8_codepoints = false }); + try std.testing.expectEqualStrings("97", buffer.items); + buffer.clearRetainingCapacity(); + + // Short escaped codepoint + try stringifier.utf8Codepoint('\n'); + try std.testing.expectEqualStrings("'\\n'", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.int('\n'); + try std.testing.expectEqualStrings("10", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value('\n', .{ .emit_utf8_codepoints = true }); + try std.testing.expectEqualStrings("'\\n'", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value('\n', .{ .emit_utf8_codepoints = false }); + try std.testing.expectEqualStrings("10", buffer.items); + buffer.clearRetainingCapacity(); + + // Large codepoint + try stringifier.utf8Codepoint('⚡'); + try std.testing.expectEqualStrings("'\\xe2\\x9a\\xa1'", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.int('⚡'); + try std.testing.expectEqualStrings("9889", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value('⚡', .{ .emit_utf8_codepoints = true }); + try std.testing.expectEqualStrings("'\\xe2\\x9a\\xa1'", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value('⚡', .{ .emit_utf8_codepoints = false }); + try std.testing.expectEqualStrings("9889", buffer.items); + buffer.clearRetainingCapacity(); + + // Invalid codepoint + try std.testing.expectError(error.InvalidCodepoint, stringifier.utf8Codepoint(0x110000 + 1)); + + try stringifier.int(0x110000 + 1); + try std.testing.expectEqualStrings("1114113", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value(0x110000 + 1, .{ .emit_utf8_codepoints = true }); + try std.testing.expectEqualStrings("1114113", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value(0x110000 + 1, .{ .emit_utf8_codepoints = false }); + try std.testing.expectEqualStrings("1114113", buffer.items); + buffer.clearRetainingCapacity(); + + // Valid codepoint, not a codepoint type + try stringifier.value(@as(u22, 'a'), .{ .emit_utf8_codepoints = true }); + try std.testing.expectEqualStrings("97", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value(@as(i32, 'a'), .{ .emit_utf8_codepoints = false }); + try std.testing.expectEqualStrings("97", buffer.items); + buffer.clearRetainingCapacity(); + + // Make sure value options are passed to children + try stringifier.value(.{ .c = '⚡' }, .{ .emit_utf8_codepoints = true }); + try std.testing.expectEqualStrings(".{ .c = '\\xe2\\x9a\\xa1' }", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value(.{ .c = '⚡' }, .{ .emit_utf8_codepoints = false }); + try std.testing.expectEqualStrings(".{ .c = 9889 }", buffer.items); + buffer.clearRetainingCapacity(); +} + +test "stringify strings" { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + const writer = buffer.writer(); + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{}); + + // Minimal case + try stringifier.string("abc⚡\n"); + try std.testing.expectEqualStrings("\"abc\\xe2\\x9a\\xa1\\n\"", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.slice("abc⚡\n", .{}); + try std.testing.expectEqualStrings( + \\&.{ + \\ 97, + \\ 98, + \\ 99, + \\ 226, + \\ 154, + \\ 161, + \\ 10, + \\} + , buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value("abc⚡\n", .{}); + try std.testing.expectEqualStrings("\"abc\\xe2\\x9a\\xa1\\n\"", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value("abc⚡\n", .{ .emit_strings_as_containers = true }); + try std.testing.expectEqualStrings( + \\&.{ + \\ 97, + \\ 98, + \\ 99, + \\ 226, + \\ 154, + \\ 161, + \\ 10, + \\} + , buffer.items); + buffer.clearRetainingCapacity(); + + // Value options are inherited by children + try stringifier.value(.{ .str = "abc" }, .{}); + try std.testing.expectEqualStrings(".{ .str = \"abc\" }", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.value(.{ .str = "abc" }, .{ .emit_strings_as_containers = true }); + try std.testing.expectEqualStrings( + \\.{ .str = &.{ + \\ 97, + \\ 98, + \\ 99, + \\} } + , buffer.items); + buffer.clearRetainingCapacity(); + + // Arrays (rather than pointers to arrays) of u8s are not considered strings, so that data can round trip + // correctly. + try stringifier.value("abc".*, .{ .emit_strings_as_containers = false }); + try std.testing.expectEqualStrings( + \\.{ + \\ 97, + \\ 98, + \\ 99, + \\} + , buffer.items); + buffer.clearRetainingCapacity(); +} + +test "stringify skip default fields" { + const Struct = struct { + x: i32 = 2, + y: i8, + z: u32 = 4, + inner1: struct { a: u8 = 'z', b: u8 = 'y', c: u8 } = .{ + .a = '1', + .b = '2', + .c = '3', + }, + inner2: struct { u8, u8, u8 } = .{ + 'a', + 'b', + 'c', + }, + inner3: struct { u8, u8, u8 } = .{ + 'a', + 'b', + 'c', + }, + }; + + // Not skipping if not set + try expectStringifyEqual( + \\.{ + \\ .x = 2, + \\ .y = 3, + \\ .z = 4, + \\ .inner1 = .{ + \\ .a = '1', + \\ .b = '2', + \\ .c = '3', + \\ }, + \\ .inner2 = .{ + \\ 'a', + \\ 'b', + \\ 'c', + \\ }, + \\ .inner3 = .{ + \\ 'a', + \\ 'b', + \\ 'd', + \\ }, + \\} + , Struct{ + .y = 3, + .z = 4, + .inner1 = .{ + .a = '1', + .b = '2', + .c = '3', + }, + .inner3 = .{ + 'a', + 'b', + 'd', + }, + }, + .{ .emit_utf8_codepoints = true }, + ); + + // Top level defaults + try expectStringifyEqual( + \\.{ .y = 3, .inner3 = .{ + \\ 'a', + \\ 'b', + \\ 'd', + \\} } + , Struct{ + .y = 3, + .z = 4, + .inner1 = .{ + .a = '1', + .b = '2', + .c = '3', + }, + .inner3 = .{ + 'a', + 'b', + 'd', + }, + }, + .{ + .emit_default_optional_fields = false, + .emit_utf8_codepoints = true, + }, + ); + + // Inner types having defaults, and defaults changing the number of fields affecting the formatting + try expectStringifyEqual( + \\.{ + \\ .y = 3, + \\ .inner1 = .{ .b = '2', .c = '3' }, + \\ .inner3 = .{ + \\ 'a', + \\ 'b', + \\ 'd', + \\ }, + \\} + , Struct{ + .y = 3, + .z = 4, + .inner1 = .{ + .a = 'z', + .b = '2', + .c = '3', + }, + .inner3 = .{ + 'a', + 'b', + 'd', + }, + }, + .{ + .emit_default_optional_fields = false, + .emit_utf8_codepoints = true, + }, + ); +} + +test "depth limits" { + var buf = std.ArrayList(u8).init(std.testing.allocator); + defer buf.deinit(); + + const Recurse = struct { r: []const @This() }; + + // Normal operation + try stringifyMaxDepth(.{ 1, .{ 2, 3 } }, .{}, buf.writer(), 16); + try std.testing.expectEqualStrings(".{ 1, .{ 2, 3 } }", buf.items); + buf.clearRetainingCapacity(); + + try stringifyUnchecked(.{ 1, .{ 2, 3 } }, .{}, buf.writer()); + try std.testing.expectEqualStrings(".{ 1, .{ 2, 3 } }", buf.items); + buf.clearRetainingCapacity(); + + // Max depth failing on non recursive type + try std.testing.expectError(error.MaxDepth, stringifyMaxDepth(.{ 1, .{ 2, .{ 3, 4 } } }, .{}, buf.writer(), 3)); + try std.testing.expectEqualStrings("", buf.items); + buf.clearRetainingCapacity(); + + // Max depth passing on recursive type + { + const maybe_recurse = Recurse { .r = &.{} }; + try stringifyMaxDepth(maybe_recurse, .{}, buf.writer(), 2); + try std.testing.expectEqualStrings(".{ .r = &.{} }", buf.items); + buf.clearRetainingCapacity(); + } + + // Unchecked passing on recursive type + { + const maybe_recurse = Recurse { .r = &.{} }; + try stringifyUnchecked(maybe_recurse, .{}, buf.writer()); + try std.testing.expectEqualStrings(".{ .r = &.{} }", buf.items); + buf.clearRetainingCapacity(); + } + + // Max depth failing on recursive type due to depth + { + var maybe_recurse = Recurse { .r = &.{} }; + maybe_recurse.r = &.{ .{ .r = &.{} } }; + try std.testing.expectError(error.MaxDepth, stringifyMaxDepth(maybe_recurse, .{}, buf.writer(), 2)); + try std.testing.expectEqualStrings("", buf.items); + buf.clearRetainingCapacity(); + } + + // Same but for a slice + { + var temp: [1]Recurse = .{ .{ .r = &.{} } }; + const maybe_recurse: []const Recurse = &temp; + + try std.testing.expectError(error.MaxDepth, stringifyMaxDepth(maybe_recurse, .{}, buf.writer(), 2)); + try std.testing.expectEqualStrings("", buf.items); + buf.clearRetainingCapacity(); + } + + // A slice succeeding + { + var temp: [1]Recurse = .{ .{ .r = &.{} } }; + const maybe_recurse: []const Recurse = &temp; + + try stringifyMaxDepth(maybe_recurse, .{}, buf.writer(), 3); + try std.testing.expectEqualStrings("&.{.{ .r = &.{} }}", buf.items); + buf.clearRetainingCapacity(); + } + + // Max depth failing on recursive type due to recursion + { + var temp: [1]Recurse = .{ .{ .r = &.{} } }; + temp[0].r = &temp; + const maybe_recurse: []const Recurse = &temp; + + try std.testing.expectError(error.MaxDepth, stringifyMaxDepth(maybe_recurse, .{}, buf.writer(), 128)); + try std.testing.expectEqualStrings("", buf.items); + buf.clearRetainingCapacity(); + } + + // Max depth on the lower level API + { + const writer = buf.writer(); + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{}); + + const maybe_recurse: []const Recurse = &.{}; + + try std.testing.expectError(error.MaxDepth, stringifier.valueMaxDepth(1, .{}, 0)); + try stringifier.valueMaxDepth(2, .{}, 1); + try stringifier.value(3, .{}); + try stringifier.valueUnchecked(maybe_recurse, .{}); + + var s = try stringifier.startStruct(.{}); + try std.testing.expectError(error.MaxDepth, s.fieldMaxDepth("a", 1, .{}, 0)); + try s.fieldMaxDepth("b", 4, .{}, 1); + try s.field("c", 5, .{}); + try s.fieldUnchecked("d", maybe_recurse, .{}); + try s.finish(); + + var t = try stringifier.startTuple(.{}); + try std.testing.expectError(error.MaxDepth, t.fieldMaxDepth(1, .{}, 0)); + try t.fieldMaxDepth(6, .{}, 1); + try t.field(7, .{}); + try t.fieldUnchecked(maybe_recurse, .{}); + try t.finish(); + + var a = try stringifier.startSlice(.{}); + try std.testing.expectError(error.MaxDepth, a.itemMaxDepth(1, .{}, 0)); + try a.itemMaxDepth(8, .{}, 1); + try a.item(9, .{}); + try a.itemUnchecked(maybe_recurse, .{}); + try a.finish(); + + try std.testing.expectEqualStrings( + \\23&.{}.{ + \\ .b = 4, + \\ .c = 5, + \\ .d = &.{}, + \\}.{ + \\ 6, + \\ 7, + \\ &.{}, + \\}&.{ + \\ 8, + \\ 9, + \\ &.{}, + \\} + , buf.items); + } +} + +test "stringify primitives" { + try expectStringifyEqual( + \\.{ + \\ .a = 1.5, + \\ .b = 0.3333333333333333, + \\ .c = 3.141592653589793, + \\ .d = 0, + \\ .e = -0, + \\ .f = inf, + \\ .g = -inf, + \\ .h = nan, + \\} + , .{ + .a = 1.5, + .b = 1.0 / 3.0, + .c = std.math.pi, + .d = 0.0, + .e = -0.0, + .f = std.math.inf(f32), + .g = -std.math.inf(f32), + .h = std.math.nan(f32), + }, + .{}, + ); + + try expectStringifyEqual( + \\.{ + \\ .a = 18446744073709551616, + \\ .b = -18446744073709551616, + \\ .c = 680564733841876926926749214863536422912, + \\ .d = -680564733841876926926749214863536422912, + \\ .e = 0, + \\} + , .{ + .a = 18446744073709551616, + .b = -18446744073709551616, + .c = 680564733841876926926749214863536422912, + .d = -680564733841876926926749214863536422912, + .e = 0, + }, + .{}, + ); + + try expectStringifyEqual( + \\.{ + \\ .a = true, + \\ .b = false, + \\ .c = .foo, + \\ .d = {}, + \\ .e = null, + \\} + , .{ + .a = true, + .b = false, + .c = .foo, + .d = {}, + .e = null, + }, + .{}, + ); + + const Struct = struct { x: f32, y: f32 }; + try expectStringifyEqual( + ".{ .a = .{ .x = 1, .y = 2 }, .b = null }" + , .{ + .a = @as(?Struct, .{ .x = 1, .y = 2 }), + .b = @as(?Struct, null), + }, + .{}, + ); + + const E = enum (u8) { + foo, + bar, + _ + }; + try expectStringifyEqual( + \\.{ + \\ .a = .foo, + \\ .b = .foo, + \\ .c = 10, + \\} + , .{ + .a = .foo, + .b = E.foo, + .c = @as(E, @enumFromInt(10)), + }, + .{}, + ); +} + +test "stringify ident" { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + const writer = buffer.writer(); + var stringifier = Stringifier(@TypeOf(writer)).init(writer, .{}); + + try stringifier.ident("a"); + try std.testing.expectEqualStrings("a", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.ident("foo_1"); + try std.testing.expectEqualStrings("foo_1", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.ident("_foo_1"); + try std.testing.expectEqualStrings("_foo_1", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.ident("foo bar"); + try std.testing.expectEqualStrings("@\"foo bar\"", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.ident("1foo"); + try std.testing.expectEqualStrings("@\"1foo\"", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.ident("var"); + try std.testing.expectEqualStrings("@\"var\"", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.ident("true"); + try std.testing.expectEqualStrings("true", buffer.items); + buffer.clearRetainingCapacity(); + + try stringifier.ident("_"); + try std.testing.expectEqualStrings("_", buffer.items); + buffer.clearRetainingCapacity(); + + const Enum = enum { + @"foo bar", + }; + try expectStringifyEqual(".{ .@\"var\" = .@\"foo bar\", .@\"1\" = .@\"foo bar\" }", .{ + .@"var" = .@"foo bar", + .@"1" = Enum.@"foo bar", + }, .{}); +} diff --git a/src/AstGen.zig b/src/AstGen.zig index e6bd5c5b3e9c..f11513bdfab9 100644 --- a/src/AstGen.zig +++ b/src/AstGen.zig @@ -10828,34 +10828,17 @@ fn strLitAsString(astgen: *AstGen, str_lit_token: Ast.TokenIndex) !IndexSlice { } fn strLitNodeAsString(astgen: *AstGen, node: Ast.Node.Index) !IndexSlice { - const tree = astgen.tree; - const node_datas = tree.nodes.items(.data); - - const start = node_datas[node].lhs; - const end = node_datas[node].rhs; - const gpa = astgen.gpa; + const data = astgen.tree.nodes.items(.data); const string_bytes = &astgen.string_bytes; const str_index = string_bytes.items.len; - // First line: do not append a newline. - var tok_i = start; - { - const slice = tree.tokenSlice(tok_i); - const carriage_return_ending: usize = if (slice[slice.len - 2] == '\r') 2 else 1; - const line_bytes = slice[2 .. slice.len - carriage_return_ending]; - try string_bytes.appendSlice(gpa, line_bytes); - tok_i += 1; - } - // Following lines: each line prepends a newline. - while (tok_i <= end) : (tok_i += 1) { - const slice = tree.tokenSlice(tok_i); - const carriage_return_ending: usize = if (slice[slice.len - 2] == '\r') 2 else 1; - const line_bytes = slice[2 .. slice.len - carriage_return_ending]; - try string_bytes.ensureUnusedCapacity(gpa, line_bytes.len + 1); - string_bytes.appendAssumeCapacity('\n'); - string_bytes.appendSliceAssumeCapacity(line_bytes); + var parser = std.zig.string_literal.multilineParser(string_bytes.writer(gpa)); + var tok_i = data[node].lhs; + while (tok_i <= data[node].rhs) : (tok_i += 1) { + try parser.line(astgen.tree.tokenSlice(tok_i)); } + const len = string_bytes.items.len - str_index; try string_bytes.append(gpa, 0); return IndexSlice{ diff --git a/src/Module.zig b/src/Module.zig index 448b3632a50e..b9e7b29d721d 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -3737,9 +3737,9 @@ const LowerZon = struct { const gpa = self.mod.gpa; const data = self.file.tree.nodes.items(.data); const tags = self.file.tree.nodes.items(.tag); + const main_tokens = self.file.tree.nodes.items(.main_token); switch (tags[node]) { .identifier => { - const main_tokens = self.file.tree.nodes.items(.main_token); const token = main_tokens[node]; const bytes = self.file.tree.tokenSlice(token); // XXX: make comptime string map or something? @@ -3757,7 +3757,6 @@ const LowerZon = struct { .negation => return self.number(data[node].lhs, true), // XXX: make sure works with @""! .enum_literal => { - const main_tokens = self.file.tree.nodes.items(.main_token); const token = main_tokens[node]; const bytes = self.file.tree.tokenSlice(token); return self.mod.intern_pool.get(gpa, .{ @@ -3765,24 +3764,25 @@ const LowerZon = struct { }); }, .string_literal => { - const main_tokens = self.file.tree.nodes.items(.main_token); const token = main_tokens[node]; const raw_string = self.file.tree.tokenSlice(token); var bytes = std.ArrayListUnmanaged(u8){}; defer bytes.deinit(gpa); - const offset = self.file.tree.tokens.items(.start)[token]; switch (try std.zig.string_literal.parseWrite(bytes.writer(gpa), raw_string)) { .success => {}, - .failure => |err| return AstGen.failWithStrLitError( - self, - failWithStrLitError, - err, - token, - raw_string, - offset, - ), + .failure => |err| { + const offset = self.file.tree.tokens.items(.start)[token]; + return AstGen.failWithStrLitError( + self, + failWithStrLitError, + err, + token, + raw_string, + offset, + ); + } } const array_ty = try self.mod.arrayType(.{ @@ -3807,6 +3807,41 @@ const LowerZon = struct { .addr = .{ .anon_decl = .{ .val = val, .orig_ty = ptr_ty } }, } }); }, + .multiline_string_literal => { + var string_bytes = std.ArrayListUnmanaged(u8){}; + defer string_bytes.deinit(gpa); + + var parser = std.zig.string_literal.multilineParser(string_bytes.writer(gpa)); + var tok_i = data[node].lhs; + while (tok_i <= data[node].rhs) : (tok_i += 1) { + try parser.line(self.file.tree.tokenSlice(tok_i)); + } + + const len = string_bytes.items.len; + try string_bytes.append(gpa, 0); + + const array_ty = try self.mod.arrayType(.{ + .len = len, + .sentinel = .zero_u8, + .child = .u8_type + }); + const val = try self.mod.intern(.{ .aggregate = .{ + .ty = array_ty.toIntern(), + .storage = .{ .bytes = string_bytes.items }, + } }); + const ptr_ty = (try self.mod.ptrType(.{ + .child = array_ty.toIntern(), + .flags = .{ + .alignment = .none, + .is_const = true, + .address_space = .generic, + }, + })).toIntern(); + return try self.mod.intern(.{ .ptr = .{ + .ty = ptr_ty, + .addr = .{ .anon_decl = .{ .val = val, .orig_ty = ptr_ty } }, + } }); + }, // XXX: enforce no named structs // XXX: does zig support any kind of reepated array syntax or is that just mul? do we support that in zon? // XXX: am i supposed to special case empty struct? @@ -3961,9 +3996,9 @@ const LowerZon = struct { fn number(self: *LowerZon, node: Ast.Node.Index, is_negative: bool) !InternPool.Index { const gpa = self.mod.gpa; const tags = self.file.tree.nodes.items(.tag); + const main_tokens = self.file.tree.nodes.items(.main_token); switch (tags[node]) { .char_literal => { - const main_tokens = self.file.tree.nodes.items(.main_token); const token = main_tokens[node]; const token_bytes = self.file.tree.tokenSlice(token); const char = switch (std.zig.string_literal.parseCharLiteral(token_bytes)) { @@ -3983,7 +4018,6 @@ const LowerZon = struct { }}); }, .number_literal => { - const main_tokens = self.file.tree.nodes.items(.main_token); const token = main_tokens[node]; const token_bytes = self.file.tree.tokenSlice(token); const parsed = std.zig.number_literal.parseNumberLiteral(token_bytes); diff --git a/test/behavior/zon.zig b/test/behavior/zon.zig index 44550871d295..4cd4cd66298a 100644 --- a/test/behavior/zon.zig +++ b/test/behavior/zon.zig @@ -3,6 +3,7 @@ const std = @import("std"); const expectEqual = std.testing.expectEqual; const expectEqualDeep = std.testing.expectEqualDeep; const expectEqualSlices = std.testing.expectEqualSlices; +const expectEqualStrings = std.testing.expectEqualStrings; test "void" { try expectEqual({}, @import("zon/void.zon")); @@ -17,8 +18,10 @@ test "optional" { // Coercion const some: ?u32 = @import("zon/some.zon"); const none: ?u32 = @import("zon/none.zon"); + const @"null": @TypeOf(null) = @import("zon/none.zon"); try expectEqual(some, 10); try expectEqual(none, null); + try expectEqual(@"null", null); // No coercion try expectEqual(some, @import("zon/some.zon")); @@ -86,6 +89,11 @@ test "string literals" { try expectEqualDeep("ab\\c", @import("zon/abc-escaped.zon")); const zero_terminated: [:0]const u8 = @import("zon/abc.zon"); try expectEqualDeep(zero_terminated, "abc"); + try expectEqualStrings( + \\Hello, world! + \\This is a multiline string! + \\ There are no escapes, we can, for example, include \n in the string + , @import("zon/multiline_string.zon")); } test "enum literals" { diff --git a/test/behavior/zon/false.zon b/test/behavior/zon/false.zon index c508d5366f70..0064d7bc7d22 100644 --- a/test/behavior/zon/false.zon +++ b/test/behavior/zon/false.zon @@ -1 +1,4 @@ -false +// Comment +false // Another comment +// Yet another comment + diff --git a/test/behavior/zon/multiline_string.zon b/test/behavior/zon/multiline_string.zon new file mode 100644 index 000000000000..5908802ecc65 --- /dev/null +++ b/test/behavior/zon/multiline_string.zon @@ -0,0 +1,4 @@ +// zig fmt: off + \\Hello, world! +\\This is a multiline string! + \\ There are no escapes, we can, for example, include \n in the string