516 lines
18 KiB
Zig
516 lines
18 KiB
Zig
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});
|
|
}
|