OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace-storage / opencog / persist / sexpr / ValueSexpr.cc

539 lines · cc ref:
/*
 * ValueSexpr.cc
 * Encode/decode S-Expressions for Atomese Values.
 *
 * Copyright (c) 2019 Linas Vepstas <linas@linas.org>
 * SPDX-License-Identifier: AGPL-3.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
#include <iomanip>

#include <opencog/atoms/base/Atom.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atoms/value/StringValue.h>
#include <opencog/atoms/value/VoidValue.h>
#include <opencog/atoms/value/ValueFactory.h>
#include <opencog/atomspace/AtomSpace.h>

#include "Sexpr.h"

using namespace opencog;

/* ================================================================== */
/**
 * Look for a type name, either of the form "ConceptNode" (with quotes)
 * or 'ConceptNode (symbol) starting at location `pos` in `tna`.
 * Return the type and update `pos` to point after the typename.
 */
Type Sexpr::decode_type(const std::string& tna, size_t& pos)
{
	// Advance past whitespace.
	pos = tna.find_first_not_of(" \n\t", pos);
	if (std::string::npos == pos)
		throw SyntaxException(TRACE_INFO, "Bad Type >>%s<<",
			tna.substr(pos).c_str());

	// Advance to next whitespace.
	size_t nos = tna.find_first_of(") \n\t", pos);
	if (std::string::npos == nos)
		nos = tna.size();

	size_t sos = nos;
	if ('\'' == tna[pos]) pos++;
	if ('"' == tna[pos]) { pos++; sos--; }

	Type t = nameserver().getType(tna.substr(pos, sos-pos));
	if (NOTYPE == t)
		throw SyntaxException(TRACE_INFO, "Unknown Type >>%s<<",
			tna.substr(pos, sos-pos).c_str());

	pos = nos;
	return t;
}

/* ================================================================== */

/**
 * Return a Value corresponding to the input string.
 * It is assumed the input string is encoded as a scheme string.
 * For example, `(FloatValue 1 2 3 4)` or more complex things:
 * `(LinkValue (Concept "a") (FloatValue 1 2 3))`
 *
 * The `pos` should point at the open-paren of the value-string.
 * Upon return, `pos` is updated to point at the matching closing paren.
 *
 * It is currently assumed that there is no whitespace between the
 * open-paren, and the string encoding the value.
 *
 * XXX FIXME This needs to be fuzzed; it is very likely to crash
 * and/or contain bugs if it is given strings of unexpected formats.
 */
ValuePtr Sexpr::decode_value(const std::string& stv, size_t& pos)
{
	size_t totlen = stv.size();

	// Skip past whitespace
	pos = stv.find_first_not_of(" \n\t", pos);

	// Special-case: Both #f and '() are used to denote "no value".
	// This is commonly used to erase keys from atoms. So handle this
	// first.
	if (0 == stv.compare(pos, 2, "#f"))
	{
		pos += 2;
		return nullptr;
	}
	if (0 == stv.compare(pos, 3, "'()"))
	{
		pos += 3;
		return nullptr;
	}

	// What kind of value is it?
	// Increment pos by one to point just after the open-paren.
	// (VoidValue) will not have whitespace before the close-paren.
	size_t vos = stv.find_first_of(" \n\t)", ++pos);
	if (std::string::npos == vos)
		throw SyntaxException(TRACE_INFO, "Badly formatted Value %s",
			stv.substr(pos).c_str());

	Type vtype = nameserver().getType(stv.substr(pos, vos-pos));
	if (NOTYPE == vtype)
	{
		throw SyntaxException(TRACE_INFO, "Unknown Value >>%s<<",
			stv.substr(pos, vos-pos).c_str());
	}

	if (nameserver().isA(vtype, ATOM))
	{
		// Decrement pos by one to point at the open-paren.
		return decode_atom(stv, --pos);
	}

	// LinkValues are vectors of Values.
	if (nameserver().isA(vtype, LINK_VALUE))
	{
		std::vector<ValuePtr> vv;
		vos = stv.find('(', vos);
		size_t epos = vos;
		size_t done = vos + 1;
		while (vos != std::string::npos and vos < done)
		{
			// Find the next balanced paren, and restart there.
			// This is not very efficient, but it works.
			epos = vos;
			int pcnt = 1;
			while (0 < pcnt and epos < totlen)
			{
				char c = stv[++epos];

				// Advance past escaped quotes.
				if ('"' == c)
				{
					++epos;
					// Search for ending quote, advancing past escaped quotes.
					while (epos < totlen and stv[epos] != '"')
					{
						if (stv[epos] == '\\') epos++;
						epos++;
					}
					continue;
				}
				if ('(' == c) pcnt ++;
				else if (')' == c) pcnt--;
			}
			if (epos >= totlen)
				throw SyntaxException(TRACE_INFO,
					"Malformed LinkValue: %s", stv.substr(pos).c_str());

			vv.push_back(decode_value(stv, vos));
			done = stv.find(')', epos+1);
			vos = stv.find('(', epos+1);
		}
		if (std::string::npos == done)
			throw SyntaxException(TRACE_INFO,
				"Malformed LinkValue: %s", stv.substr(pos).c_str());
		pos = done + 1;
		return valueserver().create(vtype, vv);
	}

	if (nameserver().isA(vtype, FLOAT_VALUE))
	{
		std::vector<double> fv;
		while (vos < totlen and stv[vos] != ')')
		{
			size_t epos;
			fv.push_back(stod(stv.substr(vos), &epos));
			vos += epos;
		}
		pos = vos + 1;

		return valueserver().create(vtype, fv);
	}

	// BoolValue can be either "(BoolValue 1 0 1 ...)" or "(BoolValue #x...)"
	if (nameserver().isA(vtype, BOOL_VALUE))
	{
		std::vector<bool> bv;
		vos = stv.find_first_not_of(" \n\t", vos);

		// Handle hex format: (BoolValue #x...)
		if (vos < totlen and stv[vos] == '#' and vos+1 < totlen and stv[vos+1] == 'x')
		{
			vos += 2; // Skip "#x"
			// Parse hex digits until we hit ')'
			while (vos < totlen and stv[vos] != ')')
			{
				char c = stv[vos];
				if (std::isxdigit(c))
				{
					int nibble = (c >= '0' and c <= '9') ? c - '0' :
					             (c >= 'a' and c <= 'f') ? c - 'a' + 10 :
					             (c >= 'A' and c <= 'F') ? c - 'A' + 10 : 0;
					// Each hex digit represents 4 bits
					for (int bit = 3; bit >= 0; bit--)
						bv.push_back((nibble >> bit) & 1);
				}
				vos++;
			}
		}
		else
		{
			// Handle individual bits: (BoolValue 1 0 1 ...)
			while (vos < totlen and stv[vos] != ')')
			{
				vos = stv.find_first_not_of(" \n\t", vos);
				if (vos >= totlen or stv[vos] == ')') break;
				if (stv[vos] == '0') bv.push_back(false);
				else if (stv[vos] == '1') bv.push_back(true);
				vos++;
			}
		}
		pos = vos + 1;
		return valueserver().create(vtype, bv);
	}

	// Unescape escaped quotes
	if (nameserver().isA(vtype, STRING_VALUE))
	{
		std::vector<std::string> sv;
		size_t p = vos+1;

		// p is pointing to the opening quote.
		while ('"' == stv[p])
		{
			size_t e = p + 1;

			// Search for ending quote, advancing past escaped quotes.
			while (e < totlen and stv[e] != '"')
			{
				if (stv[e] == '\\') e++;
				e++;
			}
			e++; // Now e points just past the close-quote.
			if (totlen <= e)
				throw SyntaxException(TRACE_INFO,
					"Malformed StringValue: %s", stv.substr(vos).c_str());

			// Unescape quotes in the string.
			std::stringstream ss;
			std::string val;
			ss << stv.substr(p, e-p);
			ss >> quoted(val);
			sv.push_back(val);

			// Skip past whitespace
			p = stv.find_first_not_of(" \n\t", e);
		}
		if (')' != stv[p])
			throw SyntaxException(TRACE_INFO,
				"Missing closing paren at %zu (after %zu strings) in StringValue: %s",
				p, sv.size(), stv.substr(vos).c_str());

		pos = ++p;
		return valueserver().create(vtype, sv);
	}

	if (nameserver().isA(vtype, VOID_VALUE))
	{
		pos = vos + 1;
		return createVoidValue();
	}

	throw SyntaxException(TRACE_INFO, "Unsupported decode of Value %s",
		stv.substr(pos, vos-pos).c_str());
}

/* ================================================================== */

/**
 * Decode a Valuation association list.
 * This list has the format
 * ((KEY . VALUE)(KEY2 . VALUE2)...)
 * Store the results as values on the atom.
 */
void Sexpr::decode_alist(const Handle& atom,
                         const std::string& alist, size_t& pos)
{
	AtomSpace* as = atom->getAtomSpace();

	pos = alist.find_first_not_of(" \n\t", pos);
	if (std::string::npos == pos) return;
	if ('(' != alist[pos])
		throw SyntaxException(TRACE_INFO, "Badly formed alist: %s",
			alist.substr(pos).c_str());

	// Skip over opening paren
	pos++;
	size_t totlen = alist.size();
	pos = alist.find('(', pos);
	while (std::string::npos != pos and pos < totlen)
	{
		++pos;  // over first paren of pair
		Handle key(decode_atom(alist, pos));

		pos = alist.find(" . ", pos);
		pos += 3;
		ValuePtr val(decode_value(alist, pos));

		// Make sure all atoms have found a nice home.
		if (as)
		{
			Handle hkey = as->add_atom(key);
			if (hkey) key = hkey; // might be null, if `as` is read-only
			val = add_atoms(as, val);
			as->set_value(atom, key, val);
		}
		else
			atom->setValue(key, val);
		pos = alist.find('(', pos);
	}
}

/* ================================================================== */

/**
 * Decode a Valuation association list.
 * This list has the format
 *    (alist (cons KEY VALUE)(cons KEY2 VALUE2)...)
 * Store the results as values on the atom.
 */
void Sexpr::decode_slist(const Handle& atom,
                         const std::string& alist, size_t& pos)
{
	AtomSpace* as = atom->getAtomSpace();

	pos = alist.find_first_not_of(" \n\t", pos);
	if (std::string::npos == pos) return;
	if (alist.compare(pos, 6, "(alist"))
		throw SyntaxException(TRACE_INFO, "Badly formed alist: %s",
			alist.substr(pos).c_str());

	size_t totlen = alist.size();
	size_t nxt = alist.find("(cons ", pos);
	while (std::string::npos != nxt and nxt < totlen)
	{
		nxt += 5;
		nxt = alist.find_first_not_of(" \n\t", nxt);
		Handle key(decode_atom(alist, nxt));
		nxt++;
		nxt = alist.find_first_not_of(" \n\t", nxt);
		ValuePtr val(decode_value(alist, nxt));
		pos = nxt + 1;   // move past closing paren of (cons ...)
		if (as)
		{
			// Make sure all atoms have found a nice home.
			Handle hkey = as->add_atom(key);
			if (hkey) key = hkey; // might be null, if `as` is read-only
			val = add_atoms(as, val);
			as->set_value(atom, key, val);
		}
		else
			atom->setValue(key, val);
		nxt = alist.find("(cons ", nxt);
	}

	// Move past closing parent of (alist ...)
	pos = alist.find(")", pos);
	pos++;
}

/* ================================================================== */
// Atom printers that do NOT print associated Values.

static std::string prt_node(const Handle& h, bool multispace)
{
	std::stringstream ss;
	ss << "(" << nameserver().getTypeName(h->get_type())
		<< " " << std::quoted(h->get_name());

	if (multispace and h->getAtomSpace())
		ss << " (AtomSpace \"" << h->getAtomSpace()->get_name() << "\")";

	ss << ")";

	return ss.str();
}

static std::string prt_atom(const Handle&, bool);

static std::string prt_link(const Handle& h, bool multispace)
{
	std::string txt = "(" + nameserver().getTypeName(h->get_type()) + " ";
	for (const Handle& ho : h->getOutgoingSet())
		txt += prt_atom(ho, multispace);

	if (multispace and h->getAtomSpace())
		txt += " (AtomSpace \"" + h->getAtomSpace()->get_name() + "\")";

	txt += ")";
	return txt;
}

static std::string prt_atom(const Handle& h, bool multispace)
{
	if (h->is_node()) return prt_node(h, multispace);
	return prt_link(h, multispace);
}

/// Convert the Atom into a string. It does NOT print any of the
/// associated values; use `dump_atom()` to get those.
std::string Sexpr::encode_atom(const Handle& h, bool multispace)
{
	return prt_atom(h, multispace);
}

/// Convert value (or Atom) into a string.
std::string Sexpr::encode_value(const ValuePtr& v)
{
	// Empty values are used to erase keys from atoms.
	if (nullptr == v) return " #f";

	if (not v->is_atom())
		return v->to_short_string();
	return prt_atom(HandleCast(v), false);
}

/* ================================================================== */

/// Get all of the values on an Atom and print them as an
/// association list.
std::string Sexpr::encode_atom_values(const Handle& h)
{
	std::stringstream rv;

	rv << "(alist ";
	for (const Handle& k: h->getKeys())
	{
		ValuePtr p = h->getValue(k);
		rv << "(cons " << prt_atom(k, false) << encode_value(p) << ")";
	}
	rv << ")";
	return rv.str();
}

/* ================================================================== */
// Atom printers that encode ALL associated Values.

static std::string dump_node(const Handle& h)
{
	std::stringstream ss;
	ss << "(" << nameserver().getTypeName(h->get_type())
		<< " " << std::quoted(h->get_name());

	if (h->haveValues())
		ss << " " << Sexpr::encode_atom_values(h);

	ss << ")";

	return ss.str();
}

static std::string dump_link(const Handle& h)
{
	std::string txt = "(" + nameserver().getTypeName(h->get_type()) + " ";
	for (const Handle& ho : h->getOutgoingSet())
		txt += prt_atom(ho, false);

	if (h->haveValues())
	{
		txt += " ";
		txt += Sexpr::encode_atom_values(h);
	}

	txt += ")";
	return txt;
}

/// Print the Atom, and all of the values attached to it.
/// Similar to `encode_atom()`, except that it also prints the values.
/// Values on going Atoms in a Link are NOT dumped!
/// This is in order to avoid duplication.
std::string Sexpr::dump_atom(const Handle& h)
{
	if (h->is_node()) return dump_node(h);
	return dump_link(h);
}

/* ================================================================== */
// Atom printers that encode only one associated Value.

static std::string dump_vnode(const Handle& h, const Handle& key)
{
	std::stringstream ss;
	ss << "(" << nameserver().getTypeName(h->get_type())
		<< " " << std::quoted(h->get_name());

	ValuePtr p = h->getValue(key);
	if (nullptr != p)
		ss << " (alist (cons " << prt_atom(key, false) << Sexpr::encode_value(p) << ")))";

	return ss.str();
}

static std::string dump_vlink(const Handle& h, const Handle& key)
{
	std::string txt = "(" + nameserver().getTypeName(h->get_type()) + " ";
	for (const Handle& ho : h->getOutgoingSet())
		txt += prt_atom(ho, false);

	ValuePtr p = h->getValue(key);
	if (nullptr != p)
		txt += " (alist (cons " + prt_atom(key, false) + Sexpr::encode_value(p) + ")))";

	return txt;
}

/// Print the Atom, and just one of the values attached to it.
std::string Sexpr::dump_vatom(const Handle& h, const Handle& key)
{
	if (h->is_node()) return dump_vnode(h, key);
	return dump_vlink(h, key);
}

/* ================================================================== */

/// Make sure that any Atoms appearing buried in the value have found
/// a nice home to live in.
ValuePtr Sexpr::add_atoms(AtomSpace* as, const ValuePtr& vptr)
{
	return as->add_atoms(vptr);
}

/* ============================= END OF FILE ================= */