changeset 682:117603444601

sexpr: allow compacting list and alists into arrays and nvlists While sexprs are flexible, at times they are a bit annoying to use. The easiest way to improve their usability is to allow compaction of alists and lists into nvlists and arrays, respectively. Signed-off-by: Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
author Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
date Tue, 17 Apr 2018 20:17:14 -0400
parents f51d1b524af0
children b3a4ac14a22d
files CMakeLists.txt include/jeffpc/sexpr.h mapfile-vers sexpr_compact.c tests/.hgignore tests/CMakeLists.txt tests/test_sexpr_compact.c
diffstat 7 files changed, 493 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Sun Feb 24 17:51:10 2019 -0500
+++ b/CMakeLists.txt	Tue Apr 17 20:17:14 2018 -0400
@@ -107,6 +107,7 @@
 	rbtree.c
 	scgisvc.c
 	sexpr.c
+	sexpr_compact.c
 	sexpr_dump.c
 	sexpr_eval.c
 	${FLEX_sexpr_OUTPUTS} ${BISON_sexpr_OUTPUTS}
--- a/include/jeffpc/sexpr.h	Sun Feb 24 17:51:10 2019 -0500
+++ b/include/jeffpc/sexpr.h	Tue Apr 17 20:17:14 2018 -0400
@@ -66,6 +66,9 @@
 extern struct val *sexpr_assoc(struct val *val, const char *name);
 extern bool sexpr_equal(struct val *lhs, struct val *rhs);
 
+/* compact cons-lists/alists into VT_ARRAY/VT_NVL */
+extern struct val *sexpr_compact(struct val *in);
+
 extern struct val *sexpr_eval(struct val *val, struct sexpr_eval_env *);
 
 /*
--- a/mapfile-vers	Sun Feb 24 17:51:10 2019 -0500
+++ b/mapfile-vers	Tue Apr 17 20:17:14 2018 -0400
@@ -178,6 +178,7 @@
 		sexpr_assoc;
 		sexpr_car;
 		sexpr_cdr;
+		sexpr_compact;
 		sexpr_dump;
 		sexpr_dump_file;
 		sexpr_equal;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sexpr_compact.c	Tue Apr 17 20:17:14 2018 -0400
@@ -0,0 +1,381 @@
+/*
+ * Copyright (c) 2018-2019 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * 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 <jeffpc/sexpr.h>
+#include <jeffpc/nvl.h>
+#include <jeffpc/mem.h>
+
+#include "sexpr_impl.h"
+
+/*
+ * Compact an array.
+ *
+ * Iterate over all the elements converting each.  If at the end, we end up
+ * with the same array we free the new one we made, and just return the
+ * old.
+ *
+ * Note: Under no circumstance can we modify the array in-place.  It may
+ * have multiple references and we do not wish to modify all of them - only
+ * the one that was passed in.
+ */
+static struct val *sexpr_compact_array(struct val *in)
+{
+	struct val **vals;
+	size_t nelem;
+	bool equal;
+	size_t i;
+
+	VERIFY3U(in->type, ==, VT_ARRAY);
+
+	nelem = in->array.nelem;
+
+	vals = alloca(sizeof(struct val *) * nelem);
+
+	equal = true;
+
+	for (i = 0; i < nelem; i++) {
+		vals[i] = sexpr_compact(val_getref(in->array.vals[i]));
+
+		/*
+		 * We compare pointers on purpose.  This checks whether or
+		 * not the sexpr_compact call above was a no-op.
+		 */
+		equal = equal && (vals[i] == in->array.vals[i]);
+	}
+
+	if (equal) {
+		for (i = 0; i < nelem; i++)
+			val_putref(vals[i]);
+
+		return in;
+	} else {
+		val_putref(in);
+
+		return val_alloc_array_dup(vals, nelem);
+	}
+}
+
+/*
+ * Compact an nvlist.
+ *
+ * Iterate over all the nv pairs converting each.  If at the end, we end up
+ * with the same nvlist we free the new one we made, and just return the
+ * old.
+ *
+ * Note: Under no circumstance can we modify the nvlist in-place.  It may
+ * have multiple references and we do not wish to modify all of them - only
+ * the one that was passed in.
+ */
+static struct val *sexpr_compact_nvl(struct val *in)
+{
+	const struct nvpair *pair;
+	struct nvlist *src, *dst;
+	struct val *ret_ptr;
+	bool equal;
+
+	VERIFY3U(in->type, ==, VT_NVL);
+
+	src = val_cast_to_nvl(in);
+
+	dst = nvl_alloc();
+	if (IS_ERR(dst)) {
+		ret_ptr = ERR_CAST(dst);
+		goto err;
+	}
+
+	equal = true;
+
+	nvl_for_each(pair, src) {
+		struct nvpair tmp;
+		int ret;
+
+		tmp.name = pair->name;
+		tmp.value = sexpr_compact(val_getref(pair->value));
+		if (IS_ERR(tmp.value)) {
+			ret_ptr = tmp.value;
+			goto err_dst;
+		}
+
+		/*
+		 * We compare pointers on purpose.  This checks whether or
+		 * not the sexpr_compact call above was a no-op.
+		 */
+		equal = equal && (tmp.value == pair->value);
+
+		/* takes a new ref on both name & val */
+		ret = nvl_set_pair(dst, &tmp);
+
+		val_putref(tmp.value); /* release our ref */
+
+		if (ret) {
+			ret_ptr = ERR_PTR(ret);
+			goto err_dst;
+		}
+	}
+
+	if (equal) {
+		nvl_putref(dst);
+		return in;
+	} else {
+		nvl_putref(src);
+		return nvl_cast_to_val(dst);
+	}
+
+err_dst:
+	nvl_putref(dst);
+err:
+	val_putref(in);
+	return ret_ptr;
+}
+
+/*
+ * Compact a cons list into an array.
+ *
+ * We assert a number of return values because the calls should never fail.
+ * A failure means that our what-is-it logic is incorrect.  In that case, it
+ * is better to assert than to incorrectly mangle the data.
+ */
+static struct val *sexpr_compact_cons_array(struct val *in)
+{
+	struct val **arr;
+	ssize_t len;
+	size_t i;
+	int ret;
+
+	len = sexpr_length(val_getref(in));
+
+	/* assert, to catch issues with what-is-it logic */
+	ASSERT3S(len, >=, 0);
+
+	arr = alloca(sizeof(struct val *) * len);
+
+	ret = sexpr_list_to_array(in, arr, len);
+
+	/* assert, to catch issues with what-is-it logic */
+	ASSERT3S(ret, ==, len);
+
+	/* compact each element */
+	for (i = 0; i < len; i++) {
+		arr[i] = sexpr_compact(arr[i]);
+		if (IS_ERR(arr[i])) {
+			ret = PTR_ERR(arr[i]);
+			goto err;
+		}
+	}
+
+	/* make a new VT_ARRAY with the compacted values */
+	return val_alloc_array_dup(arr, len);
+
+err:
+	for (i = 0; i < len; i++) {
+		if (IS_ERR(arr[i]))
+			continue;
+
+		val_putref(arr[i]);
+	}
+
+	return ERR_PTR(ret);
+}
+
+/*
+ * Compact a cons alist into an nvlist.
+ */
+static struct val *sexpr_compact_cons_nvl(struct val *in)
+{
+	struct val *cur, *tmp;
+	struct nvlist *out;
+	int ret;
+
+	out = nvl_alloc();
+	if (IS_ERR(out)) {
+		val_putref(in);
+		return nvl_cast_to_val(out);
+	}
+
+	/*
+	 * compact each element
+	 *
+	 * For example:
+	 *
+	 *   ((a . b) (c . d))
+	 *
+	 * will cause iterations (cur):
+	 *
+	 *   (a . b)
+	 *   (c . d)
+	 */
+	sexpr_for_each_noref(cur, tmp, in) {
+		struct val *name = cur->cons.head;
+		struct val *value = cur->cons.tail;
+		struct nvpair new;
+
+		if (name->type == VT_STR)
+			new.name = val_getref_str(name);
+		else
+			new.name = str_dup(val_cstr(name));
+
+		new.value = sexpr_compact(val_getref(value));
+		if (IS_ERR(new.value)) {
+			str_putref(new.name);
+			ret = PTR_ERR(new.value);
+			goto err;
+		}
+
+		/* takes a new ref on both name & value */
+		ret = nvl_set_pair(out, &new);
+
+		/* release our refs */
+		str_putref(new.name);
+		val_putref(new.value);
+
+		if (ret)
+			goto err;
+	}
+
+	val_putref(in);
+
+	return nvl_cast_to_val(out);
+
+err:
+	val_putref(in);
+	nvl_putref(out);
+
+	return ERR_PTR(ret);
+}
+
+
+/*
+ * Tries to figure out what sort of structure the passed in cons cell
+ * represents.
+ *
+ * Returns:
+ *    VT_NULL  - empty cons (technically convertible to either nvlist or array)
+ *    VT_NVL   - an alist that's convertible to an nvlist
+ *    VT_ARRAY - a list that's convertible to an array
+ *    VT_CONS  - no conversion can be made
+ */
+static enum val_type what_is_it(struct val *in)
+{
+	struct val *cur;
+	int nvl;
+
+	if (sexpr_is_null(in))
+		return VT_NULL;
+
+	nvl = true;
+
+	/*
+	 * Iterate over the cons list checking each cell for whether it is a
+	 * part of a list (i.e., cdr is cons) and whether it is a part of an
+	 * alist (i.e., car is a cons with a string key)
+	 *
+	 * We don't use sexpr_for_each here because it stops iteration of
+	 * non-lists early.
+	 */
+	for (cur = in; !sexpr_is_null(cur); cur = cur->cons.tail) {
+		struct val *item;
+
+		if (cur->type != VT_CONS)
+			return VT_CONS;
+
+		/* a cons cell = we have a list */
+
+		/*
+		 * Don't bother checking for nvl-ness if we already found at
+		 * least one issue earlier.
+		 */
+		if (!nvl)
+			continue;
+
+		item = cur->cons.head;
+
+		/* Is the current item a name-value cons? */
+
+		/* ...is it a cons? */
+		if (sexpr_is_null(item) || (item->type != VT_CONS)) {
+			nvl = false;
+			continue;
+		}
+
+		/* ...does it have a string/symbol name? */
+		if (sexpr_is_null(item->cons.head) ||
+		    ((item->cons.head->type != VT_STR) &&
+		     (item->cons.head->type != VT_SYM))) {
+			nvl = false;
+			continue;
+		}
+
+		/* looks like an nvlist pair, keep going */
+	}
+
+	return nvl ? VT_NVL : VT_ARRAY;
+}
+
+static struct val *sexpr_compact_cons(struct val *in)
+{
+	VERIFY3U(in->type, ==, VT_CONS);
+
+	switch (what_is_it(in)) {
+		case VT_CONS:
+			/* no conversion possible */
+			return in;
+		case VT_NULL:
+			/* an empty cons; leave it as-is */
+			return in;
+		case VT_ARRAY:
+			return sexpr_compact_cons_array(in);
+		case VT_NVL:
+			return sexpr_compact_cons_nvl(in);
+		case VT_INT:
+		case VT_STR:
+		case VT_SYM:
+		case VT_BOOL:
+		case VT_CHAR:
+		case VT_BLOB:
+			break;
+	}
+
+	panic("impossible cons compaction target");
+}
+
+struct val *sexpr_compact(struct val *in)
+{
+	switch (in->type) {
+		case VT_NULL:
+		case VT_INT:
+		case VT_BOOL:
+		case VT_STR:
+		case VT_SYM:
+		case VT_CHAR:
+		case VT_BLOB:
+			/* nothing to do */
+			return in;
+		case VT_ARRAY:
+			return sexpr_compact_array(in);
+		case VT_NVL:
+			return sexpr_compact_nvl(in);
+		case VT_CONS:
+			return sexpr_compact_cons(in);
+	}
+
+	panic("Cannot compact unknown val type (%d)", in->type);
+}
--- a/tests/.hgignore	Sun Feb 24 17:51:10 2019 -0500
+++ b/tests/.hgignore	Tue Apr 17 20:17:14 2018 -0400
@@ -35,6 +35,7 @@
 test_padding
 test_qstring
 test_sexpr_parser
+test_sexpr_compact
 test_sexpr_eval
 test_sexpr_iter
 test_str2uint
--- a/tests/CMakeLists.txt	Sun Feb 24 17:51:10 2019 -0500
+++ b/tests/CMakeLists.txt	Tue Apr 17 20:17:14 2018 -0400
@@ -61,6 +61,7 @@
 endif()
 build_test_bin_and_run(nvl)
 build_test_bin_and_run(padding)
+build_test_bin_and_run(sexpr_compact)
 build_test_bin_and_run(sexpr_eval)
 build_test_bin_and_run(sexpr_iter)
 build_test_bin_and_run(str2uint)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_sexpr_compact.c	Tue Apr 17 20:17:14 2018 -0400
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2019 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * 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 <jeffpc/sexpr.h>
+
+#include "test.c"
+
+static const struct test {
+	const char *in;
+	enum val_type type;
+	bool same;
+} tests[] = {
+	{
+		.in = "()",
+		.type = VT_CONS,
+		.same = true,
+	},
+	{
+		.in = "(1 2 3)",
+		.type = VT_ARRAY,
+		.same = false,
+	},
+	{
+		.in = "((\"a\" . b) (\"c\" . d))",
+		.type = VT_NVL,
+		.same = false,
+	},
+	{
+		.in = "((a . b) (c . d))",
+		.type = VT_NVL,
+		.same = false,
+	},
+	{
+		.in = "((a b) . (c d))",
+		.type = VT_ARRAY,
+		.same = false,
+	},
+	{
+		.in = "((a b) . d)",
+		.type = VT_CONS,
+		.same = true,
+	},
+};
+
+void test(void)
+{
+	size_t i;
+
+	for (i = 0; i < ARRAY_LEN(tests); i++) {
+		const struct test *run = &tests[i];
+		struct val *in;
+		struct val *out;
+
+		fprintf(stderr, "%2zu: %s, expecting %s %s...\n", i, run->in,
+			run->same ? "identical" : "new",
+			val_typename(run->type));
+
+		in = sexpr_parse(run->in, strlen(run->in));
+		ASSERT(!IS_ERR(in));
+
+		fprintf(stderr, "    ");
+		sexpr_dump_file(stderr, in, false);
+		fprintf(stderr, " <== parsed/reformated sexpr\n");
+
+		out = sexpr_compact(val_getref(in));
+		ASSERT(!IS_ERR(out));
+
+		val_dump_file(stderr, out, 1);
+
+		if (run->same && (in != out))
+			fail("compaction was not a no-op");
+		if (!run->same && (in == out))
+			fail("compaction was a no-op");
+
+		if (out->type != run->type)
+			fail("compaction returned wrong type "
+			     "(exp: %s, got: %s)",
+			     val_typename(run->type),
+			     val_typename(out->type));
+
+		val_putref(in);
+		val_putref(out);
+
+		fprintf(stderr, "%2zu: ok.\n", i);
+	}
+}