diff --git a/Makefile b/Makefile index f43e0e9d..f2cea23b 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ endif CHIBI_COMPILED_LIBS = lib/chibi/filesystem$(SO) lib/chibi/weak$(SO) \ lib/chibi/heap-stats$(SO) lib/chibi/disasm$(SO) lib/chibi/ast$(SO) \ - lib/chibi/emscripten$(SO) + lib/chibi/json$(SO) lib/chibi/emscripten$(SO) CHIBI_POSIX_COMPILED_LIBS = lib/chibi/process$(SO) lib/chibi/time$(SO) \ lib/chibi/system$(SO) lib/chibi/stty$(SO) lib/chibi/pty$(SO) \ lib/chibi/net$(SO) diff --git a/lib/chibi/json-test.sld b/lib/chibi/json-test.sld new file mode 100644 index 00000000..69eefd63 --- /dev/null +++ b/lib/chibi/json-test.sld @@ -0,0 +1,64 @@ + +(define-library (chibi json-test) + (import (scheme base) (chibi json) (chibi test)) + (export run-tests) + (begin + (define (run-tests) + (test-begin "json") + (test '((glossary + (title . "example glossary") + (GlossDiv + (title . "S") + (GlossList + (GlossEntry + (ID . "SGML") + (SortAs . "SGML") + (GlossTerm . "Standard Generalized Markup Language") + (Acronym . "SGML") + (Abbrev . "ISO 8879:1986") + (GlossDef + (para . "A meta-markup language, used to create markup languages such as DocBook.") + (GlossSeeAlso . #("GML" "XML"))) + (GlossSee . "markup")))))) + (parse-json "{ + \"glossary\": { + \"title\": \"example glossary\", + \"GlossDiv\": { + \"title\": \"S\", + \"GlossList\": { + \"GlossEntry\": { + \"ID\": \"SGML\", + \"SortAs\": \"SGML\", + \"GlossTerm\": \"Standard Generalized Markup Language\", + \"Acronym\": \"SGML\", + \"Abbrev\": \"ISO 8879:1986\", + \"GlossDef\": { + \"para\": \"A meta-markup language, used to create markup languages such as DocBook.\", + \"GlossSeeAlso\": [\"GML\", \"XML\"] + }, + \"GlossSee\": \"markup\" + } + } + } + } +}")) + (test '((menu + (id . "file") + (value . "File") + (popup + (menuitem + . #(((value . "New") (onclick . "CreateNewDoc()")) + ((value . "Open") (onclick . "OpenDoc()")) + ((value . "Close") (onclick . "CloseDoc()"))))))) + (parse-json "{\"menu\": { + \"id\": \"file\", + \"value\": \"File\", + \"popup\": { + \"menuitem\": [ + {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"}, + {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"}, + {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"} + ] + } +}}")) + (test-end)))) diff --git a/lib/chibi/json.c b/lib/chibi/json.c new file mode 100644 index 00000000..178bcbfc --- /dev/null +++ b/lib/chibi/json.c @@ -0,0 +1,251 @@ +/* json.c -- fast json parser */ +/* Copyright (c) 2019 Alex Shinn. All rights reserved. */ +/* BSD-style license: http://synthcode.com/license.txt */ + +#include + +sexp parse_json (sexp ctx, sexp self, sexp str, const char* s, int* i, const int len); + +sexp sexp_json_exception (sexp ctx, sexp self, const char* msg, sexp str, const int pos) { + sexp_gc_var2(res, tmp); + sexp_gc_preserve2(ctx, res, tmp); + tmp = sexp_list2(ctx, str, sexp_make_fixnum(pos)); + res = sexp_user_exception(ctx, self, msg, tmp); + sexp_gc_release2(ctx); + return res; +} + +sexp parse_json_number (sexp ctx, sexp self, sexp str, const char* s, int* i, const int len) { + sexp_sint_t res = 0; + int j = *i; + while (j < len && isdigit(s[j])) + res = res * 10 + s[j++] - '0'; + *i = j; + return sexp_make_fixnum(res); +} + +sexp parse_json_literal (sexp ctx, sexp self, sexp str, const char* s, int* i, const int len, const char* name, int namelen, sexp value) { + sexp res; + if (strncasecmp(s+*i, name, namelen) == 0 && (*i+namelen >= len || !isalnum(s[*i+namelen]))) { + res = value; + *i += namelen; + } else { + res = sexp_json_exception(ctx, self, "unexpected character in json at", str, *i); + } + return res; +} + +sexp parse_json_string (sexp ctx, sexp self, sexp str, const char* s, int* i, const int len) { + sexp_gc_var2(res, tmp); + sexp_gc_preserve2(ctx, res, tmp); + int from = *i, to = *i; + res = SEXP_NULL; + for ( ; s[to] != '"'; ++to) { + if (to+1 >= len) { + res = sexp_json_exception(ctx, self, "unterminated string in json started at", str, *i); + break; + } + if (s[to] == '\\') { + tmp = sexp_c_string(ctx, s+from, to-from); + res = sexp_stringp(res) ? sexp_list2(ctx, tmp, res) : sexp_cons(ctx, tmp, res); + switch (s[++to]) { + case 'n': + tmp = sexp_c_string(ctx, "\n", -1); + res = sexp_cons(ctx, tmp, res); + from = to+1; + break; + case 't': + tmp = sexp_c_string(ctx, "\t", -1); + res = sexp_cons(ctx, tmp, res); + from = to+1; + break; + default: + from = to; + break; + } + } + } + if (!sexp_exceptionp(res)) { + tmp = sexp_c_string(ctx, s+from, to-from); + if (res == SEXP_NULL) { + res = tmp; + } else { + res = sexp_stringp(res) ? sexp_list2(ctx, tmp, res) : sexp_cons(ctx, tmp, res); + res = sexp_nreverse(ctx, res); + res = sexp_string_concatenate(ctx, res, SEXP_FALSE); + } + } + *i = to+1; + sexp_gc_release2(ctx); + return res; +} + +sexp parse_json_array (sexp ctx, sexp self, sexp str, const char* s, int* i, const int len) { + sexp_gc_var2(res, tmp); + sexp_gc_preserve2(ctx, res, tmp); + int j = *i; + int comma = 1; + res = SEXP_NULL; + while (1) { + if (j >= len) { + res = sexp_json_exception(ctx, self, "unterminated array in json started at", str, *i); + break; + } else if (s[j] == ']') { + if (comma && res != SEXP_NULL) { + res = sexp_json_exception(ctx, self, "missing value after comma in json array at", str, j); + } else { + res = sexp_nreverse(ctx, res); + res = sexp_list_to_vector(ctx, res); + } + ++j; + break; + } else if (s[j] == ',' && comma) { + res = sexp_json_exception(ctx, self, "unexpected comma in json array at", str, j); + break; + } else if (s[j] == ',') { + comma = 1; + ++j; + } else if (isspace(s[j])) { + ++j; + } else { + if (comma) { + tmp = parse_json(ctx, self, str, s, &j, len); + if (sexp_exceptionp(tmp)) { + res = tmp; + break; + } + res = sexp_cons(ctx, tmp, res); + comma = 0; + } else { + res = sexp_json_exception(ctx, self, "unexpected value in json array at", str, j); + break; + } + } + } + *i = j; + sexp_gc_release2(ctx); + return res; +} + +sexp parse_json_object (sexp ctx, sexp self, sexp str, const char* s, int* i, const int len) { + sexp_gc_var2(res, tmp); + sexp_gc_preserve2(ctx, res, tmp); + int j = *i; + int comma = 1; + res = SEXP_NULL; + while (1) { + if (j >= len) { + res = sexp_json_exception(ctx, self, "unterminated object in json started at", str, *i); + break; + } else if (s[j] == '}') { + if (comma && res != SEXP_NULL) { + res = sexp_json_exception(ctx, self, "missing value after comma in json object at", str, j); + } else { + res = sexp_nreverse(ctx, res); + } + ++j; + break; + } else if (s[j] == ',' && comma) { + res = sexp_json_exception(ctx, self, "unexpected comma in json object at", str, j); + break; + } else if (s[j] == ',') { + comma = 1; + ++j; + } else if (isspace(s[j])) { + ++j; + } else { + if (comma) { + tmp = parse_json(ctx, self, str, s, &j, len); + if (sexp_exceptionp(tmp)) { + res = tmp; + break; + } else if (sexp_stringp(tmp)) { + tmp = sexp_string_to_symbol(ctx, tmp); + } + tmp = sexp_cons(ctx, tmp, SEXP_VOID); + while (j < len && isspace(s[j])) + ++j; + if (s[j] != ':') { + res = sexp_json_exception(ctx, self, "missing colon in json object at", str, j); + break; + } + ++j; + sexp_cdr(tmp) = parse_json(ctx, self, str, s, &j, len); + if (sexp_exceptionp(sexp_cdr(tmp))) { + res = sexp_cdr(tmp); + break; + } + res = sexp_cons(ctx, tmp, res); + comma = 0; + } else { + res = sexp_json_exception(ctx, self, "unexpected value in json object at", str, j); + break; + } + } + } + *i = j; + sexp_gc_release2(ctx); + return res; +} + +sexp parse_json (sexp ctx, sexp self, sexp str, const char* s, int* i, const int len) { + sexp res; + int j = *i; + while (j < len && isspace(s[j])) + ++j; + switch (s[j]) { + case '{': + ++j; + res = parse_json_object(ctx, self, str, s, &j, len); + break; + case '[': + ++j; + res = parse_json_array(ctx, self, str, s, &j, len); + break; + case '"': + ++j; + res = parse_json_string(ctx, self, str, s, &j, len); + break; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + res = parse_json_number(ctx, self, str, s, &j, len); + break; + case 'n': case 'N': + res = parse_json_literal(ctx, self, str, s, &j, len, "null", 4, SEXP_VOID); + break; + case 't': case 'T': + res = parse_json_literal(ctx, self, str, s, &j, len, "true", 4, SEXP_TRUE); + break; + case 'f': case 'F': + res = parse_json_literal(ctx, self, str, s, &j, len, "false", 5, SEXP_FALSE); + break; + case '}': + res = sexp_json_exception(ctx, self, "unexpected closing brace in json at", str, j); + break; + case ']': + res = sexp_json_exception(ctx, self, "unexpected closing bracket in json at", str, j); + break; + default: + res = sexp_json_exception(ctx, self, "unexpected character in json at", str, j); + break; + } + *i = j; + return res; +} + +sexp sexp_parse_json (sexp ctx, sexp self, sexp_sint_t n, sexp str) { + const char *s; + int i=0, len; + sexp_assert_type(ctx, sexp_stringp, SEXP_STRING, str); + s = sexp_string_data(str); + len = sexp_string_size(str); + return parse_json(ctx, self, str, s, &i, len); +} + +sexp sexp_init_library (sexp ctx, sexp self, sexp_sint_t n, sexp env, const char* version, const sexp_abi_identifier_t abi) { + if (!(sexp_version_compatible(ctx, version, sexp_version) + && sexp_abi_compatible(ctx, abi, SEXP_ABI_IDENTIFIER))) + return SEXP_ABI_ERROR; + sexp_define_foreign(ctx, env, "parse-json", 1, sexp_parse_json); + return SEXP_VOID; +} diff --git a/lib/chibi/json.sld b/lib/chibi/json.sld new file mode 100644 index 00000000..d5043a93 --- /dev/null +++ b/lib/chibi/json.sld @@ -0,0 +1,5 @@ + +(define-library (chibi json) + (import (scheme base)) + (export parse-json) + (include-shared "json"))