Browse Source

Merge branch 'lua' of scossu/lsup_rdf into master

scossu 2 weeks ago
parent
commit
a63f30321c

+ 22 - 0
Makefile

@@ -33,6 +33,7 @@ LDFLAGS := -L$(libdir) -L$(outdir) -L. -llmdb -lxxhash -luuid
 PARSER = bin/lemon
 PARSER = bin/lemon
 LEMON_SRC = ext/sqlite/tool/lemon.c
 LEMON_SRC = ext/sqlite/tool/lemon.c
 CODEC_DIR = src/codec
 CODEC_DIR = src/codec
+LUA_DIR = lua
 
 
 # External sources compiled in core object.
 # External sources compiled in core object.
 EXT_SRC := $(wildcard ext/log/src/*.c) \
 EXT_SRC := $(wildcard ext/log/src/*.c) \
@@ -98,9 +99,12 @@ help:
 		| column -t  -s '|'
 		| column -t  -s '|'
 	
 	
 
 
+
+.PHONY: lib
 lib: codec $(LIBS) ## Compile main library (static and dynamic linking).
 lib: codec $(LIBS) ## Compile main library (static and dynamic linking).
 
 
 
 
+.PHONY: debug
 debug: codec_dbg $(DBG_LIBS) ## Compile main library with debug symbols.
 debug: codec_dbg $(DBG_LIBS) ## Compile main library with debug symbols.
 
 
 
 
@@ -250,3 +254,21 @@ depgraph: $(LSUP_SRC) $(CODEC_SRC) include/* include/codec/* ## Build a visual d
 	cinclude2dot --merge=module --include=$(DEPS) \
 	cinclude2dot --merge=module --include=$(DEPS) \
 		--exclude='test|ext' >| $(DOCS)/dev/deps.dot
 		--exclude='test|ext' >| $(DOCS)/dev/deps.dot
 	dot $(DOCS)/dev/deps.dot -Tpdf >| $(DOCS)/dev/deps.pdf
 	dot $(DOCS)/dev/deps.dot -Tpdf >| $(DOCS)/dev/deps.pdf
+
+
+################
+# Lua bindings #
+################
+
+
+.PHONY: lua
+lua:
+	$(MAKE) -C $(LUA_DIR) lib
+
+.PHONY: lua_install
+lua_install:
+	$(MAKE) -C $(LUA_DIR) install
+
+.PHONY: lua_uninstall
+lua_uninstall:
+	$(MAKE) -C $(LUA_DIR) uninstall

+ 5 - 0
include/buffer.h

@@ -151,11 +151,16 @@ LSUP_buffer_new_borrowed (unsigned char *data, const size_t size)
 
 
 
 
 /** @brief Free the content of a buffer.
 /** @brief Free the content of a buffer.
+ *
+ * @note This function is safe to use on borrowed buffers.
  */
  */
 void LSUP_buffer_done (LSUP_Buffer *buf);
 void LSUP_buffer_done (LSUP_Buffer *buf);
 
 
 
 
 /** @brief Free a buffer.
 /** @brief Free a buffer.
+ *
+ * @note This function is safe to use on borrowed buffers. In such case, only
+ *  the `LSUP_Buffer` structure is freed, but not the underlying data.
  */
  */
 void LSUP_buffer_free (LSUP_Buffer *buf);
 void LSUP_buffer_free (LSUP_Buffer *buf);
 
 

+ 2 - 1
include/codec.h

@@ -21,10 +21,11 @@ typedef struct codec_t LSUP_Codec;
 
 
 
 
 /// Parser state.
 /// Parser state.
-typedef struct {
+typedef struct ttl_parser_state {
     LSUP_GraphIterator *    it;     ///< Iterator used to build the graph.
     LSUP_GraphIterator *    it;     ///< Iterator used to build the graph.
     LSUP_NSMap *            nsm;    ///< NS map used in the document.
     LSUP_NSMap *            nsm;    ///< NS map used in the document.
     LSUP_Term *             base;   ///< Base IRI used in the document.
     LSUP_Term *             base;   ///< Base IRI used in the document.
+    LSUP_Term *             lms;    ///< Link map subject.
     size_t                  ct;     ///< Statements parsed.
     size_t                  ct;     ///< Statements parsed.
     LSUP_rc                 rc;     ///< Internal return code.
     LSUP_rc                 rc;     ///< Internal return code.
 } LSUP_TTLParserState;
 } LSUP_TTLParserState;

+ 1 - 1
include/core.h

@@ -106,7 +106,7 @@ typedef int LSUP_rc;
  * with a different form or value. The caller should find the value of the
  * with a different form or value. The caller should find the value of the
  * existing resource to be different than the one that was attempted to store.
  * existing resource to be different than the one that was attempted to store.
  * If this is returned from the iteration of multiple updates, it means that
  * If this is returned from the iteration of multiple updates, it means that
- * other resources in the loop may have changed state and the operation as a
+ * some resources in the loop may have changed state and the operation as a
  * whole completed successfully.
  * whole completed successfully.
  *
  *
  * The error-level counterpart to this is #LSUP_CONFLICT_ERR.
  * The error-level counterpart to this is #LSUP_CONFLICT_ERR.

+ 23 - 12
include/graph.h

@@ -99,7 +99,21 @@ LSUP_graph_copy_contents_txn (
         const LSUP_Term *s, const LSUP_Term *p, const LSUP_Term *o);
         const LSUP_Term *s, const LSUP_Term *p, const LSUP_Term *o);
 
 
 
 
-/// Non-transactional version of #LSUP_graph_copy_contents_txn().
+/* @brief Copy all triples from a graph (non-transactional).
+ *
+ * This is a shortcut for #LSUP_graph_copy_contents_txn(). If you need to
+ * specify a transaction handle for the copy, use that.
+ *
+ * @param[in] src Source graph.
+ *
+ * @param[in] dest Destination graph.
+ *
+ * @param[in] s, p, o Terms to look up for filtering. Any and all terms can be
+ * NULL, which indicate unbound terms.
+ *
+ * @return LSUP_OK on success; LSUP_NOACTION if no triples were copied; <0
+ *  if an error occurred.
+ */
 #define LSUP_graph_copy_contents(...) \
 #define LSUP_graph_copy_contents(...) \
     LSUP_graph_copy_contents_txn (NULL, __VA_ARGS__)
     LSUP_graph_copy_contents_txn (NULL, __VA_ARGS__)
 
 
@@ -397,7 +411,7 @@ LSUP_graph_iter_free (LSUP_GraphIterator *it);
  * @param[in] gr Graph handle to display.
  * @param[in] gr Graph handle to display.
  */
  */
 void
 void
-LSUP_graph_print (LSUP_Graph *gr);
+LSUP_graph_print (const LSUP_Graph *gr);
 
 
 
 
 /** @brief Get term pairs connected to a term in a graph.
 /** @brief Get term pairs connected to a term in a graph.
@@ -419,7 +433,7 @@ LSUP_graph_print (LSUP_Graph *gr);
  */
  */
 LSUP_LinkMap *
 LSUP_LinkMap *
 LSUP_graph_connections (
 LSUP_graph_connections (
-        const LSUP_Graph *gr, LSUP_Term *t, LSUP_LinkType type);
+        const LSUP_Graph *gr, const LSUP_Term *t, const LSUP_LinkType type);
 
 
 
 
 /** @brief Get a list of terms related to a term pair in a graph.
 /** @brief Get a list of terms related to a term pair in a graph.
@@ -438,8 +452,8 @@ LSUP_graph_connections (
  */
  */
 LSUP_TermSet *
 LSUP_TermSet *
 LSUP_graph_term_set (
 LSUP_graph_term_set (
-        const LSUP_Graph *gr, LSUP_Term *t1, LSUP_TriplePos t1_pos,
-        LSUP_Term *t2, LSUP_TriplePos t2_pos);
+        const LSUP_Graph *gr, const LSUP_Term *t1, const LSUP_TriplePos t1_pos,
+        const LSUP_Term *t2, const LSUP_TriplePos t2_pos);
 
 
 
 
 /** @brief Get all unique subjcts, predicates, or objects in a graph.
 /** @brief Get all unique subjcts, predicates, or objects in a graph.
@@ -452,23 +466,20 @@ LSUP_TermSet *
 LSUP_graph_unique_terms (const LSUP_Graph *gr, LSUP_TriplePos pos);
 LSUP_graph_unique_terms (const LSUP_Graph *gr, LSUP_TriplePos pos);
 
 
 
 
-/** @brief Add triples for a term and related connection list to a graph.
+/** @brief Add triples for a term and related link map to a graph.
  *
  *
- * The connection list can be of inbound, outbound, or edge type; depending on
+ * The link map can be of inbound, outbound, or edge type; depending on
  * that, triples are added with the given term as the subject, the predicate,
  * that, triples are added with the given term as the subject, the predicate,
  * or the object.
  * or the object.
  *
  *
  * @param[in] it Graph iterator obtained with #LSUP_graph_add_init_txn().
  * @param[in] it Graph iterator obtained with #LSUP_graph_add_init_txn().
  *
  *
- * @param[in] t Term to be associated with the collection list.
- *
- * @param[in] cl Link map.
+ * @param[in] lm Link map.
  *
  *
  * @return Number of triples parsed on success, or <0 (LSUP_*_ERR) on error.
  * @return Number of triples parsed on success, or <0 (LSUP_*_ERR) on error.
  */
  */
 size_t
 size_t
-LSUP_graph_add_link_map (
-        LSUP_GraphIterator *it, LSUP_Term *t, LSUP_LinkMap *cl);
+LSUP_graph_add_link_map ( LSUP_GraphIterator *it, LSUP_LinkMap *lm);
 
 
 
 
 /** @brief Add triples for an anonymous collection to a graph.
 /** @brief Add triples for an anonymous collection to a graph.

+ 2 - 0
include/namespace.h

@@ -20,6 +20,8 @@
  *
  *
  * It contains a double hash map of pfx->ns and ns->pfx for fast 2-way lookup.
  * It contains a double hash map of pfx->ns and ns->pfx for fast 2-way lookup.
  *
  *
+ * FIXME that's not true, currently ns lookup by prefix involves an iteration.
+ *
  * Prefixes are fixed PFX_LEN-size strings, namespaces are arbitrary sized
  * Prefixes are fixed PFX_LEN-size strings, namespaces are arbitrary sized
  * strings.
  * strings.
  */
  */

+ 33 - 37
include/store_interface.h

@@ -156,20 +156,20 @@ typedef LSUP_rc (*store_txn_begin_fn_t)(void *store, int flags, void **txn);
  *
  *
  * Only for LSUP_STORE_TXN stores.
  * Only for LSUP_STORE_TXN stores.
  *
  *
- * @param[in] store Store handle.
+ * @param[in] txn Transaction handle initialized by #store_txn_begin_fn_t().
  *
  *
  * @return LSUP_OK if the transaction was committed successfully, <0 on error.
  * @return LSUP_OK if the transaction was committed successfully, <0 on error.
  */
  */
-typedef LSUP_rc (*store_txn_commit_fn_t)(void *store);
+typedef LSUP_rc (*store_txn_commit_fn_t)(void *txn);
 
 
 
 
 /** @brief Abort a transaction.
 /** @brief Abort a transaction.
  *
  *
  * Only for LSUP_STORE_TXN stores.
  * Only for LSUP_STORE_TXN stores.
  *
  *
- * @param[in] store Store handle.
+ * @param[in] txn Transaction handle initialized by #store_txn_begin_fn_t().
  */
  */
-typedef void (*store_txn_abort_fn_t)(void *store);
+typedef void (*store_txn_abort_fn_t)(void *txn);
 
 
 
 
 /** @brief Update the context of triples in a context-aware store.
 /** @brief Update the context of triples in a context-aware store.
@@ -402,17 +402,18 @@ typedef LSUP_NSMap * (*store_nsm_get_fn_t)(void *store);
 
 
 /** @brief Prototype: yield the matching triples and advance the iterator.
 /** @brief Prototype: yield the matching triples and advance the iterator.
  *
  *
- * NOTE: Iterators keep transactions open. Don't hold on to them longer than
+ * @note Iterators keep transactions open. Don't hold on to them longer than
  * necessary.
  * necessary.
  *
  *
- * NOTE: If the store interface has the LSUP_STORE_COW feature, the memory
+ * @note If the store interface has the LSUP_STORE_COW feature, the memory
  * buffer referenced by the #LSUP_Buffer handle is owned by the database. It
  * buffer referenced by the #LSUP_Buffer handle is owned by the database. It
  * must not be written to or freed. To modify the data or use them beyond the
  * must not be written to or freed. To modify the data or use them beyond the
  * caller's scope, this memory must be copied. Note that the #LSUP_Buffer
  * caller's scope, this memory must be copied. Note that the #LSUP_Buffer
  * handle must still be freed (with a plain `free()`), but not the underlying
  * handle must still be freed (with a plain `free()`), but not the underlying
- * data buffer, since only the latter is owned by the back end. For stores
- * without the LSUP_STORE_COW, data are copied on retrieval and the resulting
- * buffers can be freed with #LSUP_buffer_free() or analogous methods.
+ * data buffer, since only the latter is owned by the back end. Implementations
+ * with the `LSUP_STORE_COW` feature SHOULD create buffers with the
+ * `LSUP_BUF_BORROWED` flag, so that they can be safely freed with
+ * #LSUP_buffer_free() and #LSUP_btriple_free().
  *
  *
  * @param[in] it Opaque iterator handle obtained with #store_lookup_fn_t.
  * @param[in] it Opaque iterator handle obtained with #store_lookup_fn_t.
  *
  *
@@ -484,27 +485,24 @@ typedef struct store_if_t {
     iter_txn_fn_t       iter_txn_fn;    ///< Get iterator's transaction.
     iter_txn_fn_t       iter_txn_fn;    ///< Get iterator's transaction.
 
 
     //Context setting.
     //Context setting.
-    store_update_ctx_fn_t update_ctx_fn; /**< Update context URI.
-                                         *
-                                         *   Only available in stores with
-                                         *   #LSUP_STORE_CTX feature. Optional.
-                                         */
+    store_update_ctx_fn_t update_ctx_fn; ///< Update context URI.
+                                         ///<
+                                         ///< Only available in stores with
+                                         ///< #LSUP_STORE_CTX feature. Optional.
 
 
     // Addition.
     // Addition.
     store_add_init_fn_t add_init_fn;    ///< Initialize add iteration.
     store_add_init_fn_t add_init_fn;    ///< Initialize add iteration.
     store_add_iter_fn_t add_iter_fn;    ///< Add one triple.
     store_add_iter_fn_t add_iter_fn;    ///< Add one triple.
-    store_add_abort_fn_t add_abort_fn;  /**< Abort (roll back) the add process.
-                                         *
-                                         *   Only available in
-                                         *   stores with #LSUP_STORE_TXN
-                                         *   feature. Optional.
-                                         */
+    store_add_abort_fn_t add_abort_fn;  ///< Abort (roll back) the add process.
+                                        ///<
+                                        ///< Only available in
+                                        ///< stores with #LSUP_STORE_TXN
+                                        ///< feature. Optional.
     store_add_done_fn_t add_done_fn;    ///< Complete the add process.
     store_add_done_fn_t add_done_fn;    ///< Complete the add process.
-    store_add_term_fn_t add_term_fn;    /**< Add (index) a term to the store.
-                                         *
-                                         *   Only available in stores with
-                                         *   #LSUP_STORE_IDX feature. Optional.
-                                         */
+    store_add_term_fn_t add_term_fn;    ///< Add (index) a term to the store.
+                                        ///<
+                                        ///< Only available in stores with
+                                        ///< #LSUP_STORE_IDX feature. Optional.
 
 
     // Look up.
     // Look up.
     store_lookup_fn_t   lookup_fn;      ///< Look up triples by pattern. 
     store_lookup_fn_t   lookup_fn;      ///< Look up triples by pattern. 
@@ -516,18 +514,16 @@ typedef struct store_if_t {
     store_remove_fn_t   remove_fn;      ///< Remove triples by pattern.
     store_remove_fn_t   remove_fn;      ///< Remove triples by pattern.
 
 
     // Namespace prefix mapping.
     // Namespace prefix mapping.
-    store_nsm_put_fn_t  nsm_put_fn;     /**< Add a ns/pfx pair to the map.
-                                         *
-                                         *  Only available (and mandatory)
-                                         *  in stores with the
-                                         *  #LSUP_STORE_IDX feature.
-                                         */
-    store_nsm_get_fn_t  nsm_get_fn;     /**< Get a namespace from the map.
-                                         *
-                                         *  Only available (and mandatory)
-                                         *  in stores with the
-                                         *  #LSUP_STORE_IDX feature.
-                                         */
+    store_nsm_put_fn_t  nsm_put_fn;     ///< Add a ns/pfx pair to the map.
+                                        ///<
+                                        ///< Only available (and mandatory)
+                                        ///< in stores with the
+                                        ///< #LSUP_STORE_IDX feature.
+    store_nsm_get_fn_t  nsm_get_fn;     ///< Get a namespace from the map.
+                                        ///<
+                                        ///< Only available (and mandatory)
+                                        ///< in stores with the
+                                        ///< #LSUP_STORE_IDX feature.
 } LSUP_StoreInt;
 } LSUP_StoreInt;
 
 
 
 

+ 36 - 38
include/term.h

@@ -30,10 +30,9 @@ typedef char LSUP_LangTag[8];
 
 
 /// Term type.
 /// Term type.
 typedef enum {
 typedef enum {
-    LSUP_TERM_UNDEFINED = 0,/**<
-                              * Undefined placeholder or result of an error.
-                              * Invalid for most operations.
-                              */
+    LSUP_TERM_UNDEFINED = 0,///< Undefined placeholder or result of an error.
+                            ///<
+                            ///< Invalid for most operations.
     LSUP_TERM_IRIREF,       ///< IRI reference.
     LSUP_TERM_IRIREF,       ///< IRI reference.
     LSUP_TERM_NS_IRIREF,    ///< Namespace-prefixed IRI reference.
     LSUP_TERM_NS_IRIREF,    ///< Namespace-prefixed IRI reference.
     LSUP_TERM_LITERAL,      ///< Literal without language tag.
     LSUP_TERM_LITERAL,      ///< Literal without language tag.
@@ -49,14 +48,14 @@ typedef struct link_map_iter LSUP_LinkMapIterator;
 
 
 /// RDF term.
 /// RDF term.
 typedef struct term_t {
 typedef struct term_t {
-    char *              data;       // URI, literal value, or BNode label.
+    char *              data;       ///< URI, literal value, or BNode label.
     union {
     union {
-        struct term_t * datatype;   // Data type IRI for LSUP_TERM_LITERAL.
-        LSUP_LangTag    lang;       // Lang tag for LSUP_TERM_LT_LITERAL.
-        LSUP_Key        bnode_id;   // BNode ID for comparison & skolemization.
-        LSUP_IRIInfo *  iri_info;   // IRI information structure.
+        struct term_t * datatype;   ///< Data type IRI for LSUP_TERM_LITERAL.
+        LSUP_LangTag    lang;       ///< Lang tag for LSUP_TERM_LT_LITERAL.
+        LSUP_Key        bnode_id;   ///< BNode ID for comparison & skolemization.
+        LSUP_IRIInfo *  iri_info;   ///< IRI information structure.
     };
     };
-    LSUP_TermType       type;       // Term type.
+    LSUP_TermType       type;       ///< Term type.
 } LSUP_Term;
 } LSUP_Term;
 
 
 
 
@@ -105,11 +104,13 @@ typedef enum {
  *
  *
  * If the type of the link map is `LSUP_LINK_INBOUND`, the map keys
  * If the type of the link map is `LSUP_LINK_INBOUND`, the map keys
  * represent predicates and the sets related to them are the objects, and the
  * represent predicates and the sets related to them are the objects, and the
- * term associated to the link map is the object; if
- * `LSUP_LINK_OUTBOUND`, the keys represent predicates, the related sets
- * objects, and the associated term is the subject. If `LSUP_LINK_EDGE`, the
- * keys represent subjects and the related sets objects, and the associated
- * term is the predicate.
+ * term associated to the link map is the object.
+ *
+ * If the type is `LSUP_LINK_OUTBOUND`, the keys represent predicates, the
+ * related sets objects, and the associated term is the subject.
+ *
+ * If the type is `LSUP_LINK_EDGE`, the keys represent subjects and the related
+ * sets objects, and the associated term is the predicate.
  */
  */
 typedef struct link_map LSUP_LinkMap;
 typedef struct link_map LSUP_LinkMap;
 
 
@@ -271,7 +272,8 @@ LSUP_lt_literal_new (const char *data, char *lang)
  *
  *
  * Must be freed with #LSUP_term_free.
  * Must be freed with #LSUP_term_free.
  *
  *
- * @param[in] data The BNode identifier.
+ * @param[in] data The BNode identifier. It can be NULL, in which case, a
+ *  random identifier is minted.
  *
  *
  * @return same as #LSUP_term_new().
  * @return same as #LSUP_term_new().
  */
  */
@@ -455,7 +457,7 @@ LSUP_triple_free (LSUP_Triple *spo);
  * @return Corresponding triple term or NULL if n is out of range.
  * @return Corresponding triple term or NULL if n is out of range.
  */
  */
 inline LSUP_Term *
 inline LSUP_Term *
-LSUP_triple_pos (const LSUP_Triple *trp, LSUP_TriplePos n)
+LSUP_triple_pos (const LSUP_Triple *trp, const LSUP_TriplePos n)
 {
 {
     if (n == TRP_POS_S) return trp->s;
     if (n == TRP_POS_S) return trp->s;
     if (n == TRP_POS_P) return trp->p;
     if (n == TRP_POS_P) return trp->p;
@@ -549,27 +551,26 @@ LSUP_term_set_next (LSUP_TermSet *ts, size_t *i, LSUP_Term **term);
 
 
 /** @brief New link map.
 /** @brief New link map.
  *
  *
- * The initial state of the returned list is: `{t: [NULL], tl: [NULL]}`
- *
- * Terms can be added to a term list with #LSUP_term_set_add().
+ * @param[in] linked_term Term to be linked to map. The term is copied and may
+ *  be freed after this function call.
  *
  *
  * @param[in] type Type of links that the link map shall contain.
  * @param[in] type Type of links that the link map shall contain.
  * @sa #LSUP_LinkType
  * @sa #LSUP_LinkType
  *
  *
- * @return a new empty predicate-object list.
+ * @return a new empty link map.
  */
  */
 LSUP_LinkMap *
 LSUP_LinkMap *
-LSUP_link_map_new (LSUP_LinkType type);
+LSUP_link_map_new (const LSUP_Term *linked_term, LSUP_LinkType type);
 
 
 
 
 /** @brief Free a link map.
 /** @brief Free a link map.
  *
  *
  * All arrays and term handles are recursively freed.
  * All arrays and term handles are recursively freed.
  *
  *
- * @param[in] pol link map handle obtained with #LSUP_link_map_new().
+ * @param[in] lm link map handle obtained with #LSUP_link_map_new().
  */
  */
 void
 void
-LSUP_link_map_free (LSUP_LinkMap *pol);
+LSUP_link_map_free (LSUP_LinkMap *lm);
 
 
 
 
 /** @brief Return the link map type.
 /** @brief Return the link map type.
@@ -589,34 +590,27 @@ LSUP_link_map_type (const LSUP_LinkMap *map);
  * In any case, the caller should not directly use the term and term set after
  * In any case, the caller should not directly use the term and term set after
  * passing them to this function.
  * passing them to this function.
  *
  *
- * @param[in] cmap Link map handle obtained with #LSUP_link_map_new().
+ * @param[in] lmap Link map handle obtained with #LSUP_link_map_new().
  *
  *
  * @param[in] term Term to be associated with the given object list. The
  * @param[in] term Term to be associated with the given object list. The
- *  link map structure takes ownership of the term.
+ *  link map takes ownership of the term.
  *
  *
  * @param[in] tset term set to be associated with the given term. The link
  * @param[in] tset term set to be associated with the given term. The link
- *  list structire takes ownership of the term set and the terms in it.
+ *  map takes ownership of the term set and the terms in it.
  *
  *
  * @return LSUP_OK on success; LSUP_MEM_ERR on allocation error.
  * @return LSUP_OK on success; LSUP_MEM_ERR on allocation error.
  */
  */
 LSUP_rc
 LSUP_rc
 LSUP_link_map_add (
 LSUP_link_map_add (
-        LSUP_LinkMap *cmap, LSUP_Term *term, LSUP_TermSet *tset);
+        LSUP_LinkMap *lmap, LSUP_Term *term, LSUP_TermSet *tset);
 
 
 
 
 /** @brief Create a new iterator to loop through a link map.
 /** @brief Create a new iterator to loop through a link map.
  *
  *
  * @param[in] lmap Map handle to iterate.
  * @param[in] lmap Map handle to iterate.
- *
- * @param[in] ext External term to look for connections.
  */
  */
 LSUP_LinkMapIterator *
 LSUP_LinkMapIterator *
-LSUP_link_map_iter_new (const LSUP_LinkMap *lmap, LSUP_Term *ext);
-
-
-/// Free a link map iterator.
-void
-LSUP_link_map_iter_free (LSUP_LinkMapIterator *it);
+LSUP_link_map_iter_new (const LSUP_LinkMap *lmap);
 
 
 
 
 /** @brief Iterate through a link map.
 /** @brief Iterate through a link map.
@@ -637,6 +631,11 @@ LSUP_link_map_next (
         LSUP_LinkMapIterator *it, LSUP_Term **lt, LSUP_TermSet **ts);
         LSUP_LinkMapIterator *it, LSUP_Term **lt, LSUP_TermSet **ts);
 
 
 
 
+/// Free a link map iterator.
+void
+LSUP_link_map_iter_free (LSUP_LinkMapIterator *it);
+
+
 /**@brief Iterate over a link map and generate triples.
 /**@brief Iterate over a link map and generate triples.
  *
  *
  * Calling this function repeatedly builds triples for all the linked terms and
  * Calling this function repeatedly builds triples for all the linked terms and
@@ -654,8 +653,7 @@ LSUP_link_map_next (
  *  has been reached; <0 on error.
  *  has been reached; <0 on error.
  */
  */
 LSUP_rc
 LSUP_rc
-LSUP_link_map_triples (
-        LSUP_LinkMapIterator *it, LSUP_Triple *spo);
+LSUP_link_map_triples (LSUP_LinkMapIterator *it, LSUP_Triple *spo);
 
 
 ///@} END defgroup term
 ///@} END defgroup term
 #endif
 #endif

+ 48 - 0
lua/Makefile

@@ -0,0 +1,48 @@
+PREFIX=/usr/local
+
+INCLUDE = -I/usr/local/include/lsup
+CFLAGS = -shared -DDEBUG -Og -ggdb -Wall -fPIC
+LDFLAGS = -L/usr/local/lib -llua -llsuprdf_dbg
+
+LUAC_SRC = $(wildcard src/*.c)
+#OBJ = $(patsubst src/%.c, lib/%.so, $(LUAC_SRC))
+OBJ = lsup.so
+OBJPATH = lib/$(OBJ)
+
+LUA_VER=5.4
+INSTALL_DIR=$(PREFIX)/lib/lua/$(LUA_VER)
+
+VALGRIND_LOG=/tmp/lua_lsup_valgrind.log
+
+.DEFAULT_GOAL := lib
+
+
+.PHONY: lib
+lib: $(OBJPATH)
+
+
+$(OBJPATH): src/*.c src/lua_lsup.h
+	$(CC) $(INCLUDE) $(CFLAGS) $(LDFLAGS) -o $@ src/*.c
+
+
+install: $(OBJPATH)
+	mkdir -p $(INSTALL_DIR)
+	cp $(OBJPATH) $(INSTALL_DIR)
+
+
+.PHONY: uninstall
+uninstall:
+	$(RM) $(INSTALL_DIR)/$(OBJ)
+
+
+.PHONY: clean
+clean:
+	$(RM) lib/*.so
+
+
+.PHONY: memcheck
+memcheck:
+	valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \
+	--log-fd=9 --suppressions=valgrind-lua-noreadline.supp \
+	lua -e "dofile 'scratch.lua'" 9>|$(VALGRIND_LOG); \
+	echo "Log file in $(VALGRIND_LOG)"

+ 48 - 0
lua/lsup-scm-1.rockspec

@@ -0,0 +1,48 @@
+package = "lsup"
+version = "scm-1"
+source = {
+   --url = "./"
+   url = "git://git.knowledgetx.com/scossu/lsup_rdf.git",
+   branch = "master",
+   tag = "HEAD",
+   --tag = "scm",
+}
+description = {
+   summary = "Compact, minimalistic RDF library and persistent store.",
+   detailed = [[
+      `lsup_rdf` is an embedded library to manipulate and permanently store
+      Linked Data. It handles terms, triples, graphs, and has an in-memory and
+      a filesystem-based storage back ends.
+   ]],
+   homepage = "http://git.knowledgetx.com/scossu/lsup_rdf",
+   license = "https://git.knowledgetx.com/scossu/lsup_rdf/src/master/LICENSE"
+}
+dependencies = {
+   "lua >= 5.4, < 6",
+}
+build = {
+   type = "make",
+   --build_target = "lua",
+   --install_target = "lua_install",
+   --[=[
+   modules = {
+      lsup = {
+         sources = {
+             "src/lua_*.c",
+             --[[
+             "src/lua_namespace.c",
+             "src/lua_term.c",
+             "src/lua_triple.c",
+             "src/lua_graph.c",
+             "src/lua_lsup.c",
+             --]]
+         },
+         --defines = {},
+         --libraries = {"lsuprdf_dbg"},
+         --incdirs = {"./include", "./ext/log/src", "./ext/hashmap"},
+         --libdirs = {}
+      }
+   },
+   --copy_directories = {}
+   --]=]
+}

+ 60 - 0
lua/parse_valgrind_supp.sh

@@ -0,0 +1,60 @@
+#! /usr/bin/awk -f
+# Copyright: 2022 David Hart
+# Licence:   wxWindows licence
+#
+# A script to extract the actual suppression info from the output of (for example) valgrind --leak-check=full --show-reachable=yes --error-limit=no --gen-suppressions=all ./minimal
+# The desired bits are between ^{ and ^} (including the braces themselves).
+# The combined output should either be appended to /usr/lib/valgrind/default.supp, or placed in a .supp of its own
+# If the latter, either tell valgrind about it each time with --suppressions=<filename>, or add that line to ~/.valgrindrc
+
+# NB This script uses the |& operator, which I believe is gawk-specific. In case of failure, check that you're using gawk rather than some other awk
+
+# The script looks for suppressions. When it finds one it stores it temporarily in an array,
+# and also feeds it line by line to the external app 'md5sum' which generates a unique checksum for it.
+# The checksum is used as an index in a different array. If an item with that index already exists the suppression must be a duplicate and is discarded.
+
+BEGIN { suppression=0; md5sum = "md5sum" }
+  # If the line begins with '{', it's the start of a supression; so set the var and initialise things
+  /^{/  {
+           suppression=1;  i=0; next 
+        }
+  # If the line begins with '}' its the end of a suppression
+  /^}/  {
+          if (suppression)
+           { suppression=0;
+             close(md5sum, "to")  # We've finished sending data to md5sum, so close that part of the pipe
+             ProcessInput()       # Do the slightly-complicated stuff in functions
+             delete supparray     # We don't want subsequent suppressions to append to it!
+           }
+     }
+  # Otherwise, it's a normal line. If we're inside a supression, store it, and pipe it to md5sum. Otherwise it's cruft, so ignore it
+     { if (suppression)
+         { 
+            supparray[++i] = $0
+            print |& md5sum
+         }
+     }
+
+
+ function ProcessInput()
+ {
+    # Pipe the result from md5sum, then close it     
+    md5sum |& getline result
+    close(md5sum)
+    # gawk can't cope with enormous ints like $result would be, so stringify it first by prefixing a definite string
+    resultstring = "prefix"result
+
+    if (! (resultstring in chksum_array) )
+      { chksum_array[resultstring] = 0;  # This checksum hasn't been seen before, so add it to the array
+        OutputSuppression()              # and output the contents of the suppression
+      }
+ }
+
+ function OutputSuppression()
+ {
+  # A suppression is surrounded by '{' and '}'. Its data was stored line by line in the array  
+  print "{"  
+  for (n=1; n <= i; ++n)
+    { print supparray[n] }
+  print "}" 
+ }

+ 49 - 0
lua/scratch.lua

@@ -0,0 +1,49 @@
+term = require "lsup.term"
+triple = require "lsup.triple"
+graph = require "lsup.graph"
+
+---[[
+t1 = term.new_bnode()
+t2 = term.new_iriref("urn:p:11")
+t3 = term.new_lit("123", "xsd:int")
+t4 = term.new_lit("Hola", nil, "es_ES")
+t5 = term.new_lit("مرحبا", nil, "ar_AR")
+
+trp1 = triple.new (
+    term.new_iriref("urn:s:1"),
+    term.new_iriref("urn:p:1"),
+    term.new_lit("hello", nil, "en_US"))
+
+triples = {
+    trp1,
+    triple.new (t1, t2, t3),
+    triple.new (t1, t2, t4),
+    triple.new (t1, t2, t5),
+}
+
+gr1 = graph.new()
+ct = gr1:add(triples)
+print("Triples added: " .. ct)
+gr2 = graph.new()
+for i in gr1:lookup() do print(i) end
+
+lm = gr1:connections(t1, term.LINK_OUTBOUND)
+print("Connections")
+for t1, ts in pairs(lm) do
+    for t2 in pairs(ts) do print(t1, t2) end
+end
+
+unique_t = gr1:unique_terms(triple.POS_S)
+print("Unique subjects")
+for t in pairs(unique_t) do print(t) end
+print("Unique predicates")
+unique_t = gr1:unique_terms(triple.POS_P)
+for t in pairs(unique_t) do print(t) end
+print("Unique objects")
+unique_t = gr1:unique_terms(triple.POS_O)
+for t in pairs(unique_t) do print(t) end
+
+print("Terms connected to t1 t2:")
+tset = gr1:term_set(t2, triple.POS_P, t1, triple.POS_S)
+for t in pairs(tset) do print(t) end
+--]]

+ 371 - 0
lua/src/lua_graph.c

@@ -0,0 +1,371 @@
+#include "lua_lsup.h"
+
+#define check_graph(L) \
+    *(LSUP_Graph **)luaL_checkudata(L, 1, "LSUP.Graph")
+
+
+static LSUP_Graph **allocate_graph (lua_State *L)
+{
+    LSUP_Graph **gp = lua_newuserdatauv (L, sizeof (*gp), 1);
+    luaL_getmetatable (L, "LSUP.Graph");
+    lua_setmetatable (L, -2);
+
+    return gp;
+}
+
+
+static int l_graph_new (lua_State *L)
+{
+    const LSUP_StoreType store_type = lua_tointeger (L, 1);
+    const char *uri_str = lua_tostring (L, 2);
+
+    LSUP_Graph **gp = allocate_graph (L);
+
+    LSUP_Store *store = NULL;
+    if (store_type) {
+        const LSUP_StoreInt *sif = LSUP_store_int (store_type);
+        if (UNLIKELY (!sif)) return luaL_error (
+                L, "No interface defined for store type: %d.", store_type);
+
+        // TODO Move store creation fn and handle into a separate module.
+        store = LSUP_store_new (store_type, NULL, 0);
+        // Set up the store if a function for that is defined.
+        if (sif->setup_fn) {
+            if (sif->setup_fn(NULL, false) < LSUP_OK)
+                return luaL_error (L, "Error initializing back end store.");
+        }
+    }
+
+    // TODO Make store ID, nsm and initial size accessible.
+    *gp = LSUP_graph_new (store, uri_str, NULL);
+    LUA_NLCHECK (*gp, "Error creating graph.");
+
+    return 1;
+}
+
+
+/*
+ * Class methods.
+ */
+
+static int l_graph_gc (lua_State *L)
+{
+    LSUP_Graph *gr = check_graph (L);
+    if (gr) LSUP_graph_free (gr);
+
+    return 0;
+}
+
+
+static int l_graph_to_string (lua_State *L)
+{
+    const LSUP_Graph *gr = check_graph (L);
+    lua_pushfstring (
+            L, "LSUP.Graph @%p <%s>: %d triples",
+            gr, LSUP_graph_uri (gr)->data, LSUP_graph_size (gr));
+
+    return 1;
+}
+
+
+static int l_graph_len (lua_State *L)
+{
+    const LSUP_Graph *gr = check_graph (L);
+    lua_pushinteger (L, LSUP_graph_size (gr));
+
+    return 1;
+}
+
+
+static int l_graph_copy (lua_State *L)
+{
+    const LSUP_Graph *src = check_graph (L);
+    LSUP_Graph *dest = *(LSUP_Graph **)luaL_checkudata(L, 2, "LSUP.Graph");
+
+    LUA_PCHECK (LSUP_graph_copy (src, dest), "Error copying graph");
+
+    return 1;
+}
+
+
+static int l_graph_copy_contents (lua_State *L)
+{
+    const LSUP_Graph *src = check_graph (L);
+    LSUP_Graph *dest = *(LSUP_Graph **)luaL_checkudata (L, 2, "LSUP.Graph");
+    const LSUP_Term *s, *p, *o;
+
+    if lua_isnoneornil (L, 3) s = NULL;
+    else s = *(LSUP_Term **)luaL_checkudata (L, 3, "LSUP.Term");
+    if lua_isnoneornil (L, 4) p = NULL;
+    else p = *(LSUP_Term **)luaL_checkudata (L, 4, "LSUP.Term");
+    if lua_isnoneornil (L, 5) o = NULL;
+    else o = *(LSUP_Term **)luaL_checkudata (L, 5, "LSUP.Term");
+
+    LUA_PCHECK (LSUP_graph_copy_contents (
+                src, dest, s, p, o), "Error copying graph.");
+
+    return 1;
+}
+
+
+static int l_graph_equals (lua_State *L)
+{
+    const LSUP_Graph *gr1 = check_graph (L);
+    const LSUP_Graph *gr2 =
+        *(LSUP_Graph **)luaL_checkudata (L, 2, "LSUP.Graph");
+
+    LOG_DEBUG ("Comparing graphs %p %p", gr1, gr2);
+    int eq_rc = LSUP_graph_equals (gr1, gr2);
+    lua_pushboolean (L, eq_rc);
+
+    return 1;
+}
+
+
+static int l_graph_contains (lua_State *L)
+{
+    const LSUP_Graph *gr = check_graph (L);
+    const LSUP_Triple *spo =
+        *(LSUP_Triple **)luaL_checkudata (L, 2, "LSUP.Triple");
+
+    lua_pushboolean (L, LSUP_graph_contains (gr, spo));
+
+    return 1;
+}
+
+
+static int l_graph_add (lua_State *L)
+{
+    LSUP_Graph *gr = check_graph (L);
+    LOG_DEBUG ("Triples type: %s", lua_typename (L, lua_type (L, 2)));
+    int rc;
+    LSUP_rc lsup_rc= LSUP_NOACTION;
+    size_t i = 0, ct = 0;
+    LSUP_GraphIterator *it = LSUP_graph_add_init (gr);
+
+    while ((rc = lua_rawgeti (L, 2, ++i)) != LUA_TNIL) {
+        //LOG_DEBUG ("Triple type: %s", lua_typename (L, rc));
+        const LSUP_Triple *spo =
+            *(LSUP_Triple **)luaL_checkudata (L, -1, "LSUP.Triple");
+        LOG_DEBUG (
+                "Got triple %d: {%s %s %s}\n",
+                i, spo->s->data, spo->p->data, spo->o->data);
+        lsup_rc = LSUP_graph_add_iter (it, spo);
+
+        if (lsup_rc < LSUP_OK) break;
+        if (lsup_rc == LSUP_OK) ct++;
+    };
+    LSUP_graph_add_done (it);
+    lua_pushinteger (L, ct);
+
+    if (UNLIKELY (lsup_rc < LSUP_OK)) return luaL_error (
+            L, "Error adding triple at position %d: %s",
+            i, LSUP_strerror (lsup_rc));
+    else return 1;
+}
+
+
+static int graph_iter_next (lua_State *L)
+{
+    LSUP_GraphIterator *it =
+        *(LSUP_GraphIterator **)lua_touserdata (L, lua_upvalueindex (1));
+
+    LSUP_Triple **spo_p = lua_newuserdatauv (L, sizeof (*spo_p), 1);
+    luaL_getmetatable (L, "LSUP.Triple");
+    lua_setmetatable (L, -2);
+    *spo_p = NULL;
+
+    LSUP_rc rc = LSUP_graph_iter_next (it, spo_p);
+
+    if (rc == LSUP_END) {
+        lua_pushnil (L);
+        lua_pushstring (L, "End of lookup results.");
+        return 2;
+    }
+    LUA_PCHECK (rc, "Error retrieving a lookup result.");
+
+    return 1;
+}
+
+
+static int l_graph_lookup (lua_State *L)
+{
+    const LSUP_Graph *gr = check_graph (L);
+    const LSUP_Term *s, *p, *o;
+    if lua_isnoneornil (L, 2) s = NULL;
+    else s = *(LSUP_Term **)luaL_checkudata (L, 2, "LSUP.Term");
+    if lua_isnoneornil (L, 3) p = NULL;
+    else p = *(LSUP_Term **)luaL_checkudata (L, 3, "LSUP.Term");
+    if lua_isnoneornil (L, 4) o = NULL;
+    else o = *(LSUP_Term **)luaL_checkudata (L, 4, "LSUP.Term");
+
+    LSUP_GraphIterator **it_p =
+        (LSUP_GraphIterator **)lua_newuserdata (L, sizeof *it_p);
+    *it_p = NULL;
+    luaL_getmetatable (L, "LSUP.GraphIterator");
+    lua_setmetatable (L, -2);
+
+    size_t ct;
+    *it_p = LSUP_graph_lookup (gr, s, p, o, &ct);
+    LUA_NLCHECK (*it_p, "Error creating graph iterator.");
+    LOG_DEBUG ("Found triples: %d", ct);
+
+    lua_pushcclosure (L, graph_iter_next, 1);
+
+    return 1;
+}
+
+
+static int graph_iter_gc (lua_State *L)
+{
+    LSUP_GraphIterator **it_p = lua_touserdata (L, 1);
+
+    if (UNLIKELY (!it_p || *it_p)) return 0;
+
+    LSUP_graph_iter_free (*it_p);
+    *it_p = NULL;
+
+    return 0;
+}
+
+
+/** Returns a LinkMap that can be iterated over with iter().
+ */
+static int l_graph_connections (lua_State *L)
+{
+    const LSUP_Graph *gr = check_graph (L);
+    LSUP_Term *t = *(LSUP_Term **)luaL_checkudata (L, 2, "LSUP.Term");
+    const LSUP_LinkType type = luaL_checkinteger (L, 3);
+    LOG_DEBUG ("Adding term for connections: @%p", *t);
+
+    LSUP_LinkMap **lm_p = lua_newuserdata (L, sizeof *lm_p);
+    *lm_p = LSUP_graph_connections (gr, t, type);
+    luaL_getmetatable (L, "LSUP.LinkMap");
+    lua_setmetatable (L, -2);
+
+    return 1;
+}
+
+
+static int l_graph_term_set (lua_State *L)
+{
+    const LSUP_Graph *gr = check_graph (L);
+    const LSUP_Term *t1 = *(LSUP_Term **)luaL_checkudata (L, 2, "LSUP.Term");
+    const LSUP_TriplePos t1_pos = luaL_checkinteger (L, 3);
+    const LSUP_Term *t2 = *(LSUP_Term **)luaL_checkudata (L, 4, "LSUP.Term");
+    const LSUP_TriplePos t2_pos = luaL_checkinteger (L, 5);
+
+    LSUP_TermSet **ts_p = lua_newuserdata (L, sizeof *ts_p);
+    luaL_getmetatable (L, "LSUP.TermSet");
+    lua_setmetatable (L, -2);
+
+    *ts_p = LSUP_graph_term_set (gr, t1, t1_pos, t2, t2_pos);
+    LUA_NLCHECK (*ts_p, "Error creating term set.");
+
+    return 1;
+}
+
+
+static int l_graph_unique_terms (lua_State *L)
+{
+    const LSUP_Graph *gr = check_graph (L);
+    const LSUP_TriplePos pos = luaL_checkinteger (L, 2);
+
+    LSUP_TermSet **ts_p = lua_newuserdata (L, sizeof *ts_p);
+    luaL_getmetatable (L, "LSUP.TermSet");
+    lua_setmetatable (L, -2);
+
+    *ts_p = LSUP_graph_unique_terms (gr, pos);
+    LUA_NLCHECK (*ts_p, "Error creating term set.");
+
+    return 1;
+}
+
+
+/*
+ * Library setup.
+ */
+
+static const luaL_Reg graph_lib_fn [] = {
+    {"new", l_graph_new},
+
+    {NULL}
+};
+
+
+/*
+static const luaL_Reg graph_getters [] = {
+    {"uri", l_graph_get_uri},
+    {"namespace", l_graph_get_nsm},
+
+    {NULL}
+};
+*/
+
+
+/*
+static const luaL_Reg graph_setters [] = {
+    {"uri", l_graph_set_uri},
+
+    {NULL}
+};
+*/
+
+static const luaL_Reg graph_lib_meth [] = {
+    {"__eq", l_graph_equals},
+    {"__gc", l_graph_gc},
+    //{"__index", get_attr},
+    //{"__newindex", set_attr},
+    {"__tostring", l_graph_to_string},
+    {"__len", l_graph_len},
+
+    {"copy", l_graph_copy},
+    {"copy_contents", l_graph_copy_contents},
+    {"contains", l_graph_contains},
+
+    {"add", l_graph_add},
+    {"lookup", l_graph_lookup},
+    {"connections", l_graph_connections},
+    {"term_set", l_graph_term_set},
+    {"unique_terms", l_graph_unique_terms},
+
+    //{"to_n3", l_graph_to_n3},
+    {NULL}
+};
+
+
+static const LEnumConst graph_enums[] = {
+    {NULL, 0}
+};
+
+
+int luaopen_lsup_graph (lua_State *L)
+{
+    LSUP_init();  // This is idempotent: no problem calling it multiple times.
+    luaL_newmetatable (L, "LSUP.Graph");
+    lua_pushvalue (L, -1);
+    lua_setfield (L, -2, "__index");
+    luaL_setfuncs (L, graph_lib_meth, 0);
+
+    // Metatables for ancillary types.
+    luaL_newmetatable (L, "LSUP.GraphIterator");
+    lua_pushcfunction (L, graph_iter_gc);
+    lua_setfield (L, -2, "__gc");
+
+    /*
+    // Getters table.
+    lua_newtable (L);
+    for (int i = 0; graph_getters[i].name != NULL; i++) {
+        lua_pushcfunction (L, graph_getters[i].func);
+        lua_setfield (L, -2, graph_getters[i].name);
+    }
+    // Set getters table as a value for the Graph metatable.
+    lua_setfield (L, -2, "getters");
+    */
+    luaL_newlib (L, graph_lib_fn);
+
+    // Module-level constants.
+    push_int_const (L, graph_enums);
+
+    return 1;
+}

+ 19 - 0
lua/src/lua_lsup.c

@@ -0,0 +1,19 @@
+#include "lua_lsup.h"
+
+void push_int_const (lua_State *L, const LEnumConst *list) {
+    for (int i = 0; list[i].k; i++) {
+        lua_pushinteger (L, list[i].v);
+        lua_setfield (L, -2, list[i].k);
+    };
+}
+
+
+void push_string_const (lua_State *L, const LStringConst *list) {
+    for (int i = 0; list[i].k; i++) {
+        lua_pushstring (L, list[i].v);
+        lua_setfield (L, -2, list[i].k);
+    };
+}
+
+
+

+ 43 - 0
lua/src/lua_lsup.h

@@ -0,0 +1,43 @@
+#include <lua.h>
+#include <lauxlib.h>
+
+#include "lsup_rdf.h"
+
+#ifndef _LUA_LSUP_H
+#define _LUA_LSUP_H
+
+/// Raise Lua error including LSUP error message on negative rc.
+#define LUA_PCHECK(exp, message) do {\
+    if (UNLIKELY ((exp) < LSUP_OK)) \
+        return luaL_error (L, "%s: %s", message, LSUP_strerror (exp)); \
+} while (0)
+
+#define LUA_NLCHECK(exp, ...) do {\
+    if (UNLIKELY ((exp) == NULL)) return luaL_error (L, __VA_ARGS__); \
+} while (0)
+
+
+/// Enum (int) constants to be passed to the module.
+typedef struct l_enum_const {
+    const char  *k;
+    const int   v;
+} LEnumConst;
+
+
+/// String constants to be passed to the module.
+typedef struct l_string_const {
+    const char  *k;
+    const char  *v;
+} LStringConst;
+
+
+void push_int_const (lua_State *L, const LEnumConst *list);
+void push_string_const (lua_State *L, const LStringConst *list);
+
+/// Create new namespace map.
+int l_nsmap_new (lua_State *L);
+
+/// Allocate space for a term object.
+LSUP_Term **allocate_term (lua_State *L);
+
+#endif  // _LUA_LSUP_H

+ 173 - 0
lua/src/lua_namespace.c

@@ -0,0 +1,173 @@
+#include "lua_lsup.h"
+
+#define check_nsm(L) \
+    *(LSUP_NSMap **)luaL_checkudata(L, 1, "LSUP.NSMap")
+
+
+/*
+ * Factory methods.
+ */
+
+int l_nsmap_new (lua_State *L)
+{
+    LSUP_NSMap **nsm_p = lua_newuserdatauv (L, sizeof (*nsm_p), 1);
+    luaL_getmetatable (L, "LSUP.NSMap");
+    lua_setmetatable (L, -2);
+
+    *nsm_p = LSUP_nsmap_new ();
+    if (!*nsm_p) return luaL_error (L, "Error while creating a term!");
+
+    return 1;
+}
+
+
+/*
+ * Class methods.
+ */
+
+static int l_nsmap_gc (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+    if (nsm) LSUP_nsmap_free (nsm);
+
+    return 0;
+}
+
+
+static int l_nsmap_add (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+    const char
+        *pfx = luaL_checkstring (L, 2),
+        *nsstr = luaL_checkstring (L, 3);
+
+    LUA_PCHECK (
+            LSUP_nsmap_add (nsm, pfx, nsstr),
+            "Error adding member to NS map");
+
+    return 0;
+}
+
+
+static int l_nsmap_remove (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+    const char *pfx = luaL_checkstring (L, 2);
+
+    LUA_PCHECK (
+            LSUP_nsmap_remove (nsm, pfx),
+            "Error removing member to NS map");
+
+    return 0;
+}
+
+
+static int l_nsmap_get_ns (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+    const char *pfx = luaL_checkstring (L, 2);
+
+    const char *ns = LSUP_nsmap_get_ns (nsm, pfx);
+
+    if (ns) lua_pushstring (L, ns);
+    else return 0;
+
+    return 1;
+}
+
+
+static int l_nsmap_get_pfx (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+    const char *ns = luaL_checkstring (L, 2);
+
+    const char *pfx = LSUP_nsmap_get_pfx (nsm, ns);
+
+    if (pfx) lua_pushstring (L, pfx);
+    else lua_pushnil (L);
+
+    return 1;
+}
+
+
+static int l_nsmap_normalize_uri (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+    const char *pfx_uri = luaL_checkstring (L, 2);
+
+    char *fq_uri;
+    LUA_PCHECK (
+            LSUP_nsmap_normalize_uri (nsm, pfx_uri, &fq_uri),
+            "Error normalizing URI");
+
+    if (fq_uri) lua_pushstring (L, fq_uri);
+    else return 0;
+
+    free (fq_uri);
+    return 1;
+}
+
+
+static int l_nsmap_denormalize_uri (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+    const char *fq_uri = luaL_checkstring (L, 2);
+
+    char *pfx_uri;
+    LUA_PCHECK (
+            LSUP_nsmap_denormalize_uri (nsm, fq_uri, &pfx_uri),
+            "Error denormalizing URI");
+
+    if (pfx_uri) lua_pushstring (L, pfx_uri);
+    else return 0;
+
+    free (pfx_uri);
+    return 1;
+}
+
+
+static int l_nsmap_iter (lua_State *L)
+{
+    LSUP_NSMap *nsm = check_nsm (L);
+
+    // TODO
+
+    return 0;
+}
+
+
+/*
+ * Library setup.
+ */
+
+static const luaL_Reg nsmap_lib_fn [] = {
+    {"new", l_nsmap_new},
+    {NULL}
+};
+
+
+static const luaL_Reg nsmap_lib_meth [] = {
+    {"__gc", l_nsmap_gc},
+
+    {"add", l_nsmap_add},
+    {"remove", l_nsmap_remove},
+    {"get_ns", l_nsmap_get_ns},
+    {"get_pfx", l_nsmap_get_pfx},
+    {"normalize_uri", l_nsmap_normalize_uri},
+    {"denormalize_uri", l_nsmap_denormalize_uri},
+    //{"iter", l_nsmap_iter},
+
+    {NULL}
+};
+
+
+int luaopen_lsup_namespace (lua_State *L)
+{
+    luaL_newmetatable (L, "LSUP.NSMap");
+    lua_pushvalue (L, -1);
+    lua_setfield (L, -2, "__index");
+    luaL_setfuncs (L, nsmap_lib_meth, 0);
+    luaL_newlib (L, nsmap_lib_fn);
+
+    return 1;
+}

+ 595 - 0
lua/src/lua_term.c

@@ -0,0 +1,595 @@
+#include "lua_lsup.h"
+#include "stackdump.h"
+
+#define check_term(L) \
+    *(LSUP_Term **)luaL_checkudata(L, 1, "LSUP.Term")
+
+
+LSUP_Term **allocate_term (lua_State *L)
+{
+    LSUP_Term **tp = lua_newuserdatauv (L, sizeof (*tp), 1);
+    luaL_getmetatable (L, "LSUP.Term");
+    lua_setmetatable (L, -2);
+
+    return tp;
+}
+
+
+LSUP_TermSet **allocate_tset (lua_State *L)
+{
+    LSUP_TermSet **ts_p = lua_newuserdatauv (L, sizeof (*ts_p), 1);
+    luaL_getmetatable (L, "LSUP.TermSet");
+    lua_setmetatable (L, -2);
+
+    return ts_p;
+}
+
+
+/*
+ * Factory methods.
+ */
+
+static int l_new_iriref (lua_State *L)
+{
+    const char *data = luaL_checkstring (L, 1);
+    // TODO handle nsm.
+    LSUP_NSMap *nsm = lua_touserdata (L, 2);
+
+    LSUP_Term **tp = allocate_term (L);
+
+    *tp = LSUP_iriref_new (data, nsm);
+    LUA_NLCHECK (*tp, "Error creating term.");
+
+    return 1;
+}
+
+
+static int l_new_iriref_abs (lua_State *L)
+{
+    LSUP_Term
+        *root = check_term (L),
+        *iri = *(LSUP_Term **)luaL_checkudata (L, 2, "LSUP.Term");
+
+    LSUP_Term **tp = allocate_term (L);
+
+    *tp = LSUP_iriref_absolute (root, iri);
+    LUA_NLCHECK (*tp, "Error creating term.");
+
+    return 1;
+}
+
+
+static int l_new_iriref_rel (lua_State *L)
+{
+    LSUP_Term
+        *root = check_term (L),
+        *iri = *(LSUP_Term **)luaL_checkudata (L, 2, "LSUP.Term");
+
+    LSUP_Term **tp = allocate_term (L);
+
+    *tp = LSUP_iriref_relative (root, iri);
+    LUA_NLCHECK (*tp, "Error creating term.");
+
+    return 1;
+}
+
+
+/** @brief create a new literal.
+ *
+ * Argument 2 (data type) and 3 (lang) are exclusive. If both are specified,
+ * datatype has precedence. If both are nil, datatype is set to the default
+ * (xsd:string).
+ */
+static int l_new_lit (lua_State *L)
+{
+    const char
+        *data = luaL_checkstring (L, 1),
+        *dtype_str = lua_tostring (L, 2);
+    const char *lang = lua_tostring (L, 3);
+
+    LSUP_Term **tp = allocate_term (L);
+    LSUP_Term *dtype;
+
+    if (dtype_str) {
+        // TODO check memory leak.
+        dtype = LSUP_iriref_new (dtype_str, LSUP_default_nsm);
+        *tp = LSUP_literal_new (data, dtype);
+    }
+    else if (lang) *tp = LSUP_lt_literal_new (data, (char *)lang);
+    else *tp = LSUP_literal_new (data, NULL);
+
+    LUA_NLCHECK (*tp, "Error creating term.");
+
+    return 1;
+}
+
+
+static int l_new_bnode (lua_State *L)
+{
+    const char *data = lua_tostring (L, 1);
+
+    LSUP_Term **tp = allocate_term (L);
+
+    *tp = LSUP_bnode_new (data);
+    LUA_NLCHECK (*tp, "Error creating term.");
+
+    return 1;
+}
+
+
+static int l_new_copy (lua_State *L)
+{
+    LSUP_Term *src = check_term (L);
+
+    LSUP_Term **tp = allocate_term (L);
+
+    *tp = LSUP_term_copy (src);
+    LUA_NLCHECK (*tp, "Error creating term.");
+
+    return 1;
+}
+
+
+/*
+ * Class methods.
+ */
+static int l_term_equals (lua_State *L)
+{
+    LSUP_Term
+        *t1 = check_term (L),
+        *t2 = *(LSUP_Term **)luaL_checkudata (L, 2, "LSUP.Term");
+
+    lua_pushboolean (L, LSUP_term_equals (t1, t2));
+
+    return 1;
+}
+
+
+static int l_term_gc (lua_State *L)
+{
+    LSUP_Term **tp = luaL_checkudata(L, 1, "LSUP.Term");
+    LOG_TRACE ("Garbage collecting term @%p", *tp);
+
+    LSUP_term_free (*tp);
+    *tp = NULL;
+
+    return 0;
+}
+
+
+/*
+static int l_term_to_string (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    char *nt_term = NULL;
+
+    CHECK (nt_codec.encode_term (t, NULL, &nt_term), fail);
+    lua_pushfstring (L, "LSUP.Term @%p %s", t, nt_term);
+
+    return 1;
+
+fail:
+    if (nt_term) free (nt_term);
+    return luaL_error (L, "Error encoding term for display!");
+}
+*/
+
+
+static int l_term_to_n3 (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    char *nt_term = NULL;
+
+    CHECK (nt_codec.encode_term (t, NULL, &nt_term), fail);
+    lua_pushstring (L, nt_term);
+    free (nt_term);
+
+    return 1;
+
+fail:
+    if (nt_term) free (nt_term);
+    return luaL_error (L, "Error encoding term for display.");
+}
+
+
+/*
+ * Getters.
+ */
+
+// Forward declaration.
+static const luaL_Reg term_getters [];
+
+
+static int l_term_get_attr (lua_State *L)
+{
+    const char *attr = luaL_checkstring (L, 2);
+    //printf ("Got attr: %s\n", attr);
+
+    if (luaL_getmetafield (L, 1, attr) != LUA_TNIL)
+        return 1;  // Return metamethod to be called as a function.
+
+    luaL_getmetafield (L, 1, "getters");
+    if (lua_getfield (L, -1, attr) != LUA_TNIL)
+        return lua_tocfunction (L, -1)(L);
+
+    return 0;
+}
+
+
+static int get_data (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    lua_pushstring (L, t->data);
+
+    return 1;
+}
+
+
+static int get_type (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    lua_pushinteger (L, t->type);
+
+    return 1;
+}
+
+
+static int get_iriref_nsm (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    // TODO
+    lua_pushlightuserdata (L, LSUP_iriref_nsm (t));
+
+    return 1;
+}
+
+
+static int get_iriref_prefix (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    lua_pushstring (L, LSUP_iriref_prefix (t));
+
+    return 1;
+}
+
+
+static int get_iriref_path (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    lua_pushstring (L, LSUP_iriref_path (t));
+
+    return 1;
+}
+
+
+static int get_iriref_frag (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    lua_pushstring (L, LSUP_iriref_frag (t));
+
+    return 1;
+}
+
+
+static int get_lit_datatype (lua_State *L)
+{
+    printf ("Getting datatype.\n");
+    LSUP_Term *t = check_term (L);
+    if (!LSUP_IS_LITERAL (t))
+        return luaL_error (L, "Term is not a literal.", 2);
+
+    lua_pushstring (L, t->datatype->data);
+
+    return 1;
+}
+
+
+static int get_lit_lang (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+    if (!LSUP_IS_LITERAL (t))
+        return luaL_error (L, "Term is not a literal.", 2);
+
+    lua_pushstring (L, t->lang);
+
+    return 1;
+}
+
+
+static int get_hash (lua_State *L)
+{
+    LSUP_Term *t = check_term (L);
+
+    lua_pushinteger (L, LSUP_term_hash (t));
+
+    return 1;
+}
+
+
+/*
+ * Setters.
+ */
+
+// Very simple for now.
+//static const luaL_Reg term_setters [];
+static int l_term_set_attr (lua_State *L)
+{ return luaL_error (L, "Direct setting is not allowed for this type.", 2); }
+
+
+/*
+ * Ancillary types
+ */
+
+/*
+ * Term set.
+ */
+
+static int term_set_iter_next (lua_State *L)
+{
+    LSUP_TermSet *ts =
+        *(LSUP_TermSet **)lua_touserdata (L, lua_upvalueindex (1));
+    size_t *ip = lua_touserdata (L, lua_upvalueindex (2));
+
+    LSUP_Term *tmp;
+    LSUP_rc rc = LSUP_term_set_next (ts, ip, &tmp);
+
+    LUA_PCHECK (rc, "Error iterating over term set");
+
+    if (rc == LSUP_END) {
+        free (ip);
+        return 0;
+    }
+
+    LSUP_Term **tp = allocate_term (L);
+    *tp = LSUP_term_copy (tmp);
+    if (!*tp) {
+        free (ip);
+        luaL_error (L, "Error allocating term.");
+    }
+
+    return 1;
+}
+
+
+static int l_term_set_iter_init (lua_State *L)
+{
+    size_t *ip = malloc (sizeof (*ip));
+    LUA_NLCHECK (ip, "Error allocating tset iterator.");
+    *ip = 0;
+
+    lua_pushlightuserdata (L, ip);
+    STACK_DUMP(L, "Before pushing tset next closure");
+    lua_pushcclosure (L, term_set_iter_next, 2);
+
+    return 1;
+}
+
+
+static int term_set_gc (lua_State *L)
+{
+    LSUP_TermSet **ts_p = lua_touserdata (L, 1);
+    LSUP_term_set_free (*ts_p);
+    *ts_p = NULL;
+
+    return 0;
+}
+
+
+/*
+ * Link map.
+ */
+
+static int lmap_iter_next (lua_State *L)
+{
+    LSUP_LinkMapIterator *it =
+        *(LSUP_LinkMapIterator **)lua_touserdata (L, lua_upvalueindex (1));
+
+    LSUP_Term **link_p = (LSUP_Term **)lua_newuserdata (L, sizeof *link_p);
+    *link_p = NULL;
+    luaL_getmetatable (L, "LSUP.Term");
+    lua_setmetatable (L, -2);
+
+    LSUP_TermSet *ts = NULL;
+    LSUP_Term *tmp = NULL;
+    LSUP_rc rc = LSUP_link_map_next (it, &tmp, &ts);
+
+    if (rc != LSUP_OK) {
+        if (rc == LSUP_END) return 0;
+        else LUA_PCHECK (rc, "Error iterating over link map");
+    }
+    *link_p = LSUP_term_copy (tmp);
+    LUA_NLCHECK (*link_p, "Error allocating term.");
+
+    size_t i = 0;
+    tmp = NULL;
+
+    lua_newtable (L);
+    while ((rc = LSUP_term_set_next (ts, &i, &tmp)) == LSUP_OK) {
+        LSUP_Term **t2_p = (LSUP_Term **)lua_newuserdata (L, sizeof *t2_p);
+        luaL_getmetatable (L, "LSUP.Term");
+        lua_setmetatable (L, -2);
+        *t2_p = LSUP_term_copy (tmp);
+
+        lua_pushboolean (L, true);
+        lua_rawset (L, -3);
+    }
+    LUA_PCHECK (rc, "Error iterating over term set");
+
+    // linked term + term set.
+    return 2;
+}
+
+
+/**
+ * Internally this function creates a LMapIterator, which is used as the
+ * upvalue for lmap_iter_next(). The iterator is garbage collected at the end
+ * of the iteration loop, the link map can be reused.
+ */
+static int l_lmap_iter_init (lua_State *L)
+{
+    STACK_DUMP (L, "beginning of LMap iter init fn");
+    LSUP_LinkMap *lm = *(LSUP_LinkMap **)luaL_checkudata(L, 1, "LSUP.LinkMap");
+
+    LSUP_LinkMapIterator **lmit_p =
+        (LSUP_LinkMapIterator  **)lua_newuserdata (L, sizeof *lmit_p);
+    *lmit_p = LSUP_link_map_iter_new (lm);
+    luaL_getmetatable (L, "LSUP.LMapIterator");
+    lua_setmetatable (L, -2);
+
+    lua_pushcclosure (L, lmap_iter_next, 1);
+    STACK_DUMP (L, "After pushing iter closure");
+
+    return 1;
+}
+
+
+static int link_map_gc (lua_State *L)
+{
+    LSUP_LinkMap **lm_p = lua_touserdata (L, 1);
+    LOG_DEBUG ("Garbage collecting link map @%p", *lm_p);
+    // FIXME This is to prevent a double-free on shutdown.
+    // Must find the culprit instead.
+    if (UNLIKELY (!lm_p || !*lm_p)) return 0;
+
+    LSUP_link_map_free (*lm_p);
+    *lm_p = NULL;
+
+    return 0;
+}
+
+
+static int lmap_iter_gc (lua_State *L)
+{
+    LSUP_LinkMapIterator *it = *(LSUP_LinkMapIterator **)lua_touserdata (L, 1);
+    LOG_DEBUG ("Garbage collecting link map iterator @%p", it);
+    LSUP_link_map_iter_free (it);
+
+    return 0;
+}
+
+
+/*
+ * Library setup.
+ */
+
+static const luaL_Reg term_lib_fn [] = {
+    {"new_iriref", l_new_iriref},
+    {"new_iriref_abs", l_new_iriref_abs},
+    {"new_iriref_rel", l_new_iriref_rel},
+    {"new_lit", l_new_lit},
+    {"new_bnode", l_new_bnode},
+    {"new_copy", l_new_copy},
+
+    {NULL}
+};
+
+
+static const luaL_Reg term_getters [] = {
+    // General getters.
+    {"data", get_data},
+    {"type", get_type},
+
+    // IRIRef getters.
+    {"nsm", get_iriref_nsm},
+    {"prefix", get_iriref_prefix},
+    {"path", get_iriref_path},
+    {"frag", get_iriref_frag},
+
+    // Literal getters.
+    {"datatype", get_lit_datatype},
+    {"lang", get_lit_lang},
+
+    // Generic getters.
+    {"hash", get_hash},
+
+    {NULL}
+};
+
+
+/*
+static const luaL_Reg term_setters [] = {
+    {NULL}
+};
+*/
+
+static const luaL_Reg term_lib_meth [] = {
+    {"__eq", l_term_equals},
+    {"__gc", l_term_gc},
+    {"__index", l_term_get_attr},
+    {"__tostring", l_term_to_n3},
+    {"__newindex", l_term_set_attr},
+
+    {NULL}
+};
+
+
+static const LEnumConst term_enums[] = {
+    {"TYPE_IRIREF", LSUP_TERM_IRIREF},
+    {"TYPE_NS_IRIREF", LSUP_TERM_NS_IRIREF},
+    {"TYPE_LITERAL", LSUP_TERM_LITERAL},
+    {"TYPE_LT_LITERAL", LSUP_TERM_LT_LITERAL},
+    {"TYPE_BNODE", LSUP_TERM_BNODE},
+
+    {"LINK_INBOUND", LSUP_LINK_INBOUND},
+    {"LINK_OUTBOUND", LSUP_LINK_OUTBOUND},
+    {"LINK_EDGE", LSUP_LINK_EDGE},
+
+    {NULL, 0}
+};
+
+
+static const LStringConst term_strings[] = {
+    {"RDF_TYPE", LSUP_RDF_TYPE},
+    {"RDF_TYPE_NS", LSUP_RDF_TYPE_NS},
+    {"DEFAULT_DTYPE", DEFAULT_DTYPE},
+    {"DEFAULT_DTYPE_NS", DEFAULT_DTYPE_NS},
+
+    {NULL, 0}
+};
+
+
+int luaopen_lsup_term (lua_State *L)
+{
+    LSUP_init();  // This is idempotent: no problem calling it multiple times.
+    luaL_newmetatable (L, "LSUP.Term");
+    luaL_setfuncs (L, term_lib_meth, 0);
+
+    // Getters table.
+    lua_newtable (L);
+    for (int i = 0; term_getters[i].name != NULL; i++) {
+        lua_pushcfunction (L, term_getters[i].func);
+        lua_setfield (L, -2, term_getters[i].name);
+    }
+    // Set getters table as a value for the Term metatable.
+    lua_setfield (L, -2, "getters");
+
+    /*
+     * Metatables for ancillary types.
+     */
+    // Term set.
+    luaL_newmetatable (L, "LSUP.TermSet");
+    lua_pushcfunction (L, term_set_gc);
+    lua_setfield (L, -2, "__gc");
+    lua_pushcfunction (L, l_term_set_iter_init);
+    lua_setfield (L, -2, "__pairs");
+
+    // Link map.
+    luaL_newmetatable (L, "LSUP.LinkMap");
+    lua_pushcfunction (L, link_map_gc);
+    lua_setfield (L, -2, "__gc");
+    lua_pushcfunction (L, l_lmap_iter_init);
+    lua_setfield (L, -2, "__pairs");
+
+    // Link map iterator.
+    luaL_newmetatable (L, "LSUP.LMapIterator");
+    lua_pushcfunction (L, lmap_iter_gc);
+    lua_setfield (L, -2, "__gc");
+
+    luaL_newlib (L, term_lib_fn);
+
+    // Module-level constants.
+    push_string_const (L, term_strings);
+    push_int_const (L, term_enums);
+
+    return 1;
+}

+ 187 - 0
lua/src/lua_triple.c

@@ -0,0 +1,187 @@
+#include "lua_lsup.h"
+
+#define check_triple(L) \
+    *(LSUP_Triple **)luaL_checkudata(L, 1, "LSUP.Triple")
+
+
+/*
+ * Factory methods.
+ */
+
+static int l_triple_new (lua_State *L)
+{
+    LSUP_Triple **trp_p = lua_newuserdatauv (L, sizeof (*trp_p), 1);
+    luaL_getmetatable (L, "LSUP.Triple");
+    lua_setmetatable (L, -2);
+    LSUP_Term
+        *s = *(LSUP_Term **)luaL_checkudata(L, 1, "LSUP.Term"),
+        *p = *(LSUP_Term **)luaL_checkudata(L, 2, "LSUP.Term"),
+        *o = *(LSUP_Term **)luaL_checkudata(L, 3, "LSUP.Term");
+
+    *trp_p = LSUP_triple_new (
+            LSUP_term_copy (s),
+            LSUP_term_copy (p),
+            LSUP_term_copy (o));
+    if (!*trp_p) return luaL_error (L, "Error while creating a triple!");
+
+    return 1;
+}
+
+
+/*
+ * Class methods.
+ */
+
+static int l_triple_gc (lua_State *L)
+{
+    LSUP_Triple *trp = check_triple (L);
+    if (trp) LSUP_triple_free (trp);
+
+    return 0;
+}
+
+
+/** @brief Get information about a triple's term.
+ *
+ * Note that this is only a visual representation, as terms cannot be modified
+ * outside of their containing triple (unlike in the C library).
+ *
+ * @FIXME this doesn't allow chaining access methods, e.g. trp.s.data
+ *
+ * To get an independent copy of a triple term, use triple.copy_term().
+ */
+static int l_triple_get_term (lua_State *L)
+{
+    const LSUP_Triple *trp = check_triple (L);
+    const char pos = luaL_checkstring (L, 2)[0];
+    LSUP_TriplePos pn;
+    if (pos == 's') pn = TRP_POS_S;
+    else if (pos == 'p') pn = TRP_POS_P;
+    else if (pos == 'o') pn = TRP_POS_O;
+    else return luaL_error(L, "Out of range position: %c.", pos);
+
+    LSUP_Term *t = LSUP_triple_pos (trp, pn);
+    char *nt_term = NULL;
+    CHECK (nt_codec.encode_term (t, NULL, &nt_term), fail);
+    lua_pushfstring (L, "LSUP.Term @ %p %s", t, nt_term);
+
+    return 1;
+
+fail:
+    if (nt_term) free (nt_term);
+    return luaL_error (L, "Error encoding triple term for display!");
+}
+
+
+static int l_triple_set_term (lua_State *L)
+{
+    LSUP_Triple *trp = check_triple (L);
+    const char pos = luaL_checkstring (L, 2)[0];
+    const LSUP_Term *t = *(LSUP_Term **)luaL_checkudata(L, 3, "LSUP.Term");
+
+    LSUP_Term *new_t = LSUP_term_copy (t);
+
+    if (pos == 's') {
+        LSUP_term_free (trp->s);
+        trp->s = new_t;
+    } else if (pos == 'p') {
+        LSUP_term_free (trp->p);
+        trp->p = new_t;
+    } else if (pos == 'o') {
+        LSUP_term_free (trp->o);
+        trp->o = new_t;
+    } else return luaL_error(L, "Out of range position: %c.", pos);
+
+    return 1;
+}
+
+
+static int l_triple_copy_term (lua_State *L)
+{
+    const LSUP_Triple *trp = check_triple (L);
+    const char pos = luaL_checkstring (L, 2)[0];
+    LSUP_TriplePos pn;
+    if (pos == 's') pn = TRP_POS_S;
+    else if (pos == 'p') pn = TRP_POS_P;
+    else if (pos == 'o') pn = TRP_POS_O;
+    else return luaL_error(L, "Copy: Out of range position: %c.", pos);
+
+    LSUP_Term **tp = allocate_term (L);
+    *tp = LSUP_term_copy (LSUP_triple_pos (trp, pn));
+    if (!*tp) return luaL_error (L, "Error while copying a triple term!");
+
+    return 1;
+}
+
+
+static int l_triple_to_n3 (lua_State *L)
+{
+    const LSUP_Triple *trp = check_triple (L);
+    char *nt_term = NULL;
+
+    for (int i = TRP_POS_S; i <= TRP_POS_O; i++) {
+        CHECK (nt_codec.encode_term (
+                    LSUP_triple_pos (trp, i), NULL, &nt_term), fail);
+        lua_pushstring (L, nt_term);
+        lua_pushstring (L, " ");
+    }
+    free (nt_term);
+    lua_pushstring (L, ".");  // N3 statement termination.
+    lua_concat (L, 7);
+
+    return 1;
+
+fail:
+    if (nt_term) free (nt_term);
+    return luaL_error (L, "Error encoding triple term.");
+}
+
+
+/*
+ * Library setup.
+ */
+
+static const struct luaL_Reg triple_lib_fn [] = {
+    {"new", l_triple_new},
+    {NULL}
+};
+
+
+static const struct luaL_Reg triple_lib_meth [] = {
+    {"__gc", l_triple_gc},
+    {"__index", l_triple_get_term},
+    {"__newindex", l_triple_set_term},
+    {"__tostring", l_triple_to_n3},
+
+    {"copy_term", l_triple_copy_term},
+
+    {NULL}
+};
+
+
+static const LEnumConst triple_enums[] = {
+    {"POS_S", TRP_POS_S},
+    {"POS_P", TRP_POS_P},
+    {"POS_O", TRP_POS_O},
+    {NULL, 0}
+};
+
+
+int luaopen_lsup_triple (lua_State *L)
+{
+    LSUP_init();  // This is idempotent: no problem calling it multiple times.
+    luaL_newmetatable (L, "LSUP.Triple");
+    lua_pushvalue (L, -1);
+
+    // MT
+    lua_setfield (L, -1, "__index");
+    luaL_setfuncs (L, triple_lib_meth, 0);
+
+    luaL_newlib (L, triple_lib_fn);
+
+    // Module-level constants.
+    push_int_const (L, triple_enums);
+
+    return 1;
+}
+

+ 77 - 0
lua/src/stackdump.h

@@ -0,0 +1,77 @@
+#ifndef _STACK_DUMP_H
+#define _STACK_DUMP_H
+
+#ifdef DEBUG
+#define STACK_DUMP(...) stack_dump (__VA_ARGS__)
+
+static void
+stack_dump (lua_State *L, const char *title)
+{
+    // print out the scalar value or type of args on the stack 
+    int i;
+    int top = lua_gettop(L); /* depth of the stack */
+    printf("%20s : [", title);
+    for (i = 1; i <= top; i++) { /* repeat for each level */
+        int t = lua_type(L, i);
+        switch (t)
+        {
+            case LUA_TNIL:
+                {
+                    printf("nil");
+                    break;
+                }
+            case LUA_TBOOLEAN:
+                {
+                    printf(lua_toboolean(L, i) ? "true" : "false");
+                    break;
+                }
+            case LUA_TLIGHTUSERDATA:
+                {
+                    printf("lud@%p", lua_touserdata(L, i));
+                    break;
+                }
+            case LUA_TNUMBER:
+                {
+                    printf("%g", lua_tonumber(L, i));
+                    break;
+                }
+            case LUA_TSTRING:
+                {
+                    printf("'%s'", lua_tostring(L, i));
+                    break;
+                }
+            case LUA_TTABLE:
+                {
+                    printf("{}");
+                    break;
+                }
+            case LUA_TFUNCTION:
+                {
+                    printf("f@%p", lua_touserdata(L, i));
+                    break;
+                }
+            case LUA_TUSERDATA:
+                {
+                    printf("ud(%p)", lua_touserdata(L, i));
+                    break;
+                }
+            case LUA_TTHREAD:
+                {
+                    printf("Thrd(%p)", lua_touserdata(L, i));
+                    break;
+                }
+            default:
+                { // other values
+                    printf("%s", lua_typename(L, t));
+                    break;
+                }
+        }
+        if (i<top) printf(" "); /* put a separator */
+    }
+    printf("]\n"); /* end the listing */
+}
+#else
+#define STACK_DUMP(...)
+#endif  // DEBUG
+
+#endif  // _STACK_DUMP_H

+ 9 - 0
lua/valgrind-lua-noreadline.supp

@@ -0,0 +1,9 @@
+{
+   <insert_a_suppression_name_here>
+   Memcheck:Leak
+   match-leak-kinds: all
+   fun:*
+   ...
+   fun:luaB_dofile
+   ...
+}

+ 1 - 1
src/codec/codec_ttl.c

@@ -249,7 +249,7 @@ gr_to_ttl_iter (void *h, char **res_p) {
     LSUP_LinkMap *lmap = LSUP_graph_connections (
     LSUP_LinkMap *lmap = LSUP_graph_connections (
             it->gr, s, LSUP_LINK_OUTBOUND);
             it->gr, s, LSUP_LINK_OUTBOUND);
 
 
-    LSUP_LinkMapIterator *lmit = LSUP_link_map_iter_new (lmap, s);
+    LSUP_LinkMapIterator *lmit = LSUP_link_map_iter_new (lmap);
     LSUP_Term *p = NULL;
     LSUP_Term *p = NULL;
     LSUP_TermSet *o_ts = NULL;
     LSUP_TermSet *o_ts = NULL;
     char *p_join = " ";
     char *p_join = " ";

+ 7 - 6
src/codec/grammar_ttl.y

@@ -79,7 +79,7 @@ base        ::= BASE WS IRIREF(D) PERIOD . {
             }
             }
 
 
 triples 	::= subject(S) ows predObjList(L) PERIOD . {
 triples 	::= subject(S) ows predObjList(L) PERIOD . {
-                size_t ct = LSUP_graph_add_link_map (state->it, S, L);
+                size_t ct = LSUP_graph_add_link_map (state->it, L);
                 state->ct += ct;
                 state->ct += ct;
                 state->rc = LSUP_OK;
                 state->rc = LSUP_OK;
                 LOG_TRACE("Added %lu triples.", ct);
                 LOG_TRACE("Added %lu triples.", ct);
@@ -88,7 +88,7 @@ triples 	::= subject(S) ows predObjList(L) PERIOD . {
                 LSUP_link_map_free (L);
                 LSUP_link_map_free (L);
             }
             }
 triples 	::= subject(S) ows predObjList(L) SEMICOLON PERIOD . [PERIOD] {
 triples 	::= subject(S) ows predObjList(L) SEMICOLON PERIOD . [PERIOD] {
-                size_t ct = LSUP_graph_add_link_map (state->it, S, L);
+                size_t ct = LSUP_graph_add_link_map (state->it, L);
                 state->ct += ct;
                 state->ct += ct;
                 state->rc = LSUP_OK;
                 state->rc = LSUP_OK;
                 LOG_TRACE("Added %lu triples.", ct);
                 LOG_TRACE("Added %lu triples.", ct);
@@ -100,7 +100,7 @@ triples 	::= subject(S) ows predObjList(L) SEMICOLON PERIOD . [PERIOD] {
 %type predObjList       { LSUP_LinkMap * }
 %type predObjList       { LSUP_LinkMap * }
 %destructor predObjList { LSUP_link_map_free ($$); }
 %destructor predObjList { LSUP_link_map_free ($$); }
 predObjList(A) ::= predicate(P) ows objectList(O) . [SEMICOLON] {
 predObjList(A) ::= predicate(P) ows objectList(O) . [SEMICOLON] {
-                A = LSUP_link_map_new (LSUP_LINK_OUTBOUND);
+                A = LSUP_link_map_new (state->lms, LSUP_LINK_OUTBOUND);
                 LSUP_link_map_add (A, P, O);
                 LSUP_link_map_add (A, P, O);
             }
             }
 predObjList(A) ::= predObjList(L) SEMICOLON predicate(P) ows objectList(O) . {
 predObjList(A) ::= predObjList(L) SEMICOLON predicate(P) ows objectList(O) . {
@@ -122,8 +122,8 @@ objectList(A) ::= object(O) . [IRIREF] {
 
 
 %type subject { LSUP_Term * }
 %type subject { LSUP_Term * }
 %destructor subject { LSUP_term_free ($$); }
 %destructor subject { LSUP_term_free ($$); }
-subject 	::= resource .
-subject 	::= blank .
+subject 	::= resource(D) . { state->lms = D; }
+subject 	::= blank(D) . { state->lms = D; }
 
 
 %type predicate { LSUP_Term * }
 %type predicate { LSUP_Term * }
 %destructor predicate { LSUP_term_free ($$); }
 %destructor predicate { LSUP_term_free ($$); }
@@ -216,7 +216,8 @@ blank(A)    ::= LBRACKET RBRACKET . [BNODE_ID] {
             }
             }
 blank(A)    ::= LBRACKET predObjList(L) RBRACKET . [BNODE_ID] {
 blank(A)    ::= LBRACKET predObjList(L) RBRACKET . [BNODE_ID] {
                 A = LSUP_term_new (LSUP_TERM_BNODE, NULL, NULL);
                 A = LSUP_term_new (LSUP_TERM_BNODE, NULL, NULL);
-                state->ct += LSUP_graph_add_link_map (state->it, A, L);
+                state->lms = A;
+                state->ct += LSUP_graph_add_link_map (state->it, L);
                 LOG_TRACE("Created list BN: _:%s", A->data);
                 LOG_TRACE("Created list BN: _:%s", A->data);
 
 
                 LSUP_link_map_free (L);
                 LSUP_link_map_free (L);

+ 56 - 26
src/graph.c

@@ -77,8 +77,8 @@ LSUP_graph_get_txn (
 
 
     size_t _ct = 0;
     size_t _ct = 0;
     while (store->sif->lu_next_fn (it, sspo, NULL) == LSUP_OK) {
     while (store->sif->lu_next_fn (it, sspo, NULL) == LSUP_OK) {
-        // TODO This is inefficient, it's deserializing a buffer triple that
-        // will be re-serialized by LSUP_graph_add_iter.
+        // This is deserializing a buffer triple that will be re-serialized by
+        // LSUP_graph_add_iter. But it's necessary to relativize URIs.
         LSUP_Triple *spo = LSUP_triple_new_from_btriple (sspo);
         LSUP_Triple *spo = LSUP_triple_new_from_btriple (sspo);
         LSUP_graph_add_iter (add_it, spo);
         LSUP_graph_add_iter (add_it, spo);
         LSUP_triple_free (spo);
         LSUP_triple_free (spo);
@@ -141,23 +141,47 @@ LSUP_graph_bool_op_txn (
     // Handle transactions for graphs possibly in different storage back ends.
     // Handle transactions for graphs possibly in different storage back ends.
     void
     void
         *lu1_txn = NULL,
         *lu1_txn = NULL,
-        *lu2_txn = NULL;
+        *lu2_txn = NULL,
+        *res_txn = NULL;
+    // Whether gr1 or gr2 txn will be open independently from res txn.
+    bool
+        open_txn1 = false,
+        open_txn2 = false;
 
 
     add_it = res->store->sif->add_init_fn (res->store->data, res_sc, txn);
     add_it = res->store->sif->add_init_fn (res->store->data, res_sc, txn);
 
 
-    // If either source graph is in the same store as the destination and has
-    // an open transaction, reuse that transaction. A new reader cannot be
-    // opened in LMDB while a writer is open already.
-    if (
-            gr1->store->sif->features & LSUP_STORE_TXN
-            && gr1->store == res->store)
-        lu1_txn = res->store->sif->iter_txn_fn (add_it);
+    if (res->store->sif->features & LSUP_STORE_TXN)
+        res_txn = res->store->sif->iter_txn_fn (add_it);
+
+    /* If either source graph is in the same store as the destination and has
+     * an open transaction, reuse that transaction. A new reader cannot be
+     * opened in LMDB while a writer is open already.
+     */
+    // Handle gr1 transaction.
     if (gr1->store->sif->features & LSUP_STORE_TXN) {
     if (gr1->store->sif->features & LSUP_STORE_TXN) {
-        if (gr1->store == res->store)
-            lu2_txn = res->store->sif->iter_txn_fn (add_it);
-        else if (gr2->store == gr1->store)
-            lu2_txn = lu1_txn;
+        if (gr1->store == res->store) lu1_txn = res_txn;
+        // FIXME: MDB_RDONLY is implementation-specific and doesn't belong here.
+        else {
+            CHECK (gr1->store->sif->txn_begin_fn (
+                gr1->store->data, MDB_RDONLY, &lu1_txn), fail);
+            open_txn1 = true;
+        }
+    }
+
+    // Handle gr2 transaction.
+    if (gr2->store->sif->features & LSUP_STORE_TXN) {
+        if (gr2->store == res->store) lu2_txn = res_txn;
+        else if (gr2->store == gr1->store) lu2_txn = lu1_txn;
+        // FIXME: see above.
+        else {
+            CHECK (gr2->store->sif->txn_begin_fn (
+                gr2->store->data, MDB_RDONLY, &lu2_txn), fail);
+            open_txn2 = true;
+        }
     }
     }
+    LOG_TRACE (
+            "lu1_txn: %p ; lu2_txn: %p ; res_txn: %p",
+            lu1_txn, lu2_txn, res_txn);
 
 
     /* BEGIN XOR block. */
     /* BEGIN XOR block. */
 
 
@@ -201,6 +225,9 @@ LSUP_graph_bool_op_txn (
     }
     }
     gr1->store->sif->lu_free_fn (lu1_it);
     gr1->store->sif->lu_free_fn (lu1_it);
 
 
+    if (open_txn1) gr1->store->sif->txn_commit_fn (lu1_txn);
+    if (open_txn2) gr2->store->sif->txn_commit_fn (lu2_txn);
+
     res->store->sif->add_done_fn (add_it);
     res->store->sif->add_done_fn (add_it);
     LSUP_btriple_free (sspo);
     LSUP_btriple_free (sspo);
     LSUP_buffer_free (res_sc);
     LSUP_buffer_free (res_sc);
@@ -212,6 +239,8 @@ LSUP_graph_bool_op_txn (
     return rc;
     return rc;
 
 
 fail:
 fail:
+    if (lu1_txn) gr1->store->sif->txn_abort_fn (lu1_txn);
+    if (lu2_txn) gr2->store->sif->txn_abort_fn (lu2_txn);
     LSUP_graph_free (res);
     LSUP_graph_free (res);
     return rc;
     return rc;
 }
 }
@@ -622,11 +651,14 @@ LSUP_graph_contains (const LSUP_Graph *gr, const LSUP_Triple *spo)
 
 
 
 
 void
 void
-LSUP_graph_print (LSUP_Graph *gr)
+LSUP_graph_print (const LSUP_Graph *gr)
 {
 {
     size_t ct;
     size_t ct;
     LSUP_GraphIterator *it = LSUP_graph_lookup (gr, NULL, NULL, NULL, &ct);
     LSUP_GraphIterator *it = LSUP_graph_lookup (gr, NULL, NULL, NULL, &ct);
-    if (UNLIKELY (!it)) log_error ("Could not print graph contents.");
+    if (UNLIKELY (!it)) {
+        log_error ("Could not inspect graph for printing.");
+        return;
+    }
 
 
     printf ("*** Graph %s (%zu triples):\n\n", gr->uri->data, ct);
     printf ("*** Graph %s (%zu triples):\n\n", gr->uri->data, ct);
 
 
@@ -648,9 +680,9 @@ LSUP_graph_print (LSUP_Graph *gr)
 
 
 LSUP_LinkMap *
 LSUP_LinkMap *
 LSUP_graph_connections (
 LSUP_graph_connections (
-        const LSUP_Graph *gr, LSUP_Term *t, LSUP_LinkType type)
+        const LSUP_Graph *gr, const LSUP_Term *t, const LSUP_LinkType type)
 {
 {
-    LSUP_Term
+    const LSUP_Term
         *s = NULL,
         *s = NULL,
         *p = NULL,
         *p = NULL,
         *o = NULL;
         *o = NULL;
@@ -671,7 +703,6 @@ LSUP_graph_connections (
         pos1 = TRP_POS_P;
         pos1 = TRP_POS_P;
         pos2 = TRP_POS_S;
         pos2 = TRP_POS_S;
     } else {
     } else {
-        // Very unlikely.
         log_error ("Invalid connection type: %d", type);
         log_error ("Invalid connection type: %d", type);
         return NULL;
         return NULL;
     }
     }
@@ -690,7 +721,7 @@ LSUP_graph_connections (
     }
     }
     LSUP_graph_iter_free(it);
     LSUP_graph_iter_free(it);
 
 
-    LSUP_LinkMap *ret = LSUP_link_map_new (type);
+    LSUP_LinkMap *ret = LSUP_link_map_new (t, type);
     size_t i = 0;
     size_t i = 0;
     LSUP_Term *lt;
     LSUP_Term *lt;
     while (LSUP_term_set_next (lts, &i, &lt) != LSUP_END) {
     while (LSUP_term_set_next (lts, &i, &lt) != LSUP_END) {
@@ -706,15 +737,15 @@ LSUP_graph_connections (
 
 
 LSUP_TermSet *
 LSUP_TermSet *
 LSUP_graph_term_set (
 LSUP_graph_term_set (
-        const LSUP_Graph *gr, LSUP_Term *t1, LSUP_TriplePos t1_pos,
-        LSUP_Term *t2, LSUP_TriplePos t2_pos)
+        const LSUP_Graph *gr, const LSUP_Term *t1, const LSUP_TriplePos t1_pos,
+        const LSUP_Term *t2, const LSUP_TriplePos t2_pos)
 {
 {
     if (t1_pos == t2_pos) {
     if (t1_pos == t2_pos) {
         log_error ("Term 1 and 2 positions cannot be the same!");
         log_error ("Term 1 and 2 positions cannot be the same!");
         return NULL;
         return NULL;
     }
     }
 
 
-    LSUP_Term *spo_l[3] = {NULL};
+    const LSUP_Term *spo_l[3] = {NULL};
     spo_l[t1_pos] = t1;
     spo_l[t1_pos] = t1;
     spo_l[t2_pos] = t2;
     spo_l[t2_pos] = t2;
     LSUP_TriplePos rpos = 0;  // Position of term to be added to results.
     LSUP_TriplePos rpos = 0;  // Position of term to be added to results.
@@ -760,12 +791,11 @@ LSUP_graph_unique_terms (const LSUP_Graph *gr, LSUP_TriplePos pos)
 
 
 
 
 size_t
 size_t
-LSUP_graph_add_link_map (
-        LSUP_GraphIterator *it, LSUP_Term *t, LSUP_LinkMap *lmap)
+LSUP_graph_add_link_map (LSUP_GraphIterator *it, LSUP_LinkMap *lm)
 {
 {
     LSUP_Triple *spo = TRP_DUMMY;
     LSUP_Triple *spo = TRP_DUMMY;
     size_t ct = 0;
     size_t ct = 0;
-    LSUP_LinkMapIterator *lmit = LSUP_link_map_iter_new (lmap, t);
+    LSUP_LinkMapIterator *lmit = LSUP_link_map_iter_new (lm);
 
 
     while (LSUP_link_map_triples (lmit, spo) != LSUP_END) {
     while (LSUP_link_map_triples (lmit, spo) != LSUP_END) {
         LSUP_rc rc = LSUP_graph_add_iter (it, spo);
         LSUP_rc rc = LSUP_graph_add_iter (it, spo);

+ 2 - 2
src/store_htable.c

@@ -295,9 +295,8 @@ htstore_lookup (
     it->store = store;
     it->store = store;
     it->rc = LSUP_END;
     it->rc = LSUP_END;
 
 
-    if (hashmap_count (store->keys) == 0) return it;
-
     if (ct) *ct = 0;
     if (ct) *ct = 0;
+    if (hashmap_count (store->keys) == 0) return it;
 
 
     it->luk[0] = LSUP_buffer_hash (ss);
     it->luk[0] = LSUP_buffer_hash (ss);
     it->luk[1] = LSUP_buffer_hash (sp);
     it->luk[1] = LSUP_buffer_hash (sp);
@@ -468,6 +467,7 @@ tkey_to_strp (
         if (UNLIKELY (!tmp)) return LSUP_DB_ERR;
         if (UNLIKELY (!tmp)) return LSUP_DB_ERR;
         LSUP_btriple_pos(sspo, i)->addr = tmp->data;
         LSUP_btriple_pos(sspo, i)->addr = tmp->data;
         LSUP_btriple_pos(sspo, i)->size = tmp->size;
         LSUP_btriple_pos(sspo, i)->size = tmp->size;
+        LSUP_btriple_pos(sspo, i)->flags |= LSUP_BUF_BORROWED;
     }
     }
 
 
     return LSUP_OK;
     return LSUP_OK;

+ 1 - 1
src/store_mdb.c

@@ -742,7 +742,7 @@ mdbstore_lookup (
     else {
     else {
         it->rc = mdb_txn_begin (it->store->env, NULL, MDB_RDONLY, &it->txn);
         it->rc = mdb_txn_begin (it->store->env, NULL, MDB_RDONLY, &it->txn);
         if (it->rc != MDB_SUCCESS) {
         if (it->rc != MDB_SUCCESS) {
-            log_error ("Database error: %s", LSUP_strerror (it->rc));
+            log_error ("Database error in lookup: %s", LSUP_strerror (it->rc));
             return NULL;
             return NULL;
         }
         }
         LOG_TRACE ("Opening new MDB transaction @%p", it->txn);
         LOG_TRACE ("Opening new MDB transaction @%p", it->txn);

+ 35 - 23
src/term.c

@@ -55,8 +55,7 @@ struct link_map_iter {
     const LSUP_LinkMap *map;        ///< Link map to iterate.
     const LSUP_LinkMap *map;        ///< Link map to iterate.
     size_t              i;          ///< Linking term loop cursor.
     size_t              i;          ///< Linking term loop cursor.
     size_t              j;          ///< Term set loop cursor.
     size_t              j;          ///< Term set loop cursor.
-    LSUP_Term *         ext;        ///< External link to look for connections.
-    Link *              link;       ///< Current link being retrieved.
+    const Link *        link;       ///< Current link being retrieved.
 };
 };
 
 
 
 
@@ -64,13 +63,16 @@ struct link_map_iter {
  * A link map is thus nested:
  * A link map is thus nested:
  *
  *
  * - A link map contains a hash map of Link instances (link).
  * - A link map contains a hash map of Link instances (link).
+ * - It also contains the single term that the other terms are related to
+ *    (linked_t).
  * - Each Link contains a KeyedTerm (term) and a TermSet (tset).
  * - Each Link contains a KeyedTerm (term) and a TermSet (tset).
  * - Each term set is a hash map of KeyedTerm instances.
  * - Each term set is a hash map of KeyedTerm instances.
  * - Each KeyedTerm contains a Term and its hash.
  * - Each KeyedTerm contains a Term and its hash.
  */
  */
 typedef struct link_map {
 typedef struct link_map {
     LSUP_LinkType       type;       ///< Link type.
     LSUP_LinkType       type;       ///< Link type.
-    struct hashmap *    links;      ///< Map of #Link instances.
+    LSUP_Term          *linked_t;   ///< Linked term.
+    struct hashmap     *links;      ///< Map of #Link instances.
 } LSUP_LinkMap;
 } LSUP_LinkMap;
 
 
 
 
@@ -599,28 +601,38 @@ LSUP_term_set_next (LSUP_TermSet *ts, size_t *i, LSUP_Term **term)
 
 
 void
 void
 LSUP_term_set_free (LSUP_TermSet *ts)
 LSUP_term_set_free (LSUP_TermSet *ts)
-{ hashmap_free (ts); }
+{
+    if (UNLIKELY (!ts)) return;
+    hashmap_free (ts);
+}
 
 
 
 
 LSUP_LinkMap *
 LSUP_LinkMap *
-LSUP_link_map_new (LSUP_LinkType type)
+LSUP_link_map_new (const LSUP_Term *linked_term, LSUP_LinkType type)
 {
 {
-    LSUP_LinkMap *cm;
-    MALLOC_GUARD (cm, NULL);
-    cm->type = type;
-    cm->links = hashmap_new (
+    LSUP_LinkMap *lm;
+    MALLOC_GUARD (lm, NULL);
+    lm->type = type;
+    lm->links = hashmap_new (
             sizeof (Link), 0, LSUP_HASH_SEED, 0,
             sizeof (Link), 0, LSUP_HASH_SEED, 0,
             link_map_hash_fn, link_map_cmp_fn, link_map_free_fn, NULL);
             link_map_hash_fn, link_map_cmp_fn, link_map_free_fn, NULL);
+    if (!linked_term) {
+        log_error ("term must not be NULL.");
+        free (lm);
+        return NULL;
+    }
+    lm->linked_t = LSUP_term_copy (linked_term);
 
 
-    return cm;
+    return lm;
 }
 }
 
 
 
 
 void
 void
-LSUP_link_map_free (LSUP_LinkMap *cm)
+LSUP_link_map_free (LSUP_LinkMap *lm)
 {
 {
-    hashmap_free (cm->links);
-    free (cm);
+    hashmap_free (lm->links);
+    LSUP_term_free (lm->linked_t);
+    free (lm);
 }
 }
 
 
 
 
@@ -632,12 +644,12 @@ LSUP_link_map_type (const LSUP_LinkMap *map)
 // TODO Memory error handling.
 // TODO Memory error handling.
 LSUP_rc
 LSUP_rc
 LSUP_link_map_add (
 LSUP_link_map_add (
-        LSUP_LinkMap *cmap, LSUP_Term *term, LSUP_TermSet *tset)
+        LSUP_LinkMap *lmap, LSUP_Term *term, LSUP_TermSet *tset)
 {
 {
     // Keyed term to look up the link term and insert it, if necessary.
     // Keyed term to look up the link term and insert it, if necessary.
     KeyedTerm entry_s = {.key=LSUP_term_hash (term), .term=term};
     KeyedTerm entry_s = {.key=LSUP_term_hash (term), .term=term};
 
 
-    Link *ex = hashmap_get (cmap->links, &(Link){.term=&entry_s});
+    Link *ex = hashmap_get (lmap->links, &(Link){.term=&entry_s});
     if (ex) {
     if (ex) {
         // Add terms one by one to the existing term set.
         // Add terms one by one to the existing term set.
         LOG_TRACE(
         LOG_TRACE(
@@ -666,7 +678,7 @@ LSUP_link_map_add (
         MALLOC_GUARD (ins, LSUP_MEM_ERR);
         MALLOC_GUARD (ins, LSUP_MEM_ERR);
         memcpy (ins, &entry_s, sizeof (entry_s));
         memcpy (ins, &entry_s, sizeof (entry_s));
         Link link = {.term=ins, .tset=tset};
         Link link = {.term=ins, .tset=tset};
-        hashmap_set (cmap->links, &link);
+        hashmap_set (lmap->links, &link);
     }
     }
 
 
     return LSUP_OK;
     return LSUP_OK;
@@ -674,20 +686,19 @@ LSUP_link_map_add (
 
 
 
 
 LSUP_LinkMapIterator *
 LSUP_LinkMapIterator *
-LSUP_link_map_iter_new (const LSUP_LinkMap *lmap, LSUP_Term *ext)
+LSUP_link_map_iter_new (const LSUP_LinkMap *lmap)
 {
 {
     LSUP_LinkMapIterator *it;
     LSUP_LinkMapIterator *it;
     CALLOC_GUARD (it, NULL);
     CALLOC_GUARD (it, NULL);
     it->map = lmap;
     it->map = lmap;
-    it->ext = ext;
 
 
     return it;
     return it;
 }
 }
 
 
 
 
+// This leaves the link and link map references intact.
 void
 void
-LSUP_link_map_iter_free (LSUP_LinkMapIterator *it)
-{ free (it); }
+LSUP_link_map_iter_free (LSUP_LinkMapIterator *it) { free (it); }
 
 
 
 
 LSUP_rc
 LSUP_rc
@@ -711,10 +722,10 @@ LSUP_link_map_triples (
 {
 {
     // Assign external (related) term.
     // Assign external (related) term.
     if (it->map->type == LSUP_LINK_INBOUND)
     if (it->map->type == LSUP_LINK_INBOUND)
-        spo->o = it->ext;
+        spo->o = it->map->linked_t;
     else if (it->map->type == LSUP_LINK_OUTBOUND)
     else if (it->map->type == LSUP_LINK_OUTBOUND)
-        spo->s = it->ext;
-    else spo->p = it->ext;
+        spo->s = it->map->linked_t;
+    else spo->p = it->map->linked_t;
 
 
     KeyedTerm *kt;
     KeyedTerm *kt;
 
 
@@ -855,6 +866,7 @@ term_init (
             log_warn ("Lang tag is NULL. Creating a non-tagged literal.");
             log_warn ("Lang tag is NULL. Creating a non-tagged literal.");
             term->type = LSUP_TERM_LITERAL;
             term->type = LSUP_TERM_LITERAL;
         } else {
         } else {
+            // FIXME metadata should be const all across.
             char *lang_str = (char *) metadata;
             char *lang_str = (char *) metadata;
             LOG_TRACE("Lang string: '%s'", lang_str);
             LOG_TRACE("Lang string: '%s'", lang_str);
             // Lang tags longer than 7 characters will be truncated.
             // Lang tags longer than 7 characters will be truncated.

+ 12 - 1
test/assets/triples.h

@@ -5,6 +5,17 @@
 
 
 #define NUM_TRP 10
 #define NUM_TRP 10
 
 
+/*
+ * This results in the following set:
+ * <urn:s:0> <urn:p:0> <urn:o:0> .
+ * <urn:s:1> <urn:p:1> <urn:o:1> .
+ * <urn:s:2> <urn:p:2> <urn:o:2> .
+ * <urn:s:0> <urn:p:1> <urn:o:2> .
+ * <urn:s:0> <urn:p:2> "String 1" .
+ * <urn:s:0> <urn:p:5> "String 1"^^<urn:mydatatype:string> .
+ * <urn:s:1> <urn:p:6> "String 1"@es-ES .
+ * <urn:s:0> <urn:p:2> "String 1"^^<urn:mydatatype:string> .
+ */
 LSUP_Triple **create_triples()
 LSUP_Triple **create_triples()
 {
 {
     LSUP_NSMap *nsm = LSUP_nsmap_new ();
     LSUP_NSMap *nsm = LSUP_nsmap_new ();
@@ -20,7 +31,7 @@ LSUP_Triple **create_triples()
                     "String 1",
                     "String 1",
                     LSUP_iriref_new ("urn:mydatatype:string", NULL));
                     LSUP_iriref_new ("urn:mydatatype:string", NULL));
 
 
-    LSUP_Triple **trp = calloc (sizeof (LSUP_Triple), NUM_TRP + 1);
+    LSUP_Triple **trp = calloc (sizeof (*trp), NUM_TRP + 1);
     trp[0] = LSUP_triple_new (
     trp[0] = LSUP_triple_new (
             s0,
             s0,
             LSUP_iriref_new ("urn:p:0", NULL),
             LSUP_iriref_new ("urn:p:0", NULL),

+ 142 - 6
test/test_graph.c

@@ -49,12 +49,11 @@ _graph_add (LSUP_StoreType type)
 
 
     LSUP_Graph *gr;
     LSUP_Graph *gr;
     LSUP_Store *store;
     LSUP_Store *store;
-    if (type == LSUP_STORE_HTABLE) {
-        gr = LSUP_graph_new (NULL, NULL, NULL);
-    } else {
-        store = LSUP_store_new (type, NULL, 0);
-        gr = LSUP_graph_new (store, NULL, NULL);
-    }
+
+    if (type == LSUP_STORE_HTABLE) store = NULL;
+    else store = LSUP_store_new (type, NULL, 0);
+
+    gr = LSUP_graph_new (store, NULL, NULL);
     ASSERT (gr != NULL, "Error creating graph!");
     ASSERT (gr != NULL, "Error creating graph!");
 
 
     size_t ct;
     size_t ct;
@@ -75,7 +74,17 @@ _graph_add (LSUP_StoreType type)
 
 
     free_triples (trp); // gr copied data.
     free_triples (trp); // gr copied data.
 
 
+    // Check size again after freeing triples.
+    EXPECT_INT_EQ (LSUP_graph_size (gr), 8);
+
+    LSUP_Graph *gr2 = LSUP_graph_new (NULL, NULL, NULL);
+
+    // Test equality against empty graph.
+    ASSERT (!LSUP_graph_equals (gr, gr2), "Graphs should not be equal!");
+    ASSERT (!LSUP_graph_equals (gr2, gr), "Graphs should not be equal!");
+
     LSUP_graph_free (gr);
     LSUP_graph_free (gr);
+    LSUP_graph_free (gr2);
     if (type != LSUP_STORE_HTABLE) LSUP_store_free (store);
     if (type != LSUP_STORE_HTABLE) LSUP_store_free (store);
 
 
     return 0;
     return 0;
@@ -115,6 +124,8 @@ _graph_get (LSUP_StoreType type)
     }
     }
     LSUP_graph_add_done (it2);
     LSUP_graph_add_done (it2);
 
 
+    free_triples (trp);
+
     EXPECT_INT_EQ (LSUP_graph_size (gr1), 5);
     EXPECT_INT_EQ (LSUP_graph_size (gr1), 5);
     EXPECT_INT_EQ (LSUP_graph_size (gr2), 3);
     EXPECT_INT_EQ (LSUP_graph_size (gr2), 3);
 
 
@@ -126,6 +137,8 @@ _graph_get (LSUP_StoreType type)
     EXPECT_INT_EQ (LSUP_graph_size (gr3), LSUP_graph_size (gr1));
     EXPECT_INT_EQ (LSUP_graph_size (gr3), LSUP_graph_size (gr1));
     EXPECT_INT_EQ (LSUP_graph_size (gr4), LSUP_graph_size (gr2));
     EXPECT_INT_EQ (LSUP_graph_size (gr4), LSUP_graph_size (gr2));
 
 
+    ASSERT (!LSUP_graph_equals (gr1, gr2), "Graphs 1 and 2 are equal!");
+    ASSERT (!LSUP_graph_equals (gr3, gr4), "Graphs 3 and 4 are equal!");
     ASSERT (LSUP_graph_equals (gr1, gr3), "Graphs 1 and 3 are not equal!");
     ASSERT (LSUP_graph_equals (gr1, gr3), "Graphs 1 and 3 are not equal!");
     ASSERT (LSUP_graph_equals (gr2, gr4), "Graphs 2 and 4 are not equal!");
     ASSERT (LSUP_graph_equals (gr2, gr4), "Graphs 2 and 4 are not equal!");
 
 
@@ -133,7 +146,119 @@ _graph_get (LSUP_StoreType type)
     LSUP_graph_free (gr2);
     LSUP_graph_free (gr2);
     LSUP_graph_free (gr3);
     LSUP_graph_free (gr3);
     LSUP_graph_free (gr4);
     LSUP_graph_free (gr4);
+    if (type != LSUP_STORE_HTABLE) LSUP_store_free (store);
+
+    return 0;
+}
+
+
+static int
+_graph_link_map (LSUP_StoreType type)
+{
+    const LSUP_StoreInt *sif = LSUP_store_int (type);
+
+    if (sif->setup_fn) sif->setup_fn (NULL, true);
+
+    LSUP_Triple **trp = create_triples();
+
+    LSUP_Store *store = LSUP_store_new (type, NULL, 0);
+    LSUP_Graph *gr = LSUP_graph_new (store, NULL, NULL);
+
+    size_t ct;
+    LSUP_graph_add (gr, trp, &ct);
+
+    LSUP_LinkMap *lmap;
+    LSUP_LinkMapIterator *lmit ;
+    //LSUP_Term *k = NULL;
+    LSUP_TermSet *ts = NULL;  // Term set being iterated in link map loop.
+    LSUP_Term *k_res[9] = {NULL};  // Collected keys.
+    LSUP_Term *ts_res[9] = {NULL};  // Collected terms in term set in loop.
+    size_t i = 0, j = 0;
+
+    LSUP_Term *po[6][4] = {
+        {trp[0]->p, trp[0]->o, NULL},
+        {trp[3]->p, trp[3]->o, NULL},
+        {trp[4]->p, trp[4]->o, trp[7]->o, NULL},
+        {trp[5]->p, trp[5]->o, NULL},
+        {NULL}
+    };
+
+    // terms connected to subject, urn:s:0
+    lmap = LSUP_graph_connections (gr, trp[0]->s, LSUP_LINK_OUTBOUND);
+    lmit = LSUP_link_map_iter_new (lmap);
+    while (LSUP_link_map_next (lmit, k_res + i, &ts) == LSUP_OK) {
+        while (LSUP_term_set_next (ts, &j, ts_res + j) == LSUP_OK);
+        /*
+        // TODO test exact terms. This requires a cross-check.
+        ASSERT (
+                LSUP_term_equals (k_res[i], po[i][0]),
+                "Wrong term in link map!");
+        for (size_t k = 1; po[i][k]; k++)
+            ASSERT (
+                    LSUP_term_equals (ts_res[k - 1], po[i][k]),
+                    "Wrong term in term set!");
+        */
+        i++;
+    }
+    LSUP_link_map_iter_free (lmit);
+    LSUP_link_map_free (lmap);
+    EXPECT_INT_EQ (i, 4);
+
+    i = 0; j = 0;
+    memset (k_res, 0, sizeof (k_res));
+    memset (ts_res, 0, sizeof (ts_res));
+    LSUP_Term *so[3][3] = {
+        {trp[1]->s, trp[1]->o, NULL},
+        {trp[3]->s, trp[3]->o, NULL},
+    };
+    // terms connected to predicate, urn:p:1
+    lmap = LSUP_graph_connections (gr, trp[1]->p, LSUP_LINK_EDGE);
+    lmit = LSUP_link_map_iter_new (lmap);
+    while (LSUP_link_map_next (lmit, k_res + i, &ts) == LSUP_OK) {
+        while (LSUP_term_set_next (ts, &j, ts_res + j) == LSUP_OK);
+        /*
+        ASSERT (
+                LSUP_term_equals (k_res[i], so[i][0]),
+                "Wrong term in link map!");
+        for (size_t k = 1; so[i][k]; k++)
+            ASSERT (
+                    LSUP_term_equals (ts_res[k - 1], so[i][k]),
+                    "Wrong term in term set!");
+        */
+        i++;
+    }
+    LSUP_link_map_iter_free (lmit);
+    LSUP_link_map_free (lmap);
+    EXPECT_INT_EQ (i, 2);
+
+    i = 0; j = 0;
+    memset (k_res, 0, sizeof (k_res));
+    memset (ts_res, 0, sizeof (ts_res));
+    LSUP_Term *sp[1][3] = {
+        {trp[6]->s, trp[6]->p, NULL},
+    };
+    // terms connected to object, "String 1"@es-ES
+    lmap = LSUP_graph_connections (gr, trp[6]->o, LSUP_LINK_INBOUND);
+    lmit = LSUP_link_map_iter_new (lmap);
+    while (LSUP_link_map_next (lmit, k_res + i, &ts) == LSUP_OK) {
+        while (LSUP_term_set_next (ts, &j, ts_res + j) == LSUP_OK);
+        /*
+        ASSERT (
+                LSUP_term_equals (k_res[i], sp[i][0]),
+                "Wrong term in link map!");
+        for (size_t k = 1; sp[i][k]; k++)
+            ASSERT (
+                    LSUP_term_equals (ts_res[k - 1], sp[i][k]),
+                    "Wrong term in term set!");
+        */
+        i++;
+    }
+    LSUP_link_map_iter_free (lmit);
+    LSUP_link_map_free (lmap);
+    EXPECT_INT_EQ (i, 1);
+
     free_triples (trp);
     free_triples (trp);
+    LSUP_graph_free (gr);
     if (type != LSUP_STORE_HTABLE) LSUP_store_free (store);
     if (type != LSUP_STORE_HTABLE) LSUP_store_free (store);
 
 
     return 0;
     return 0;
@@ -568,6 +693,16 @@ BACKEND_TBL
 }
 }
 
 
 
 
+static int test_graph_link_map() {
+#define ENTRY(a, b) \
+    if (_graph_link_map (LSUP_STORE_##a) != 0) return -1;
+BACKEND_TBL
+#undef ENTRY
+
+    return 0;
+}
+
+
 static int test_graph_bool_ops() {
 static int test_graph_bool_ops() {
 #define ENTRY(a, b) \
 #define ENTRY(a, b) \
     if (_graph_bool_ops (LSUP_STORE_##a) != 0) return -1;
     if (_graph_bool_ops (LSUP_STORE_##a) != 0) return -1;
@@ -674,6 +809,7 @@ int graph_tests()
     RUN (test_graph_new);
     RUN (test_graph_new);
     RUN (test_graph_add);
     RUN (test_graph_add);
     RUN (test_graph_get);
     RUN (test_graph_get);
+    RUN (test_graph_link_map);
     RUN (test_graph_bool_ops);
     RUN (test_graph_bool_ops);
     RUN (test_graph_lookup);
     RUN (test_graph_lookup);
     RUN (test_graph_remove);
     RUN (test_graph_remove);