const std = @import("std"); const mem = std.mem; const Language = @import("language.zig"); const Tokenizer = @import("tokenizer.zig"); const assert = std.debug.assert; const Self = @This(); pub const Error = error{TypeError}; language: *Language, tokenizer: *Tokenizer, pub fn parse(self: *Self, allocator: mem.Allocator) !usize { return self.language.parse(allocator, self.tokenizer); } pub fn init(allocator: mem.Allocator, text: []const u8) !Self { const self = Self{ .language = try allocator.create(Language), .tokenizer = try allocator.create(Tokenizer), }; self.language.* = .init; self.tokenizer.* = try .init(allocator, text); return self; } pub fn deinit(self: *Self, allocator: mem.Allocator) void { self.language.deinit(allocator); self.tokenizer.deinit(allocator); allocator.destroy(self.language); allocator.destroy(self.tokenizer); } /// needs an index but useful otherwise /// root starting from idx pub fn reflectT(self: *Self, comptime T: type, allocator: mem.Allocator, idx: usize) !T { const Schema = @typeInfo(T); if (std.meta.hasFn(T, "toJson")) { return T.toJson(self, allocator, idx); } switch (self.language.index.get(idx)) { .null => { if (Schema == .null or Schema == .optional) return null; }, .bool => |b| switch (Schema) { .bool => return b, .@"union" => |unionInfo| inline for (unionInfo.fields) |field| { var r: T = undefined; r = @unionInit(T, field.name, b); return r; }, else => unreachable, }, .number => |number| switch (Schema) { .int, .comptime_int => return @intCast(number.int), .float, .comptime_float => return switch (number) { .float => @floatCast(number.float), .int => @floatFromInt(number.int), }, .@"enum" => |enumInfo| { const int: enumInfo.tag_type = @intCast(number.int); return @enumFromInt(int); }, .@"struct" => |structInfo| switch (structInfo.layout) { .@"packed" => { const int: structInfo.backing_integer.? = @intCast(number.int); return @bitCast(int); }, else => unreachable, // may only cast packed structs }, .@"union" => |unionInfo| { inline for (unionInfo.fields) |field| switch (@typeInfo(field.type)) { .int, .comptime_int => return @unionInit(T, field.name, @intCast(number.int)), .float, .comptime_float => return @unionInit(T, field.name, @floatCast(number.float)), else => {}, }; return error.TypeError; }, else => unreachable, }, .string => |string| switch (Schema) { .@"enum" => |enumInfo| { const strslice = string.slice(&self.language.strings); inline for (enumInfo.fields) |field| if (mem.eql(u8, field.name, strslice)) { return std.meta.stringToEnum(T, strslice) orelse error.TypeError; }; }, .@"union" => |unionInfo| inline for (unionInfo.fields) |field| { if (field.type == []const u8) { var r: T = undefined; const strslice = string.slice(&self.language.strings); r = @unionInit(T, field.name, strslice); return r; } }, .array => |arrayInfo| { assert(arrayInfo.child == u8); const strslice = string.slice(&self.language.strings); assert(arrayInfo.len == strslice.len - 1); var r: T = undefined; for (strslice, 0..) |char, i| r[i] = char; return r; }, .pointer => |ptrInfo| switch (ptrInfo.size) { .slice => { assert(ptrInfo.child == u8); const strslice = string.slice(&self.language.strings); var arraylist: std.ArrayList(u8) = .init(allocator); try arraylist.ensureUnusedCapacity(strslice.len); for (strslice) |char| if (char != 0x00) arraylist.appendAssumeCapacity(char); if (ptrInfo.sentinel_ptr) |some| { const sentinel = @as(*align(1) const ptrInfo.child, @ptrCast(some)).*; return try arraylist.toOwnedSliceSentinel(sentinel); } if (ptrInfo.is_const) { arraylist.deinit(); return strslice; } else { arraylist.deinit(); const slice = try allocator.dupe(u8, strslice); return @as(T, slice); } return try arraylist.toOwnedSlice(); }, .many => { assert(ptrInfo.child == u8); const strslice = string.slice(&self.language.strings); var arraylist: std.ArrayList(u8) = .init(allocator); try arraylist.ensureUnusedCapacity(strslice.len); for (strslice) |char| arraylist.appendAssumeCapacity(char); if (ptrInfo.sentinel_ptr) |some| { return arraylist.toOwnedSliceSentinel(blk: { const sentinel: *align(1) const ptrInfo.child = @ptrCast(some); break :blk sentinel.*; }); } return arraylist.toOwnedSlice(); }, else => unreachable, }, else => unreachable, }, .array => |slice| switch (Schema) { .@"struct" => |structInfo| { if (structInfo.is_tuple) { assert(structInfo.fields.len == slice.len); var r: T = undefined; inline for (structInfo.fields, 0..slice.len) |field, i| { if (field.is_comptime) @panic(@typeName(T) ++ "." ++ field.name ++ " may not be a comptime field"); @field(r, field.name) = try self.reflectT(field.type, allocator, slice.tip + i); } return r; } unreachable; // may only reflect tuples }, .array => |arrayInfo| { assert(slice.len == arrayInfo.len); var r: T = undefined; for (0..slice.len) |i| r[i] = try self.reflectT(arrayInfo.child, allocator, slice.tip + i); return r; }, .pointer => |ptrInfo| switch (ptrInfo.size) { .slice => { var r: T = try allocator.alloc(ptrInfo.child, slice.len); if (ptrInfo.sentinel_ptr) |some| { r[slice.len - 1] = blk: { const sentinel: *align(1) const ptrInfo.child = @ptrCast(some); break :blk sentinel.*; }; } for (0..slice.len) |i| { // weird hack to populate the string const rptr = @constCast(r); rptr[i] = try self.reflectT(ptrInfo.child, allocator, slice.tip + i); } return r; }, else => unreachable, }, else => unreachable, }, .object => |object| switch (Schema) { .@"struct" => |structInfo| { if (structInfo.is_tuple) return error.TypeError; var tip = object.tip; var map: std.StringArrayHashMapUnmanaged(usize) = .empty; try map.ensureTotalCapacity(allocator, object.len); defer map.deinit(allocator); for (0..object.len) |_| if (self.language.property_map.get(tip)) |pen| { const key = pen.tip.slice(&self.language.properties); map.putAssumeCapacity(key, tip); tip += self.language.skipSlots(tip); }; var r: T = undefined; inline for (structInfo.fields) |field| { if (field.is_comptime) @panic(@typeName(T) ++ "." ++ field.name ++ " may not be a comptime field"); if (map.get(field.name)) |next_i| { @field(r, field.name) = try self.reflectT(field.type, allocator, next_i); } else switch (@typeInfo(field.type)) { .optional => @field(r, field.name) = null, else => @panic("Unknown property: " ++ field.name), } } return r; }, else => unreachable, }, } unreachable; } test reflectT { const allocator = std.testing.allocator; const text = \\{ \\ "age": 15, \\ "name": "Yuzu", \\ "admin": true, \\ "flags": 0, \\ "union": ":D", \\ "enum": "world", \\ "many": [1,2,3], \\ "tuple": [1, 2] \\} ; var self = try allocator.create(Self); self.* = try init(allocator, text); defer allocator.destroy(self); defer self.deinit(allocator); const idx: usize = try self.parse(allocator); const UserFlags = packed struct { is_cool: bool = false, is_friendly: bool = false, }; const UserSchema = struct { age: f64, name: []const u8, admin: bool, flags: UserFlags, @"union": union { hi: bool, bye: f64, n128: []const u8 }, @"enum": enum { hello, world }, many: []const u8, tuple: struct { u64, u64 }, }; const root = try self.reflectT(UserSchema, allocator, idx); std.debug.print("hello? {s}\n", .{@tagName(root.@"enum")}); std.debug.print("friend? {s}\n", .{root.@"union".n128}); std.debug.print("many: {any}\n", .{root.many}); std.debug.print("tuple: {any}\n", .{root.tuple}); allocator.free(root.many); } pub const Options = struct { max_buffer_size: usize = 4096, ruleset: Ruleset = .{}, indent: u8 = 0, }; pub const Ruleset = packed struct { /// print 'packed struct' as a number allow_bitfields: bool = false, /// print 'enum' as a number allow_data_cast: bool = false, /// ignore comments allow_comments: bool = false, /// whether to print '*T' as int allow_derefptrs: bool = false, /// prettify the output pretty: bool = false, pub const Pedantic = Ruleset{}; pub const Chill = Ruleset{ .allow_bitfields = true, .allow_data_cast = true, .allow_comments = true, .allow_derefptrs = true, }; }; pub fn prettyStringify(raw: anytype, comptime options: Options) ![]const u8 { return stringify(raw, comptime blk: { var opts = options; opts.ruleset.pretty = true; break :blk opts; }); } pub fn stringify(raw: anytype, comptime options: Options) ![]const u8 { if (options.indent > 0 and !options.ruleset.pretty) @compileError("Indentation is only allowed when pretty is enabled"); var buf: [options.max_buffer_size]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&buf); const allocator = fba.allocator(); if (std.meta.hasFn(@TypeOf(raw), "stringify")) { // If the type has a custom stringify function, use it. return raw.stringify(options); } switch (@typeInfo(@TypeOf(raw))) { .null => return "null", .bool => return if (raw) "true" else "false", .int, .comptime_int => return std.fmt.bufPrint(&buf, "{d}", .{raw}), .float, .comptime_float => return std.fmt.bufPrint(&buf, "{d:.1}", .{raw}), .optional => { if (raw) |value| { return stringify(value, options); } else { return "null"; } }, .@"enum" => |enumInfo| { if (options.ruleset.allow_data_cast) { const i: enumInfo.tag_type = @intFromEnum(raw); return std.fmt.bufPrint(&buf, "{d}", .{i}); } return std.fmt.bufPrint(&buf, "{s}", .{@tagName(raw)}); }, .@"struct" => |structInfo| switch (structInfo.layout) { .@"packed" => { if (options.ruleset.allow_bitfields) { const i: structInfo.backing_integer.? = @bitCast(raw); return std.fmt.bufPrint(&buf, "{d}", .{i}); } return error.TypeError; }, .auto, .@"extern" => { var string: std.ArrayListUnmanaged(u8) = .empty; var writer = string.writer(allocator); try writer.writeByte('{'); if (options.ruleset.pretty) try writer.writeByte('\n'); inline for (structInfo.fields, 0..) |field, i| { inline for (0..options.indent) |_| try writer.writeByte(' '); try writer.writeByte('"'); try writer.writeAll(field.name); try writer.writeByte('"'); if (options.ruleset.pretty) try writer.writeAll(": ") else try writer.writeByte(':'); const value = @field(raw, field.name); const str = try stringify(value, options); try writer.writeAll(str); if (i < structInfo.fields.len - 1) try writer.writeByte(','); if (options.ruleset.pretty) try writer.writeByte('\n'); } try writer.writeByte('}'); return string.toOwnedSlice(allocator); }, }, .array => |arrayInfo| { var string: std.ArrayListUnmanaged(u8) = .empty; var writer = string.writer(allocator); try writer.writeByte('['); for (0..arrayInfo.len) |i| { if (i > 0) { try writer.writeAll(','); if (options.ruleset.pretty) try writer.writeByte(' '); } const value = @field(raw, i); const str = try stringify(value, options); try writer.writeAll(str); } try writer.writeByte(']'); return string.toOwnedSlice(); }, .pointer => |ptrInfo| switch (ptrInfo.size) { .one => { if (options.ruleset.allow_derefptrs) { return stringify(blk: { const ptr: *align(ptrInfo.alignment) const ptrInfo.child = @ptrCast(raw); break :blk ptr.*; }, options); } return std.fmt.bufPrint(&buf, "{d}", .{@intFromPtr(raw)}); }, .slice => { if (ptrInfo.child == u8) { if (ptrInfo.is_const) return std.fmt.bufPrint(&buf, "\"{s}\"", .{raw}); return std.fmt.bufPrint(&buf, "\"{b}\"", .{raw}); } // it is a regular array const string: std.ArrayListUnmanaged(u8) = .empty; const writer = string.writer(allocator); try writer.writeByte('['); for (0..raw.len) |i| { if (i > 0) { try writer.writeAll(','); if (options.ruleset.pretty) try writer.writeByte(' '); } const value = raw[i]; const str = try stringify(value, options); try writer.writeAll(str); } try writer.writeByte(']'); return string.toOwnedSlice(); }, else => return error.TypeError, }, .@"union" => |unionInfo| { if (unionInfo.tag_type) |tag| { inline for (unionInfo.fields) |field| { if (field.name == @tagName(tag)) { const value = @field(raw, field.name); if (@typeInfo(field.type) == .pointer and field.type == []const u8) { // idk return std.fmt.bufPrint(&buf, "\"{s}\"", .{value}); } else { return stringify(value, options); } } return error.TypeError; } unreachable; } unreachable; // union has no tag }, else => |t| @compileError("Error on " ++ @tagName(t)), } } test stringify { const UserFlags = packed struct { is_cool: bool = false, is_friendly: bool = false, }; const UserSchema = struct { age: f64, name: []const u8, admin: bool, flags: UserFlags, @"union": union { hi: bool, bye: f64, n128: []const u8, pub fn stringify(_: anytype, comptime options: Options) ![]const u8 { _ = options; // unused return "anything here"; } }, @"enum": enum { hello, world }, many: []const u8, epicptr: *const u8, }; const e: []const u8 = "epic pointer"; const user = UserSchema{ .age = 15.0, .name = "Yuzu", .admin = true, .flags = UserFlags{ .is_cool = true, .is_friendly = false }, .@"union" = .{ .hi = true }, .@"enum" = .world, .many = "hello", .epicptr = @ptrCast(e[0..1]), }; const options = Options{ .max_buffer_size = 1024, .ruleset = Ruleset.Chill, .indent = 2, }; const str = try prettyStringify(user, options); std.debug.print("Stringified user: {s}\n", .{str}); }