I just wanted to share this feature of Zig I came across while working on my last post… It’s not a completely new concept in programming, but it’s not something which is easily done in C.
In C, we have a simple method for parsing strings, which involves iterating over a character array:
#include <stdio.h>
void main()
{
char *cmd = "hello.okay[0]";
printf("%s\n", cmd);
char out[13];
for(int i = 0; cmd[i] != '\0'; i++)
{
out[i] = cmd[i] + 1;
printf("%c", out[i]);
}
return;
}
This concept of character arrays is one of the defining features of C, and iterating over a character array is the basic way of parsing a string. We can modify this function to process the string for most situations, without using the standard library.
In Zig we can do the same:
const std = @import("std");
pub fn main() void {
const cmd = "hello.okay[0]";
std.debug.print("{s}\n", .{ cmd });
var out: [13]u8 = undefined;
var i: usize = 0;
for (cmd) |c| {
out[i] = c + 1;
std.debug.print("{c}", .{ out[i] });
i += 1;
}
}
In both cases I am making copies of the string and modifying each character individually, increasing the ascii value by 1, and printing the output.
Also in Zig, we have enhanced functions for parsing strings, found in std.mem. However we also have another option, which might be useful for certain situations, which involves the ArrayList type. I think this is an interesting use case since it represents a middle ground between the primitive C-style arrays and the optimized functions found in std.mem. By using ArrayList we can rely only on the bare minimum of the standard library, and still take advantage of a lot of Zig’s cutting edge features:
const std = @import("std");
pub fn main() !void {
const cmd = "hello.okay[0]";
std.debug.print("{s}\n", .{ cmd });
var out = std.ArrayList(u8).init(std.heap.page_allocator);
defer out.deinit();
var i: usize = 0;
for (cmd) |c| {
try out.append(c + 1);
std.debug.print("{c}", .{ out.items[i] });
i += 1;
}
}
I was able to take this concept and come up with a simple solution for parsing strings for my JSON utility class:
const std = @import("std");
const T = struct {
x: ?std.json.Value,
pub fn init(self: std.json.Value) T {
return T {
.x = self
};}
pub fn get(self: T, query: []const u8) T {
if (self.x.?.object.get(query)) |value| {
return T.init(value);
}
else {
std.debug.print("ERROR::{s}::", .{ "invalid query" });
return T.init(self.x.?);
}}
pub fn unpackInto(self: *const T, buffer: *std.ArrayList(u8)) !void {
switch (self.x.?) {
.string => |i| {
const P = struct { value: []const u8 };
try std.json.stringify(P{ .value = i }, .{ }, buffer.writer());
},
.integer => |i| {
const P = struct { value: i64 };
try std.json.stringify(P{ .value = i }, .{ }, buffer.writer());
},
.bool => |i| {
const P = struct { value: bool };
try std.json.stringify(P{ .value = i }, .{ }, buffer.writer());
},
.float => |i| {
const P = struct { value: f64 };
try std.json.stringify(P{ .value = i }, .{ }, buffer.writer());
},
.null => {
const P = struct { value: ?usize = null };
try std.json.stringify(P{ }, .{ }, buffer.writer());
},
.array => |i| {
const P = struct { value: []std.json.Value };
try std.json.stringify(P{ .value = i.items }, .{ }, buffer.writer());
},
.object => {
const i = self.x.?;
const P = struct { value: ?std.json.Value };
try std.json.stringify(P{ .value = i }, .{ }, buffer.writer());
},
else => {
std.debug.print("ERROR::{s}::", .{ "unhandled type" });
}}
std.debug.print("{s}\n", .{ buffer.items });
}
pub fn pos(self: T, i: usize) T {
switch (self.x.?) {
.array => {
if (i >= self.x.?.array.items.len) {
std.debug.print("ERROR::{s}::", .{ "index out of bounds" });
return T.init(self.x.?);
}
return T.init(self.x.?.array.items[i]);
},
else => {
std.debug.print("ERROR::{s}::", .{ "not an array" });
return T.init(self.x.?);
}}}};
pub fn main() !void {
const my_json =
\\{
\\ "hello": { "name":"hello", "id": { "key": "okay" }, "hash": [null, 2.33e+77, false, -5] },
\\ "okay": [0, 1, "maybe", { "name":"hello", "id": { "key": "okay" }, "hash": [true, 2.55e+99, null, -7] }],
\\ "maybe": "1234567"
\\}
;
const parsed = try std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, my_json, .{ });
defer parsed.deinit();
const json = T.init(parsed.value);
try read(&json, "hello;");
try read(&json, "okay;");
try read(&json, "okay[2];");
try read(&json, "okay[3];");
try read(&json, "hello.id;");
try read(&json, "hello.id.key;");
try read(&json, "hello.hash;");
try read(&json, "hello.hash[0];");
try read(&json, "hello.hash[1];");
std.debug.print("\n", .{ });
try read(&json, "invalid;");
try read(&json, "hello.hash[8];");
try read(&json, "hello[2];");
}
pub fn read(json: *const T, cmd: []const u8) !void {
var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
defer buffer.deinit();
try buffer.ensureTotalCapacity(100);
var pos = std.ArrayList(u8).init(std.heap.page_allocator);
defer pos.deinit();
var val: T = json.*;
var i: usize = 0;
var p: usize = undefined;
for (cmd) |c| {
if (c == 59) {
if (p == 93) {
try val.unpackInto(&buffer);
}
else {
val = val.get(pos.items);
pos.clearRetainingCapacity();
try val.unpackInto(&buffer);
}}
else if (c == 46) {
val = val.get(pos.items);
pos.clearRetainingCapacity();
i += 1;
}
else if (c == 91) {
val = val.get(pos.items);
pos.clearRetainingCapacity();
i += 1;
for (cmd[i..]) |u| {
if (u == 93) {
const int = try std.fmt.parseInt(usize, pos.items, 10);
val = val.pos(int);
pos.clearRetainingCapacity();
i += 1;
p = 93;
}
else {
try pos.append(u);
i += 1;
}}}
else {
try pos.append(c);
i += 1;
}
buffer.clearRetainingCapacity();
}}
This project which I was working on the past couple months is now feature complete, but there should still be room for more optimizations and validation. I’m thinking to take this and expand it so that strings can be passed in through an external file, and also processed over a network stream. There I will be trying to take full advantage of std.mem in particular, to see what else I can learn about Zig. I am open to any suggestions for improvement. Thanks for reading. Hope you enjoyed it!