From 73b397f7de881c732b8aeeb0a5e5d913bcec5073 Mon Sep 17 00:00:00 2001 From: Calvin Rose Date: Sun, 26 Aug 2018 11:28:51 -0400 Subject: [PATCH] Add json native instead of hello. Remove metabuild code. --- Makefile | 8 +- {examples => lib}/lazyseqs.dst | 24 - lib/metabuild.dst | 103 --- natives/hello/.gitignore | 76 --- natives/hello/Makefile | 24 - natives/hello/main.c | 42 -- natives/hello/metab.dst | 5 - natives/json/Makefile | 44 ++ .../{hello/build.bat => json/build_win.bat} | 4 +- natives/json/json.c | 596 ++++++++++++++++++ 10 files changed, 648 insertions(+), 278 deletions(-) rename {examples => lib}/lazyseqs.dst (82%) delete mode 100644 lib/metabuild.dst delete mode 100644 natives/hello/.gitignore delete mode 100644 natives/hello/Makefile delete mode 100644 natives/hello/main.c delete mode 100755 natives/hello/metab.dst create mode 100644 natives/json/Makefile rename natives/{hello/build.bat => json/build_win.bat} (76%) create mode 100644 natives/json/json.c diff --git a/Makefile b/Makefile index 8353fad8..f08d2f8c 100644 --- a/Makefile +++ b/Makefile @@ -130,11 +130,11 @@ valtest: $(DST_TARGET) ################### natives: $(DST_TARGET) - $(MAKE) -C natives/hello + $(MAKE) -C natives/json $(MAKE) -j 8 -C natives/sqlite3 clean-natives: - $(MAKE) -C natives/hello clean + $(MAKE) -C natives/json clean $(MAKE) -C natives/sqlite3 clean ################# @@ -154,6 +154,10 @@ install: $(DST_TARGET) cp $(DST_LIBRARY) $(LIBDIR)/$(DST_LIBRARY) $(LDCONFIG) +install-libs: natives + cp lib/* $(DST_PATH) + cp natives/*/*.so $(DST_PATH) + uninstall: -rm $(BINDIR)/$(DST_TARGET) -rm $(LIBDIR)/$(DST_LIBRARY) diff --git a/examples/lazyseqs.dst b/lib/lazyseqs.dst similarity index 82% rename from examples/lazyseqs.dst rename to lib/lazyseqs.dst index 7c031c05..6ded10ec 100644 --- a/examples/lazyseqs.dst +++ b/lib/lazyseqs.dst @@ -105,27 +105,3 @@ (when x (def thehead (get HEAD x)) (if thehead (tuple thehead (take-while pred (get TAIL x))))))) - -# Iterators are a concept that looks a lot like lazy seq -# The following functions turn iterators to lazy seq and vice versa - -(defn- iter-self - [next more] - (delay - (if (more) (tuple (next) (iter-self next more))))) - -(defn iter2lazy - "Create a lazy sequence from an iterator" - [iter] - (def {:more more :next next} iter) - (iter-self next more)) - -(defn lazy2iter - "turn a lazy-seq to an iterator" - [lazy-seq] - (var node lazy-seq) - {:more (fn [] (node)) - :next (fn [] - (when-let [n (node)] - (:= node (get n 1)) - (get n 0)))}) diff --git a/lib/metabuild.dst b/lib/metabuild.dst deleted file mode 100644 index 41d7837f..00000000 --- a/lib/metabuild.dst +++ /dev/null @@ -1,103 +0,0 @@ -# A script to help generate build files on different platforms - -# The Makefile generator - -(def- make-cflags @["-std=c99" "-Wall" "-Wextra" "-O2" "-shared" "-fpic"]) -(def- make-ldflags @[]) - -(if (= :macos (os.which)) - (array.push make-cflags "undefined -dynamic_lookup")) - -(defn- $ - "Do and get the value of a subshell command" - [command] - (def f (file.popen command)) - (def ret (file.read f :all)) - (file.close f) - ret) - -(defn- emit-rule - "Emit a rule in a makefile" - @[out target deps recipe] - (default recipe "@echo '-----'") - (file.write - out - target - ": " - (if (indexed? deps) (string.join deps " ") deps) - "\n\t" - (if (indexed? recipe) (string.join recipe "\n\t") recipe) - "\n\n") - out) - -(defn- generate-make - "Generate a makefile" - [out-path sources target] - (def out (file.open out-path :w)) - (def csources (filter (fn [x] (= ".c" (string.slice x -2))) sources)) - (def hsources (filter (fn [x] (= ".h" (string.slice x -2))) sources)) - (file.write - out - "# Autogenerated Makefile, do not edit\n" - "# Generated at " ($ `date`) - "\nCFLAGS:=" (string.join make-cflags " ") - "\nLDFLAGS:=" (string.join make-ldflags " ") - "\nSOURCES:=" (string.join csources " ") - "\nHEADERS:=" (string.join hsources " ") - "\nOBJECTS:=$(patsubst %.c,%.o,${SOURCES})" - "\nTARGET:=" target ".so" - "\n\n") - (emit-rule out "all" "${TARGET}") - (emit-rule out "%.o" @["%.c" "${HEADERS}"] "${CC} ${CFLAGS} -o $@ $< ${LDFLAGS}") - (emit-rule out "${TARGET}" "${OBJECTS}" "${CC} ${CFLAGS} -o $@ $^ ${LDFLAGS}") - (emit-rule out "clean" "" "rm ${OBJECTS}") - (emit-rule out "clean" "" @["rm ${OBJECTS}" "rm @{TARGET}"]) - # Phony targets - (emit-rule out ".PHONY" @["all" "clean"]) - nil) - -# The batch script generator (windows) - -(defn- batch-crule - "Create a snippet to compile a single C file to an .obj file" - [file-path] - (string - `cl /nologo /I..\..\src\include /c /O2 /W3 ` - file-path - "\n@if errorlevel 1 goto :BUILDFAIL")) - -(defn- generate-batch - "Generate a batch file for windows" - [out-path sources target] - (def out (file.open out-path :w)) - (def csources (filter (fn [x] (= ".c" (string.slice x -2))) sources)) - (file.write - out - "@rem Generated batch script, run in 'Visual Studio Developer Prompt'\n" - "\n@rem \n\n" - "@echo off\n\n" - (string.join (map batch-crule csources) "\n\n") - "\n\n" - `link /nologo /dll ..\..\dst.lib /out:` target `.dll *.obj` - "\nif errorlevel 1 goto :BUILDFAIL" - "\n\n@echo .\n@echo ======\n@echo Build Succeeded.\n@echo =====\n" - "exit /b 0\n\n" - ":BUILDFAIL\n" - "@echo .\n" - "@echo =====\n" - "@echo BUILD FAILED. See Output For Details.\n" - "@echo =====\n" - "@echo .\n" - "exit /b 1\n") - (file.flush out) - (file.close out) - nil) - -(defn generate - "Generate the build files for a given library." - [out-path sources target] - ((if (= :windows (os.which)) generate-batch generate-make) - out-path - sources - target)) - diff --git a/natives/hello/.gitignore b/natives/hello/.gitignore deleted file mode 100644 index 65e5b53d..00000000 --- a/natives/hello/.gitignore +++ /dev/null @@ -1,76 +0,0 @@ - -# Created by https://www.gitignore.io/api/c - -### C ### -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf - - -# End of https://www.gitignore.io/api/c - -# Created by https://www.gitignore.io/api/cmake - -### CMake ### -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -build - - -# End of https://www.gitignore.io/api/cmake diff --git a/natives/hello/Makefile b/natives/hello/Makefile deleted file mode 100644 index 1261ba2b..00000000 --- a/natives/hello/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -# Autogenerated Makefile, do not edit -# Generated at Fri Aug 3 22:55:50 EDT 2018 - -CFLAGS:=-std=c99 -Wall -Wextra -O2 -shared -fpic -LDFLAGS:= -SOURCES:=main.c -HEADERS:= -OBJECTS:=$(patsubst %.c,%.o,${SOURCES}) -TARGET:=hello.so - -all: ${TARGET} - -%.o: %.c ${HEADERS} - ${CC} ${CFLAGS} -o $@ -c $< ${LDFLAGS} - -${TARGET}: ${OBJECTS} - ${CC} ${CFLAGS} -o $@ $^ ${LDFLAGS} - -clean: - rm ${OBJECTS} - rm ${TARGET} - -.PHONY: all clean - diff --git a/natives/hello/main.c b/natives/hello/main.c deleted file mode 100644 index fdf4ce4f..00000000 --- a/natives/hello/main.c +++ /dev/null @@ -1,42 +0,0 @@ -/* -* Copyright (c) 2017 Calvin Rose -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to -* deal in the Software without restriction, including without limitation the -* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -* sell copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in -* all copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -* IN THE SOFTWARE. -*/ - -#include - -static int hello(DstArgs args) { - (void) args; - printf("Hello, world!\n"); - return 0; -} - -/*****************************************************************************/ - -static const DstReg cfuns[] = { - {"hello", hello}, - {NULL, NULL} -}; - -DST_MODULE_ENTRY (DstArgs args) { - DstTable *env = dst_env_arg(args); - dst_env_cfuns(env, cfuns); - return 0; -} diff --git a/natives/hello/metab.dst b/natives/hello/metab.dst deleted file mode 100755 index eadb80c8..00000000 --- a/natives/hello/metab.dst +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env dst - -(import metabuild :as mb) - -(mb.generate "hello" @["main.c"]) diff --git a/natives/json/Makefile b/natives/json/Makefile new file mode 100644 index 00000000..616f34a2 --- /dev/null +++ b/natives/json/Makefile @@ -0,0 +1,44 @@ +# Copyright (c) 2018 Calvin Rose +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +CFLAGS:=-std=c99 -Wall -Wextra -O2 -shared -fpic +CFLAGS=-std=c99 -Wall -Wextra -I../../src/include -O2 -shared -fpic +OBJECTS:=json.o +TARGET:=json.so + +# MacOS specifics +UNAME:=$(shell uname -s) +ifeq ($(UNAME), Darwin) + CFLAGS:=$(CFLAGS) -undefined dynamic_lookup +endif + +all: $(TARGET) + +%.o: %.c $(HEADERS) + $(CC) $(CFLAGS) -c $< + +$(TARGET): $(OBJECTS) + $(CC) $(CFLAGS) -o $@ $^ + +clean: + rm $(OBJECTS) + rm $(TARGET) + +.PHONY: all clean diff --git a/natives/hello/build.bat b/natives/json/build_win.bat similarity index 76% rename from natives/hello/build.bat rename to natives/json/build_win.bat index 873b204f..9c2233f6 100644 --- a/natives/hello/build.bat +++ b/natives/json/build_win.bat @@ -4,10 +4,10 @@ @echo off -cl /nologo /I..\..\src\include /c /O2 /W3 hello.c +cl /nologo /I..\..\src\include /c /O2 /W3 json.c @if errorlevel 1 goto :BUILDFAIL -link /nologo /dll ..\..\dst.lib /out:hello.dll *.obj +link /nologo /dll ..\..\dst.lib /out:json.dll *.obj if errorlevel 1 goto :BUILDFAIL @echo . diff --git a/natives/json/json.c b/natives/json/json.c new file mode 100644 index 00000000..7731c556 --- /dev/null +++ b/natives/json/json.c @@ -0,0 +1,596 @@ +/* +* Copyright (c) 2018 Calvin Rose +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to +* deal in the Software without restriction, including without limitation the +* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +* sell copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +* IN THE SOFTWARE. +*/ + +#include +#include +#include + +/*****************/ +/* JSON Decoding */ +/*****************/ + +/* Check if a character is whitespace */ +static int white(uint8_t c) { + return c == '\t' || c == '\n' || c == ' ' || c == '\r'; +} + +/* Skip whitespace */ +static void skipwhite(const char **p) { + const char *cp = *p; + for (;;) { + if (white(*cp)) + cp++; + else + break; + } + *p = cp; +} + +/* Get a hex digit value */ +static int hexdig(char dig) { + if (dig >= '0' && dig <= '9') + return dig - '0'; + if (dig >= 'a' && dig <= 'f') + return 10 + dig - 'a'; + if (dig >= 'A' && dig <= 'F') + return 10 + dig - 'A'; + return -1; +} + +/* Read the hex value for a unicode escape */ +static char *decode_utf16_escape(const char *p, uint32_t *outpoint) { + if (!p[0] || !p[1] || !p[2] || !p[3]) + return "unexpected end of source"; + int d1 = hexdig(p[0]); + int d2 = hexdig(p[1]); + int d3 = hexdig(p[2]); + int d4 = hexdig(p[3]); + if (d1 < 0 || d2 < 0 || d3 < 0 || d4 < 0) + return "invalid hex digit"; + *outpoint = d4 | (d3 << 4) | (d2 << 8) | (d1 << 12); + return NULL; +} + +/* Parse a string */ +const char *decode_string(const char **p, Dst *out) { + DstBuffer *buffer = dst_buffer(0); + const char *cp = *p; + while (*cp != '"') { + uint8_t b = (uint8_t) *cp; + if (b < 32) return "invalid character in string"; + if (b == '\\') { + cp++; + switch(*cp) { + default: + return "unknown string escape"; + case 'b': + b = '\b'; + break; + case 'f': + b = '\f'; + break; + case 'n': + b = '\n'; + break; + case 'r': + b = '\r'; + break; + case 't': + b = '\t'; + break; + case '"': + b = '"'; + break; + case '\\': + b = '\\'; + break; + case 'u': + { + /* Get codepoint and check for surrogate pair */ + uint32_t codepoint; + const char *err = decode_utf16_escape(cp + 1, &codepoint); + if (err) return err; + if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) { + return "unexpected utf-16 low surrogate"; + } else if (codepoint >= 0xD800 && codepoint <= 0xDBFF) { + if (cp[5] != '\\') return "expected utf-16 low surrogate pair"; + if (cp[6] != 'u') return "expected utf-16 low surrogate pair"; + uint32_t lowsur; + const char *err = decode_utf16_escape(cp + 7, &lowsur); + if (err) return err; + if (lowsur < 0xDC00 || lowsur > 0xDFFF) + return "expected utf-16 low surrogate pair"; + codepoint = ((codepoint - 0xD800) << 10) + + (lowsur - 0xDC00) + 0x10000; + cp += 11; + } else { + cp += 5; + } + /* Write codepoint */ + if (codepoint <= 0x7F) { + dst_buffer_push_u8(buffer, codepoint); + } else if (codepoint <= 0x7FF) { + dst_buffer_push_u8(buffer, ((codepoint >> 6) & 0x1F) | 0xC0); + dst_buffer_push_u8(buffer, ((codepoint >> 0) & 0x3F) | 0x80); + } else if (codepoint <= 0xFFFF) { + dst_buffer_push_u8(buffer, ((codepoint >> 12) & 0x0F) | 0xE0); + dst_buffer_push_u8(buffer, ((codepoint >> 6) & 0x3F) | 0x80); + dst_buffer_push_u8(buffer, ((codepoint >> 0) & 0x3F) | 0x80); + } else { + dst_buffer_push_u8(buffer, ((codepoint >> 18) & 0x07) | 0xF0); + dst_buffer_push_u8(buffer, ((codepoint >> 12) & 0x3F) | 0x80); + dst_buffer_push_u8(buffer, ((codepoint >> 6) & 0x3F) | 0x80); + dst_buffer_push_u8(buffer, ((codepoint >> 0) & 0x3F) | 0x80); + } + } + continue; + } + } + dst_buffer_push_u8(buffer, b); + cp++; + } + *out = dst_stringv(buffer->data, buffer->count); + *p = cp + 1; + return NULL; +} + +static const char *decode_one(const char **p, Dst *out, int depth) { + + /* Prevent stack overflow */ + if (depth > DST_RECURSION_GUARD) goto recurdepth; + + /* Skip leading whitepspace */ + skipwhite(p); + + /* Main switch */ + switch (**p) { + default: + goto badchar; + case '\0': + goto eos; + /* Numbers */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + { + errno = 0; + char *end = NULL; + double x = strtod(*p, &end); + if (end == *p) goto badnum; + *p = end; + *out = dst_wrap_real(x); + break; + } + /* false, null, true */ + case 'f': + { + const char *cp = *p; + if (cp[1] != 'a' || cp[2] != 'l' || cp[3] != 's' || cp[4] != 'e') + goto badident; + *out = dst_wrap_false(); + *p = cp + 5; + break; + } + case 'n': + { + const char *cp = *p; + + if (cp[1] != 'u' || cp[2] != 'l' || cp[3] != 'l') + goto badident; + *out = dst_wrap_nil(); + *p = cp + 4; + break; + } + case 't': + { + const char *cp = *p; + if (cp[1] != 'r' || cp[2] != 'u' || cp[3] != 'e') + goto badident; + *out = dst_wrap_true(); + *p = cp + 4; + break; + } + /* String */ + case '"': + { + const char *cp = *p + 1; + const char *start = cp; + while (*cp >= 32 && *cp != '"' && *cp != '\\') + cp++; + /* Only use a buffer for strings with escapes, else just copy + * memory from source */ + if (*cp == '\\') { + *p = *p + 1; + const char *err = decode_string(p, out); + if (err) return err; + break; + } + if (*cp != '"') goto badchar; + *p = cp + 1; + *out = dst_stringv((const uint8_t *)start, cp - start); + break; + } + /* Array */ + case '[': + { + *p = *p + 1; + DstArray *array = dst_array(0); + const char *err; + Dst subval; + skipwhite(p); + while (**p != ']') { + err = decode_one(p, &subval, depth + 1); + if (err) return err; + dst_array_push(array, subval); + skipwhite(p); + if (**p == ']') break; + if (**p != ',') goto wantcomma; + *p = *p + 1; + } + *p = *p + 1; + *out = dst_wrap_array(array); + } + break; + /* Object */ + case '{': + { + *p = *p + 1; + DstTable *table = dst_table(0); + const char *err; + Dst subkey, subval; + skipwhite(p); + while (**p != '}') { + skipwhite(p); + if (**p != '"') goto wantstring; + err = decode_one(p, &subkey, depth + 1); + if (err) return err; + skipwhite(p); + if (**p != ':') goto wantcolon; + *p = *p + 1; + err = decode_one(p, &subval, depth + 1); + if (err) return err; + dst_table_put(table, subkey, subval); + skipwhite(p); + if (**p == '}') break; + if (**p != ',') goto wantcomma; + *p = *p + 1; + } + *p = *p + 1; + *out = dst_wrap_table(table); + break; + } + } + + /* Good return */ + return NULL; + + /* Errors */ +recurdepth: + return "recured too deeply"; +eos: + return "unexpected end of source"; +badident: + return "bad identifier"; +badnum: + return "bad number"; +wantcomma: + return "expected comma"; +wantcolon: + return "expected colon"; +badchar: + return "unexpected character"; +wantstring: + return "expected json string"; +} + +static int json_decode(DstArgs args) { + Dst ret; + DST_FIXARITY(args, 1); + const char *err; + const char *start; + const char *p; + if (dst_checktype(args.v[0], DST_BUFFER)) { + DstBuffer *buffer = dst_unwrap_buffer(args.v[0]); + /* Ensure 0 padded */ + dst_buffer_push_u8(buffer, 0); + start = p = (const char *)buffer->data; + err = decode_one(&p, &ret, 0); + buffer->count--; + } else { + const uint8_t *bytes; + int32_t len; + DST_ARG_BYTES(bytes, len, args, 0); + start = p = (const char *)bytes; + err = decode_one(&p, &ret, 0); + } + /* Check trailing values */ + if (!err) { + skipwhite(&p); + if (*p) + err = "unexpected extra token"; + } + if (err) { + DST_THROWV(args, dst_wrap_string(dst_formatc( + "decode error at postion %d: %s", + p - start, + err))); + } + DST_RETURN(args, ret); +} + +/*****************/ +/* JSON Encoding */ +/*****************/ + +typedef struct { + DstBuffer *buffer; + int32_t indent; + const uint8_t *tab; + const uint8_t *newline; + int32_t tablen; + int32_t newlinelen; +} Encoder; + +static const char *encode_newline(Encoder *e) { + if (dst_buffer_push_bytes(e->buffer, e->newline, e->newlinelen)) + return "buffer overflow"; + for (int32_t i = 0; i < e->indent; i++) + if (dst_buffer_push_bytes(e->buffer, e->tab, e->tablen)) + return "buffer overflow"; + return NULL; +} + +static const char *encode_one(Encoder *e, Dst x, int depth) { + switch(dst_type(x)) { + default: + goto badtype; + case DST_NIL: + { + if (dst_buffer_push_cstring(e->buffer, "null")) + goto overflow; + } + break; + case DST_FALSE: + { + if (dst_buffer_push_cstring(e->buffer, "false")) + goto overflow; + } + break; + case DST_TRUE: + { + if (dst_buffer_push_cstring(e->buffer, "true")) + goto overflow; + } + break; + case DST_INTEGER: + { + char cbuf[20]; + sprintf(cbuf, "%d", dst_unwrap_integer(x)); + if (dst_buffer_push_cstring(e->buffer, cbuf)) + goto overflow; + } + break; + case DST_REAL: + { + char cbuf[25]; + sprintf(cbuf, "%.17g", dst_unwrap_real(x)); + if (dst_buffer_push_cstring(e->buffer, cbuf)) + goto overflow; + } + break; + case DST_STRING: + case DST_SYMBOL: + case DST_BUFFER: + { + const uint8_t *bytes; + const uint8_t *c; + const uint8_t *end; + int32_t len; + dst_bytes_view(x, &bytes, &len); + if (dst_buffer_push_u8(e->buffer, '"')) goto overflow; + c = bytes; + end = bytes + len; + while (c < end) { + + /* get codepoint */ + uint32_t codepoint; + if (*c < 0x80) { + /* one byte */ + codepoint = *c++; + } else if (*c < 0xE0) { + /* two bytes */ + if (c + 2 > end) goto overflow; + codepoint = ((c[0] & 0x1F) << 6) | + (c[1] & 0x3F); + c += 2; + } else if (*c < 0xF0) { + /* three bytes */ + if (c + 3 > end) goto overflow; + codepoint = ((c[0] & 0x0F) << 12) | + ((c[1] & 0x3F) << 6) | + (c[2] & 0x3F); + c += 3; + } else if (*c < 0xF8) { + /* four bytes */ + if (c + 4 > end) goto overflow; + codepoint = ((c[0] & 0x07) << 18) | + ((c[1] & 0x3F) << 12) | + ((c[3] & 0x3F) << 6) | + (c[3] & 0x3F); + c += 4; + } else { + /* invalid */ + goto invalidutf8; + } + + /* write codepoint */ + if (codepoint > 0x1F && codepoint < 0x80) { + /* Normal, no escape */ + if (codepoint == '\\' || codepoint == '"') + if (dst_buffer_push_u8(e->buffer, '\\')) + goto overflow; + if (dst_buffer_push_u8(e->buffer, (uint8_t) codepoint)) + goto overflow; + } else if (codepoint < 0x10000) { + /* One unicode escape */ + uint8_t buf[6]; + buf[0] = '\\'; + buf[1] = 'u'; + buf[2] = (codepoint >> 12) & 0xF; + buf[3] = (codepoint >> 8) & 0xF; + buf[4] = (codepoint >> 4) & 0xF; + buf[5] = codepoint & 0xF; + if (dst_buffer_push_bytes(e->buffer, buf, sizeof(buf))) + goto overflow; + } else { + /* Two unicode escapes (surrogate pair) */ + uint32_t hi, lo; + uint8_t buf[12]; + hi = ((codepoint - 0x10000) >> 10) + 0xD800; + lo = ((codepoint - 0x10000) & 0x3FF) + 0xDC00; + buf[0] = '\\'; + buf[1] = 'u'; + buf[2] = (hi >> 12) & 0xF; + buf[3] = (hi >> 8) & 0xF; + buf[4] = (hi >> 4) & 0xF; + buf[5] = hi & 0xF; + buf[6] = '\\'; + buf[7] = 'u'; + buf[8] = (lo >> 12) & 0xF; + buf[9] = (lo >> 8) & 0xF; + buf[10] = (lo >> 4) & 0xF; + buf[11] = lo & 0xF; + if (dst_buffer_push_bytes(e->buffer, buf, sizeof(buf))) + goto overflow; + } + } + if (dst_buffer_push_u8(e->buffer, '"')) goto overflow; + } + break; + case DST_TUPLE: + case DST_ARRAY: + { + const char *err; + const Dst *items; + int32_t len; + dst_indexed_view(x, &items, &len); + if (dst_buffer_push_u8(e->buffer, '[')) goto overflow; + e->indent++; + if ((err = encode_newline(e))) return err; + for (int32_t i = 0; i < len; i++) { + if ((err = encode_newline(e))) return err; + if ((err = encode_one(e, items[i], depth + 1))) + return err; + if (dst_buffer_push_u8(e->buffer, ',')) + goto overflow; + } + e->indent--; + if (e->buffer->data[e->buffer->count - 1] == ',') { + e->buffer->count--; + if ((err = encode_newline(e))) return err; + } + if (dst_buffer_push_u8(e->buffer, ']')) goto overflow; + } + break; + case DST_TABLE: + case DST_STRUCT: + { + const char *err; + const DstKV *kvs; + int32_t count, capacity; + dst_dictionary_view(x, &kvs, &count, &capacity); + if (dst_buffer_push_u8(e->buffer, '{')) goto overflow; + e->indent++; + for (int32_t i = 0; i < capacity; i++) { + if (dst_checktype(kvs[i].key, DST_NIL)) + continue; + if (!dst_checktype(kvs[i].key, DST_STRING)) + return "only strings keys are allowed in objects"; + if ((err = encode_newline(e))) return err; + if ((err = encode_one(e, kvs[i].key, depth + 1))) + return err; + const char *sep = e->tablen ? ": " : ":"; + if (dst_buffer_push_cstring(e->buffer, sep)) + goto overflow; + if ((err = encode_one(e, kvs[i].value, depth + 1))) + return err; + if (dst_buffer_push_u8(e->buffer, ',')) + goto overflow; + } + e->indent--; + if (e->buffer->data[e->buffer->count - 1] == ',') { + e->buffer->count--; + if ((err = encode_newline(e))) return err; + } + if (dst_buffer_push_u8(e->buffer, '}')) goto overflow; + } + break; + } + return NULL; + + /* Errors */ +overflow: + return "buffer overflow"; +badtype: + return "type not supported"; +invalidutf8: + return "string contains invalid utf-8"; +} + +static int json_encode(DstArgs args) { + DST_MINARITY(args, 1); + DST_MAXARITY(args, 3); + Encoder e; + e.indent = 0; + e.buffer = dst_buffer(10); + e.tab = NULL; + e.newline = NULL; + e.tablen = 0; + e.newlinelen = 0; + if (args.n >= 2) { + DST_ARG_BYTES(e.tab, e.tablen, args, 1); + if (args.n >= 3) { + DST_ARG_BYTES(e.newline, e.newlinelen, args, 2); + } else { + e.newline = (const uint8_t *)"\n"; + e.newlinelen = 1; + } + } + const char *err = encode_one(&e, args.v[0], 0); + if (err) DST_THROW(args, err); + DST_RETURN_BUFFER(args, e.buffer); +} + +/****************/ +/* Module Entry */ +/****************/ + +static const DstReg cfuns[] = { + {"encode", json_encode}, + {"decode", json_decode}, + {NULL, NULL} +}; + +DST_MODULE_ENTRY(DstArgs args) { + DstTable *env = dst_env_arg(args); + dst_env_cfuns(env, cfuns); + return 0; +}