aboutsummaryrefslogtreecommitdiff
path: root/src/json
diff options
context:
space:
mode:
Diffstat (limited to 'src/json')
-rw-r--r--src/json/json.c282
-rw-r--r--src/json/json.h34
-rw-r--r--src/json/test_json.c224
-rw-r--r--src/json/test_json/test_json_backspaces.json1
-rw-r--r--src/json/test_json/test_json_complex_multiline.json24
-rw-r--r--src/json/test_json/test_json_multiline.json5
6 files changed, 570 insertions, 0 deletions
diff --git a/src/json/json.c b/src/json/json.c
new file mode 100644
index 0000000..d16415b
--- /dev/null
+++ b/src/json/json.c
@@ -0,0 +1,282 @@
+#include <ctype.h>
+#include <immintrin.h>
+#include <math.h>
+#include <stdbit.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../std/assert.h"
+#include "json.h"
+#include "../std/str.h"
+#include "../std/types.h"
+
+JSONObject *JSONObject_new(Arena *a) {
+ JSONObject *object = new(a, JSONObject, 1);
+ object->len = 0;
+ object->keys = Vec_new(a, StrView, 2);
+ object->values = Vec_new(a, JSON, 2);
+ return object;
+}
+
+JSON JSON_integer(i32 value) {
+ JSON json = {0};
+ json.type = JSON_INTEGER;
+ json.as.integer = value;
+ return json;
+}
+
+JSON JSON_float(f32 value) {
+ JSON json = {0};
+ json.type = JSON_FLOAT;
+ json.as.floating = value;
+ return json;
+}
+
+JSON JSON_array(Vec items) {
+ JSON json = {0};
+ json.type = JSON_ARRAY;
+ json.as.array = items;
+ return json;
+}
+
+JSON JSON_string(StrView value) {
+ JSON json = {0};
+ json.type = JSON_STRING;
+ json.as.string = value;
+ return json;
+}
+
+JSON JSON_object(size len, Vec keys, Vec values) {
+ assert(len == keys.len && len == values.len, "length mismatch");
+ JSON json = {0};
+ json.type = JSON_OBJECT;
+ json.as.object.len = len;
+ json.as.object.keys = keys;
+ json.as.object.values = values;
+ return json;
+}
+
+JSON JSON_boolean(bool value) {
+ JSON json = {0};
+ json.type = JSON_BOOL;
+ json.as.boolean = value;
+ return json;
+}
+
+JSON JSON_null() {
+ JSON json = {0};
+ json.type = JSON_NULL;
+ return json;
+}
+
+i32 JSON_fprint(FILE *w, JSON json) {
+ switch (json.type) {
+ case JSON_STRING:
+ fprintf(w, "\"");
+ Str_fprint(w, json.as.string);
+ return fprintf(w, "\"");
+ case JSON_INTEGER:
+ return fprintf(w, "%d", json.as.integer);
+ case JSON_FLOAT:
+ return fprintf(w, "%f", json.as.floating);
+ case JSON_NULL:
+ return fprintf(w, "null");
+ case JSON_BOOL:
+ return fprintf(w, "%s", json.as.boolean ? "true" : "false");
+ case JSON_ARRAY:
+ fprintf(w, "[");
+ for (size i = 0; i < json.as.array.len; i++) {
+ JSON value = *Vec_get(json.as.array, JSON, i);
+ JSON_fprint(w, value);
+ if (i < json.as.array.len - 1) fprintf(w, ", ");
+ }
+ fprintf(w, "]");
+ return 0;
+ case JSON_OBJECT:
+ fprintf(w, "{");
+ for (size i = 0; i < json.as.object.len; i++) {
+ StrView key = *Vec_get(json.as.object.keys, StrView, i);
+ fprintf(w, "\"");
+ Str_fprint(w, key);
+ fprintf(w, "\": ");
+ JSON value = *Vec_get(json.as.object.values, JSON, i);
+ JSON_fprint(w, value);
+ if (i < json.as.object.len - 1) fprintf(w, ", ");
+ }
+ fprintf(w, "}");
+ return 0;
+ }
+ assert(false, "unknown json type %d", json.type);
+}
+
+typedef struct {
+ JSON value;
+ uptr pos;
+} parse_result;
+
+#define assert_parse(condition, msg, src, pos) \
+ assert(condition, "%s (%c at %lu)", msg, src[pos], pos)
+
+parse_result parse_any(Arena *a, byte *src, uptr pos, uptr max_pos);
+
+parse_result parse_number(byte *src, uptr start_pos) {
+ parse_result result = {0};
+ byte *end;
+ f32 number = strtod(src + start_pos, &end);
+ assert_parse(end != src, "invalide number", src, result.pos);
+ result.pos += end - src;
+ result.value =
+ floor(number) == number ? JSON_integer(number) : JSON_float(number);
+ return result;
+}
+
+uptr absorb_whitespaces(byte *src, uptr start_pos, uptr max_pos) {
+ uptr pos = start_pos;
+ while (pos < max_pos && isspace(src[pos])) {
+ pos++;
+ }
+ return pos;
+}
+
+parse_result parse_array(Arena *a, byte *src, uptr start_pos, uptr max_pos) {
+ assert_parse(src[start_pos] == '[', "invalid array start", src, start_pos);
+ parse_result result = {0};
+ result.pos = start_pos + 1;
+ if (src[result.pos] == ']') {
+ result.pos++;
+ return result;
+ }
+ Vec items = Vec_new(a, JSON, 6);
+ for (;;) {
+ result.pos = absorb_whitespaces(src, result.pos, max_pos);
+ parse_result item_result = parse_any(a, src, result.pos, max_pos);
+ Vec_push(a, &items, &item_result.value);
+ result.pos = absorb_whitespaces(src, item_result.pos, max_pos);
+ if (src[result.pos] == ',') {
+ result.pos++;
+ } else {
+ break;
+ }
+ };
+ assert_parse(src[result.pos] == ']', "invalid array end", src, result.pos);
+ result.pos++;
+ result.value = JSON_array(items);
+ return result;
+}
+
+parse_result parse_string(byte *src, uptr start_pos, uptr max_pos) {
+ assert_parse(src[start_pos] == '"', "invalid string start", src, start_pos);
+ parse_result result = {0};
+ result.pos = start_pos + 1;
+
+ // SIMD optimization: process 32 bytes at a time using AVX2
+ bytex32 quote_bytes = _mm256_set1_epi8('"');
+
+ for (; result.pos + 32 <= max_pos; result.pos += 32) {
+ bytex32 chunk = _mm256_loadu_si256((bytex32 *)(src + result.pos));
+ i32 quote_mask =
+ _mm256_movemask_epi8(_mm256_cmpeq_epi8(chunk, quote_bytes));
+
+ if (quote_mask != 0) {
+ result.pos += stdc_trailing_zeros_ui(quote_mask);
+ break;
+ };
+ }
+
+ // Fall back to original byte-by-byte processing for the remaining bytes
+ for (; result.pos < max_pos; result.pos++) {
+ if (src[result.pos] == '"') {
+ uptr backslash_count = 0;
+ while (backslash_count < result.pos &&
+ src[result.pos - backslash_count - 1] == '\\') {
+ backslash_count++;
+ }
+ if (!(backslash_count & 1)) break;
+ }
+ }
+
+ assert_parse(src[result.pos] == '"', "invalid string end", src, result.pos);
+ result.value =
+ JSON_string(Str_view(&src[start_pos + 1], result.pos - start_pos - 1));
+ result.pos++;
+
+ return result;
+}
+
+parse_result parse_object(Arena *a, byte *src, uptr start_pos, uptr max_pos) {
+ assert_parse(src[start_pos] == '{', "invalid object start", src, start_pos);
+ parse_result result = {0};
+ result.pos = absorb_whitespaces(src, start_pos + 1, max_pos);
+ size len = 0;
+ Vec keys = Vec_new(a, StrView, 6);
+ Vec values = Vec_new(a, JSON, 6);
+ if (src[result.pos] == '}') {
+ result.pos++;
+ result.value = JSON_object(len, keys, values);
+ return result;
+ }
+ for (;;) {
+ parse_result key_result = parse_string(src, result.pos, max_pos);
+ result.pos = absorb_whitespaces(src, key_result.pos, max_pos);
+ assert_parse(src[result.pos] == ':', "invalid keyval delim", src,
+ result.pos);
+ result.pos = absorb_whitespaces(src, result.pos + 1, max_pos);
+ parse_result value_result = parse_any(a, src, result.pos, max_pos);
+ len++;
+ assert(key_result.value.type == JSON_STRING,
+ "invalid json type for key: %d", key_result.value.type);
+ Vec_push(a, &keys, &key_result.value.as.string);
+ Vec_push(a, &values, &value_result.value);
+ result.pos = absorb_whitespaces(src, value_result.pos, max_pos);
+ if (src[result.pos] != ',') {
+ break;
+ }
+ result.pos = absorb_whitespaces(src, result.pos + 1, max_pos);
+ }
+ assert_parse(src[result.pos] == '}', "invalid object end", src, result.pos);
+ result.pos++;
+ result.value = JSON_object(len, keys, values);
+ return result;
+}
+
+parse_result parse_any(Arena *a, byte *src, uptr pos, uptr max_pos) {
+ pos = absorb_whitespaces(src, pos, max_pos);
+ if (isdigit(src[pos]) || src[pos] == '-') return parse_number(src, pos);
+ if (src[pos] == '[') return parse_array(a, src, pos, max_pos);
+ if (src[pos] == '"') return parse_string(src, pos, max_pos);
+ if (src[pos] == '{') return parse_object(a, src, pos, max_pos);
+ if (memcmp(src + pos, "true", 4) == 0) {
+ parse_result result = {0};
+ result.value = JSON_boolean(true);
+ result.pos = pos + 4;
+ return result;
+ }
+ if (memcmp(src + pos, "false", 5) == 0) {
+ parse_result result = {0};
+ result.value = JSON_boolean(false);
+ result.pos = pos + 5;
+ return result;
+ }
+ if (memcmp(src + pos, "null", 4) == 0) {
+ parse_result result = {0};
+ result.value = JSON_null();
+ result.pos = pos + 4;
+ return result;
+ }
+ assert_parse(false, "unexpected input", src, pos);
+}
+
+JSON JSON_parse(Arena *a, byte *src, uptr len) {
+ return parse_any(a, src, 0, len).value;
+}
+
+JSON *JSONObject_get(JSONObject obj, Str key) {
+ for (size i = 0; i < obj.len; i++) {
+ StrView tmp_key = *Vec_get(obj.keys, StrView, i);
+ if (Str_equals(tmp_key, key)) return Vec_get(obj.values, JSON, i);
+ }
+ return 0;
+}
diff --git a/src/json/json.h b/src/json/json.h
new file mode 100644
index 0000000..15b3fca
--- /dev/null
+++ b/src/json/json.h
@@ -0,0 +1,34 @@
+#pragma once
+#include "../std/memory.h"
+#include "../std/str.h"
+#include "../std/vec.h"
+
+typedef struct {
+ Vec keys; // Vec<Str>
+ Vec values; // Vec<JSON>
+ size len;
+} JSONObject;
+typedef enum {
+ JSON_NULL,
+ JSON_BOOL,
+ JSON_INTEGER,
+ JSON_FLOAT,
+ JSON_STRING,
+ JSON_ARRAY,
+ JSON_OBJECT
+} JSONType;
+typedef struct {
+ JSONType type;
+ union {
+ bool boolean;
+ i32 integer;
+ f32 floating;
+ Vec array; // Vec<JSON>
+ StrView string;
+ JSONObject object;
+ } as;
+} JSON;
+JSON *JSONObject_get(JSONObject obj, Str key);
+i32 JSON_fprint(FILE *w, JSON json);
+JSON JSON_parse(Arena *a, byte *src, uptr len);
+JSON JSON_Int(Arena *a, i32 val);
diff --git a/src/json/test_json.c b/src/json/test_json.c
new file mode 100644
index 0000000..058f191
--- /dev/null
+++ b/src/json/test_json.c
@@ -0,0 +1,224 @@
+#include "json.h"
+#include "../std/testing.h"
+
+#define JSON_parse_raw(a, s) JSON_parse(a, s, lengthof(s));
+
+i32 test_json_integer() {
+ Arena perm = Arena_new(128e3);
+
+ JSON json_int = JSON_parse_raw(&perm, "123");
+
+ t_assert_equal(json_int.as.integer, 123);
+ t_assert_equal(Arena_get_used(perm), 0);
+ return 0;
+}
+
+i32 test_json_int_array() {
+ Arena perm = Arena_new(128e3);
+
+ JSON json_int_array = JSON_parse_raw(&perm, "[1,2,3]");
+
+ t_assert_equal(json_int_array.type, JSON_ARRAY);
+ t_assert_equal(json_int_array.as.array.len, 3);
+ int ints[3];
+ int sum = 0;
+ for (int i = 0; i < json_int_array.as.array.len; i++) {
+ JSON *item = Vec_get(json_int_array.as.array, JSON, i);
+ ints[i] = item->as.integer;
+ sum += item->as.integer;
+ }
+ t_assert_equal(ints[0], 1);
+ t_assert_equal(ints[1], 2);
+ t_assert_equal(ints[2], 3);
+ t_assert_equal(sum, 6);
+ t_assert_equal(Arena_get_used(perm), 576);
+
+ return 0;
+}
+
+i32 test_json_empty_array() {
+ Arena perm = Arena_new(128e3);
+
+ JSON json_empty_array = JSON_parse_raw(&perm, "[]");
+
+ t_assert_equal(json_empty_array.as.array.len, 0);
+ t_assert_equal(Arena_get_used(perm), 0); // TODO: weird
+
+ return 0;
+}
+
+i32 test_json_nested_array() {
+ Arena perm = Arena_new(128e3);
+
+ JSON doc = JSON_parse_raw(&perm, "[[1,\"abc\"]]");
+
+ JSON sub_array = *Vec_get(doc.as.array, JSON, 0);
+ JSON val_1 = *Vec_get(sub_array.as.array, JSON, 0);
+ t_assert_equal(val_1.as.integer, 1);
+ JSON val_2 = *Vec_get(sub_array.as.array, JSON, 1);
+ t_assert_strequal(val_2.as.string, S("abc"));
+ t_assert_equal(Arena_get_used(perm), 1152);
+
+ return 0;
+}
+
+i32 test_json_string() {
+ Arena perm = Arena_new(128e3);
+
+ JSON js = JSON_parse_raw(&perm, "\"abc\"");
+
+ t_assert_strequal(js.as.string, S("abc"));
+ t_assert_equal(Arena_get_used(perm), 0);
+
+ return 0;
+}
+
+i32 test_json_empty_object() {
+ Arena perm = Arena_new(128e3);
+
+ JSON doc = JSON_parse_raw(&perm, "{}");
+
+ t_assert_equal(doc.as.object.len, 0);
+ t_assert_equal(Arena_get_used(perm), 672);
+
+ return 0;
+}
+
+i32 test_json_object() {
+ Arena perm = Arena_new(128e3);
+
+ JSON doc = JSON_parse_raw(&perm, "{\"key\": \"value\"}");
+
+ t_assert_equal(doc.as.object.len, 1);
+ StrView key = *Vec_get(doc.as.object.keys, StrView, 0);
+ t_assert_strequal(key, S("key"));
+ JSON value = *JSONObject_get(doc.as.object, S("key"));
+ t_assert_strequal(value.as.string, S("value"));
+ t_assert_equal(Arena_get_used(perm), 672);
+
+ return 0;
+}
+
+i32 test_json_boolean() {
+ Arena perm = Arena_new(128e3);
+
+ JSON doc = JSON_parse_raw(&perm, "{\"a\": true, \"b\": false}");
+
+ JSON a = *JSONObject_get(doc.as.object, S("a"));
+ t_assert_equal(a.as.boolean, true);
+ JSON b = *JSONObject_get(doc.as.object, S("b"));
+ t_assert_equal(b.as.boolean, false);
+ t_assert_equal(Arena_get_used(perm), 672);
+
+ return 0;
+}
+
+i32 test_json_float() {
+ Arena perm = Arena_new(128e3);
+
+ JSON doc = JSON_parse_raw(&perm, "[1.2, 2.3]");
+ t_assert_equal(doc.as.array.len, 2);
+ JSON first = *Vec_get(doc.as.array, JSON, 0);
+ t_assert_fequal(first.as.floating, 1.2);
+ JSON second = *Vec_get(doc.as.array, JSON, 1);
+ t_assert_fequal(second.as.floating, 2.3);
+ t_assert_equal(Arena_get_used(perm), 576);
+
+ return 0;
+}
+
+i32 test_json_null() {
+ Arena perm = Arena_new(128e3);
+
+ JSON doc = JSON_parse_raw(&perm, "[null]");
+ t_assert_equal(doc.as.array.len, 1);
+ JSON first = *Vec_get(doc.as.array, JSON, 0);
+ t_assert_equal(first.type, JSON_NULL);
+ t_assert_equal(Arena_get_used(perm), 576);
+
+ return 0;
+}
+
+i32 test_json_long_spaces() {
+ Arena perm = Arena_new(128e3);
+ JSON doc = JSON_parse_raw(
+ &perm, "{\t \t \t \t \t }");
+
+ t_assert_equal(doc.as.object.len, 0);
+
+ return 0;
+}
+
+i32 test_json_multiline() {
+ Arena perm = Arena_new(128e3);
+ MemoryFile file =
+ load_file(&perm, "src/json/test_json/test_json_multiline.json");
+ JSON doc = JSON_parse(&perm, file.content, file.size);
+ JSON target_size_mb = *JSONObject_get(doc.as.object, S("target_size_mb"));
+ t_assert_equal(target_size_mb.as.integer, 1);
+ JSON generated_at = *JSONObject_get(doc.as.object, S("generated_at"));
+ t_assert_strequal(generated_at.as.string, S("2025-07-22T10:36:30.974926"));
+
+ return 0;
+}
+
+i32 test_json_nested_object() {
+ Arena perm = Arena_new(128e3);
+
+ JSON doc = JSON_parse_raw(&perm, "{\"1\":{\"2\":{}}}");
+
+ JSON lvl_1 = *JSONObject_get(doc.as.object, S("1"));
+ t_assert_equal(lvl_1.type, JSON_OBJECT);
+ JSON lvl_2 = *JSONObject_get(lvl_1.as.object, S("2"));
+ t_assert_equal(lvl_2.type, JSON_OBJECT);
+ return 0;
+}
+
+i32 test_json_array_in_object() {
+ Arena perm = Arena_new(128e3);
+ JSON doc = JSON_parse_raw(&perm, "{\"tags\": [\"TWvZi0\",\"pM0TPv9X\"]}");
+
+ JSON tags = *JSONObject_get(doc.as.object, S("tags"));
+ t_assert_equal(tags.type, JSON_ARRAY);
+ t_assert_equal(tags.as.array.len, 2);
+
+ return 0;
+}
+
+i32 test_json_complex_multiline() {
+ Arena perm = Arena_new(128e3);
+ MemoryFile file =
+ load_file(&perm, "src/json/test_json/test_json_complex_multiline.json");
+ JSON doc = JSON_parse(&perm, file.content, file.size);
+
+ JSON id = *JSONObject_get(doc.as.object, S("id"));
+ t_assert_equal(id.as.integer, 819228);
+ JSON metadata = *JSONObject_get(doc.as.object, S("metadata"));
+ JSON preferences = *JSONObject_get(metadata.as.object, S("preferences"));
+ JSON theme = *JSONObject_get(preferences.as.object, S("theme"));
+ t_assert_strequal(theme.as.string, S("auto"));
+
+ return 0;
+}
+
+i32 test_negative_numbers() {
+ Arena perm = Arena_new(128e3);
+ JSON doc = JSON_parse_raw(&perm, "[-1, -2.5]");
+
+ JSON integer = *Vec_get(doc.as.array, JSON, 0);
+ t_assert_equal(integer.as.integer, -1);
+ JSON floating = *Vec_get(doc.as.array, JSON, 1);
+ t_assert_fequal(floating.as.floating, -2.5);
+
+ return 0;
+}
+
+i32 test_json_backspaces() {
+ Arena perm = Arena_new(128e3);
+ MemoryFile file =
+ load_file(&perm, "src/json/test_json/test_json_backspaces.json");
+ JSON doc = JSON_parse(&perm, file.content, file.size);
+
+ t_assert_strequal(doc.as.string, S("weird text\\"));
+ return 0;
+}
diff --git a/src/json/test_json/test_json_backspaces.json b/src/json/test_json/test_json_backspaces.json
new file mode 100644
index 0000000..ceb65d6
--- /dev/null
+++ b/src/json/test_json/test_json_backspaces.json
@@ -0,0 +1 @@
+"weird text\\"
diff --git a/src/json/test_json/test_json_complex_multiline.json b/src/json/test_json/test_json_complex_multiline.json
new file mode 100644
index 0000000..a0cfc9b
--- /dev/null
+++ b/src/json/test_json/test_json_complex_multiline.json
@@ -0,0 +1,24 @@
+{
+ "id": 819228,
+ "username": "rCrRv4PRnUNuAy",
+ "email": "xzbwrzj@zwod.com",
+ "first_name": "4WH7g7PsOEm5",
+ "last_name": "6ulndxUgjbIayl",
+ "age": 30,
+ "phone": "+1-529-712-3931",
+ "is_active": false,
+ "balance": 9347.49,
+ "tags": [
+ "TWvZi0",
+ "pM0TPv9X"
+ ],
+ "metadata": {
+ "last_login": "2024-06-13T21:22:32Z",
+ "login_count": 245,
+ "preferences": {
+ "theme": "auto",
+ "notifications": true,
+ "language": "en"
+ }
+ }
+}
diff --git a/src/json/test_json/test_json_multiline.json b/src/json/test_json/test_json_multiline.json
new file mode 100644
index 0000000..7cb5f4c
--- /dev/null
+++ b/src/json/test_json/test_json_multiline.json
@@ -0,0 +1,5 @@
+{
+ "generated_at": "2025-07-22T10:36:30.974926",
+ "target_size_mb": 1,
+ "generator_version": "1.0"
+}