OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-link-grammar / link-grammar / linkage / linkage.c

982 lines · c ref:
/*************************************************************************/
/* Copyright (c) 2004                                                    */
/* Daniel Sleator, David Temperley, and John Lafferty                    */
/* Copyright 2008, 2009, 2013, 2014 Linas Vepstas                        */
/* All rights reserved                                                   */
/*                                                                       */
/* Use of the link grammar parsing system is subject to the terms of the */
/* license set forth in the LICENSE file included with this software.    */
/* This license allows free redistribution and use in source and binary  */
/* forms, with or without modification, subject to certain conditions.   */
/*                                                                       */
/*************************************************************************/

#include <stdint.h>
#include <stdlib.h>

#include "api-structures.h"
#include "connectors.h"
#include "dict-common/dict-api.h"       // linkage_get_categories
#include "dict-common/dict-affix.h"     // INFIX_MARK
#include "dict-common/dict-defines.h"   // SUBSCRIPT_MARK
#include "dict-common/idiom.h"
#include "disjunct-utils.h"
#include "link-includes.h"
#include "linkage.h"
#include "lisjuncts.h"
#include "sat-solver/sat-encoder.h"
#include "string-set.h"
#include "tokenize/wordgraph.h"
#include "tokenize/tok-structures.h" // XXX TODO provide gword access methods!
#include "tokenize/word-structures.h"    // Word_struct

#define INFIX_MARK_L 1 /* INFIX_MARK is 1 character */
#define STEM_MARK_L  1 /* stem mark is 1 character */

/* Marks around a null word. */
#define NULLWORD_START '['
#define NULLWORD_END   ']'

/**
 * Append an unmarked (i.e. without INFIXMARK) morpheme to join_buff.
 * join_buff is a zeroed-out buffer which has enough room for morpheme to be
 * added + terminating NUL.
 * Note that MT_PREFIX or MT_SUFFIX can be without an INFIX_MARK, in case
 * INFIX_MARK is not defined. XXX: What about MT_MIDDLE? (not in use yet).
 *
 * FIXME Combining contracted words is not handled yet, because combining
 * morphemes which have non-LL links to other words is not yet implemented.
 */
static void add_morpheme_unmarked(Sentence sent, char *join_buff,
                                  const char *wm, Morpheme_type mt)
{
	const char infix_mark = INFIX_MARK(sent->dict->affix_table);
	const char *sm =  get_word_subscript(wm);

	if (NULL == sm) sm = (char *)wm + strlen(wm);

	if ((MT_PREFIX == mt) && (infix_mark == sm[-INFIX_MARK_L]))
		strncat(join_buff, wm, sm-wm-INFIX_MARK_L);
	else if ((MT_SUFFIX == mt) && (infix_mark == wm[0]))
		strncat(join_buff, INFIX_MARK_L+wm, sm-wm-INFIX_MARK_L);
	else if ((MT_MIDDLE == mt))
		strncat(join_buff, INFIX_MARK_L+wm, sm-wm-2*INFIX_MARK_L);
	else
		strncat(join_buff, wm, sm-wm);
}

static const char *join_null_word(Sentence sent, Gword **wgp, size_t count)
{
	size_t i;
	char *join_buff;
	const char *s;
	size_t join_len = 0;

	for (i = 0; i < count; i++)
		join_len += strlen(wgp[i]->subword);

	join_buff = alloca(join_len+1);
	join_buff[0] = 0;

	for (i = 0; i < count; i++)
		add_morpheme_unmarked(sent, join_buff, wgp[i]->subword,
		                      wgp[i]->morpheme_type);

	s = string_set_add(join_buff, sent->string_set);

	return s;
}

/**
 * Add a null word node that represents two or more null morphemes.
 * Used for "unifying" null morphemes that are part of a single subword,
 * when only some of its morphemes (2 or more) don't have a linkage.
 * The words "start" to "end" (including) are unified by the new node.
 */
static Gword *wordgraph_null_join(Sentence sent, Gword **start, Gword **end)
{
	Gword *new_word;
	Gword **w;
	char *usubword;
	size_t join_len = 0;

	for (w = start; w <= end; w++) join_len += strlen((*w)->subword);
	usubword = alloca(join_len+1);
	usubword[0] = 0;

	for (w = start; w <= end; w++)
		add_morpheme_unmarked(sent, usubword, (*w)->subword, (*w)->morpheme_type);

	new_word = gword_new(sent, usubword);
	new_word->status |= WS_PL;
	new_word->label = "NJ";
	new_word->null_subwords = NULL;
	new_word->start = (*start)->start;
	new_word->end = (*end)->end;

	/* Link the null_subwords links of the added unifying node to the null
	 * subwords it unified. */
	for (w = start; w <= end; w++)
		gwordlist_append(&new_word->null_subwords, (Gword *)(*w));
	/* Removing const qualifier, but gwordlist_append doesn't change w->... .  */

	return new_word;
}

/**
 * Add a display wordgraph placeholder for a combined morpheme with links
 * that are not discardable.
 * This is needed only when hiding morphology. This is a kind of a hack.
 * If it is not deemed nice, the "hide morphology" mode should just not be
 * used for languages with morphemes which have links that cannot be
 * discarded on that mode (like Hebrew).
 */
static Gword *wordgraph_link_placeholder(Sentence sent, Gword *w)
{
	Gword *new_word;

	new_word = gword_new(sent, "PLACE_HOLDER");
	new_word->status |= WS_PL;
	new_word->label = "PH";
	new_word->start = w->start;
	new_word->end = w->end;

	return new_word;
}

/**
 * The functions defined in this file are primarily a part of the user API
 * for working with linkages.
 */

#define SUBSCRIPT_SEP SUBSCRIPT_DOT /* multiple-subscript separator */

#define PREFIX_SUPPRESS ("PL") /* prefix links start with this */
#define PREFIX_SUPPRESS_L 2    /* length of above */
#define SUFFIX_SUPPRESS ("LL") /* suffix links start with this */
#define SUFFIX_SUPPRESS_L 2    /* length of above */

#define HIDE_MORPHO   (('\0' != infix_mark) && !display_morphology)
/* TODO? !display_guess_marks is not implemented. */
#define DISPLAY_GUESS_MARKS true // (opts->display_guess_marks)

/* FIXME: Define an affix class MORPHOLOGY_LINKS. */
static inline bool is_morphology_link(const char *link_name)
{
	if (NULL == link_name) return false;
	return (0 == strncmp(link_name, SUFFIX_SUPPRESS, SUFFIX_SUPPRESS_L)) ||
	       (0 == strncmp(link_name, PREFIX_SUPPRESS, PREFIX_SUPPRESS_L));
}

/*
 * Remap the link array according to discarded links and words.
 *
 * The remap[] elements indicate the new WordIdx of the word.
 * A value which is -1 indicates a discarded word.
 * A NULL link_name indicates a discarded link.
 */
static void remap_linkages(Linkage lkg, const int *remap)
{
	LinkIdx i, j;

	for (i = 0, j = 0; i < lkg->num_links; i++)
	{
		Link *old_lnk = &lkg->link_array[i];

		if (NULL != old_lnk->link_name &&  /* discarded link */
		   (-1 != remap[old_lnk->rw]) && (-1 != remap[old_lnk->lw]))
		{
			Link *new_lnk = &lkg->link_array[j];
			Connector *ctmp;

			/* Copy the entire link contents, thunking the word numbers.
			 * Note that j is always <= i so this is always safe. */

			new_lnk->lw = remap[old_lnk->lw];
			new_lnk->rw = remap[old_lnk->rw];

			ctmp = new_lnk->lc;
			new_lnk->lc = old_lnk->lc;
			old_lnk->lc = ctmp;

			ctmp = new_lnk->rc;
			new_lnk->rc = old_lnk->rc;
			old_lnk->rc = ctmp;

			new_lnk->link_name = old_lnk->link_name;

			j++;
		}
	}

	lkg->num_links = j;
	/* Unused memory not freed - all of it will be freed in free_linkages(). */
}

/**
 * Remove unlinked optional words from a linkage.
 * XXX Should we remove here also the dict-cap tokens? In any case, for now they
 * are left for debug.
 */
#define D_REE 7
void remove_empty_words(Linkage lkg)
{
	size_t i, j;
	Disjunct **cdj = lkg->chosen_disjuncts;
	int *remap = alloca(lkg->num_words * sizeof(*remap));
	Gword **wgp = lkg->wg_path;

	for (i = 0, j = 0; i < lkg->num_words; i++)
	{
		/* Discard optional words that are not real null-words.  Note that
		 * if optional words don't have non-optional words after them,
		 * wg_path doesn't include them, and hence *wgp is NULL then. */
		if ((NULL == *wgp) || ((*wgp)->sent_wordidx != i))
		{
			assert((NULL == cdj[i]) && lkg->sent->word[i].optional,
			   "A matching disjunct found for a skipped optional word");
			remap[i] = -1;
			continue;
		}

#if USE_SAT_SOLVER
		Disjunct *cdtmp = cdj[j];
#endif
		cdj[j] = cdj[i];
#if USE_SAT_SOLVER
		cdj[i] = cdtmp; /* The SAT parser frees chosen_disjuncts elements. */
#endif
		remap[i] = j;
		j++;
		wgp++;
	}
	if (lkg->num_words != j)
	{
		/* Unused memory not freed - all of it will be freed in free_linkages(). */
		lkg->num_words = j;
		remap_linkages(lkg, remap); /* Update lkg->link_array and lkg->num_links. */
	}
}
#undef D_REE

/**
 * This takes the Wordgraph path array and uses it to
 * compute the chosen_words array.  "I" suffixes are eliminated.
 *
 * Input:
 *
 * chosen_disjuncts[]
 *   This is an array pointers to disjuncts, one for each word, that is
 *   computed by extract_links().  It represents the chosen disjuncts
 *   for the current linkage. It is used here to compute the chosen_words[].
 *
 * wg_path[]
 *    A pointer to a NULL-terminated array of pointers to Wordgraph words.
 *    It corresponds 1-1 to the chosen_disjuncts array in Linkage structure.
 *
 *    FIXME Sometimes the word strings are taken from chosen_disjuncts,
 *    and sometimes from wordgraph subwords.
 *
 * ---
 *
 * Output:
 *
 * chosen_words[]
 *    A pointer to an array of pointers to strings.  These are the words
 *    to be displayed when printing the solution, the links, etc.
 *    Computed as a function of chosen_disjuncts[]. This differs from
 *    sentence.word[].alternatives because it contains the subscripts.  It
 *    differs from chosen_disjunct[].string in that the idiom symbols have
 *    been removed.  Furthermore, several chosen_disjuncts[].word_string
 *    elements may be combined into one chosen_words[] element if
 *    opts->display_morphology==0 or that they where linkage null-words
 *    that are morphemes of the same original word (i.e. subwords of an
 *    unsplit_word which are marked as morphemes).
 *
 * wg_path_display[]
 *    It is constructed to correspond 1-1 to chosen_words.
 *
 * chosen_disjuncts
 *    Elements are discarded to match it to chosen_words, so functions like
 *    linkage_get_disjunct_str(linkage, w) will be able to return info that is
 *    attributed to chosen_words[w].
 *
 * link_array[]
 *   This is an array of links.  These links define the current linkage.
 *   It is computed by extract_links().  It is used by analyze_linkage().
 *   It is indirectly modified here as follows:
 *     1. To correspond to chosen_words[].
 *     2. Morphology links are removed if opts->display_morphology==0.
 *
 * Note: For historical reasons there is some overlap between the results.
 */
#define D_CCW 8
static void compute_chosen_words(Sentence sent, Linkage linkage,
                                 Parse_Options opts)
{
	WordIdx i;   /* index of chosen_words */
	WordIdx j;
	Disjunct **cdjp = linkage->chosen_disjuncts;
	const char **chosen_words = alloca(linkage->num_words * sizeof(*chosen_words));
	int *remap = alloca(linkage->num_words * sizeof(*remap));
	bool *show_word = alloca(linkage->num_words * sizeof(*show_word));
	bool display_morphology = opts->display_morphology;
	const char infix_mark = INFIX_MARK(sent->dict->affix_table);

	Gword **lwg_path = linkage->wg_path;
	Gword **n_lwg_path = NULL; /* new Wordgraph path, to match chosen_words */

	Gword **nullblock_start = NULL; /* start of a null block, to be put in [] */
	size_t nbsize = 0;              /* number of word in a null block */
	Gword *sentence_word;

	memset(show_word, 0, linkage->num_words * sizeof(*show_word));

	if (verbosity_level(D_CCW))
		print_lwg_path(lwg_path, "Linkage");

	/* If morphology printing is being suppressed, then all links
	 * connecting morphemes will be discarded. */
	if (HIDE_MORPHO)
	{
		/* Discard morphology links. */
		for (i=0; i<linkage->num_links; i++)
		{
			Link * lnk = &linkage->link_array[i];

			if (is_morphology_link(lnk->link_name))
			{
				/* Mark link for discarding. */
				lnk->link_name = NULL;
			}
			else
			{
				/* Mark word for not discarding. */
				show_word[lnk->rw] = true;
				show_word[lnk->lw] = true;
			}
		}
	}

	for (i = 0; i < linkage->num_words; i++)
	{
		Disjunct *cdj = cdjp[i];
		Gword *w;              /* current word */
		const Gword *nw;       /* next word (NULL if none) */
		Gword **wgp;           /* wordgraph_path traversing pointer */

		const char *t = NULL;  /* current word string */
		bool at_nullblock_end; /* current word is at end of a nullblock */
		bool join_alt = false; /* morpheme-join this alternative */
		char *s;
		size_t l;
		size_t m;

		lgdebug(D_CCW, "Loop start, word%zu: cdj %s, path %s\n",
		        i, cdj ? cdj->word_string : "NULL",
		        lwg_path[i] ? lwg_path[i]->subword : "NULL");

		w = lwg_path[i];
		nw = lwg_path[i+1];
		wgp = &lwg_path[i];
		sentence_word = wg_get_sentence_word(sent, w);

		/* FIXME If the original word was capitalized in a capitalizable
		 * position, the displayed null word may be its downcase version. */

		if (NULL == cdj) /* a null word (the chosen disjunct was NULL) */
		{
			chosen_words[i] = NULL;
			nbsize++;
			if (NULL == nullblock_start) /* it starts a new null block */
				nullblock_start = wgp;

			at_nullblock_end = (NULL == nw) ||
				(wg_get_sentence_word(sent, nw->unsplit_word) != sentence_word);

			/* Accumulate null words in this alternative */
			if (!at_nullblock_end && (NULL == cdjp[i+1]) &&
			    ((w->morpheme_type == MT_PUNC) == (nw->morpheme_type == MT_PUNC)))
			{
				lgdebug(D_CCW, "Skipping word%zu cdjp=NULL#%zu, path %s\n",
				        i, nbsize, w->subword);
				chosen_words[i] = NULL;
				continue;
			}

			if (NULL != nullblock_start)
			{
				/* If we are here, this null word is an end of a null block */
				lgdebug(+D_CCW, "Handling %zu null words at %zu: ", nbsize, i);

				if (1 == nbsize)
				{
					/* Case 1: A single null subword. */
					lgdebug(D_CCW, "A single null subword.\n");
					t = join_null_word(sent, wgp, nbsize);

					gwordlist_append(&n_lwg_path, w);
				}
				else
				{
					lgdebug(D_CCW, "Combining null subwords");
					/* Use alternative_id to check for start of alternative. */
					if (((*nullblock_start)->alternative_id == *nullblock_start)
					    && at_nullblock_end)
					{
						/* Case 2: A null unsplit_word (all-nulls alternative).*/
						lgdebug(D_CCW, " (null alternative)\n");
						t = sentence_word->subword;

						gwordlist_append(&n_lwg_path, sentence_word);
					}
					else
					{
						/* Case 3: Join together >=2 null morphemes. */
						Gword *wgnull;

						lgdebug(D_CCW, " (null partial word)\n");
						wgnull = wordgraph_null_join(sent, wgp-nbsize+1, wgp);
						gwordlist_append(&n_lwg_path, wgnull);
						t = wgnull->subword;
					}
				}

				nullblock_start = NULL;
				nbsize = 0;
				show_word[i] = true;

				if (MT_WALL != w->morpheme_type)
				{
					/* Put brackets around the null word. */
					l = strlen(t) + 2;
					s = (char *) alloca(l+1);
					s[0] = NULLWORD_START;
					strcpy(&s[1], t);
					s[l-1] = NULLWORD_END;
					s[l] = '\0';
					t = string_set_add(s, sent->string_set);
					lgdebug(D_CCW, " %s\n", t);
					/* Null words have no links, so take care not to drop them. */
				}
			}
		}
		else
		{
			/* This word has a linkage. */
			/* TODO: Suppress "virtual-morphemes", currently the dictcap ones. */

			t = cdj->word_string;
			/* Print the subscript, as in "dog.n" as opposed to "dog". */

			if (0)
			{
				/* TODO */
			}
			else
			{
				/* Get rid of those ugly ".I" */
				if (is_idiom_word(t))
				{
					s = strdupa(t);
					char *sm = (char *)get_word_subscript(s);
					UNREACHABLE(NULL == sm); /* We know it has a subscript. */
					*sm = '\0';
					t = string_set_add(s, sent->string_set);
				}
				else if (HIDE_MORPHO)
				{
					/* Concatenate the word morphemes together into one word.
					 * Concatenate their subscripts into one subscript.
					 * Use subscript separator SUBSCRIPT_SEP.
					 * XXX Check whether we can encounter an idiom word here.
					 * FIXME Combining contracted words is not handled yet, because
					 * combining morphemes which have non-LL links to other words is
					 * not yet implemented.
					 * FIXME Move to a separate function. */
					Gword **wgaltp;
					size_t join_len = 0;
					size_t mcnt = 0;

					/* If the alternative contains morpheme subwords, mark it
					 * for joining... */

					const Gword *unsplit_word = w->unsplit_word;
					for (wgaltp = wgp, j = i; NULL != *wgaltp; wgaltp++, j++)
					{

						if ((*wgaltp)->unsplit_word != unsplit_word) break;
						if (MT_INFRASTRUCTURE ==
						    (*wgaltp)->unsplit_word->morpheme_type) break;

						mcnt++;

						if (NULL == cdjp[j])
						{
							/* ... but not if it contains a null word */
							join_alt = false;
							break;
						}
						join_len += strlen(cdjp[j]->word_string) + 1;
						if ((*wgaltp)->morpheme_type & IS_REG_MORPHEME)
							join_alt = true;
					}

					if (join_alt)
					{
						/* Join it in two steps: 1. Base words. 2. Subscripts.
						 * FIXME? Can be done in one step (more efficient but maybe
						 * less clear).
						 * Put SUBSCRIPT_SEP between the subscripts.
						 * XXX No 1-1 correspondence between the hidden base words
						 * and the subscripts after the join, in case there are base
						 * words with and without subscripts. */

						const char subscript_sep_str[] = { SUBSCRIPT_SEP, '\0'};
						char *join = alloca(join_len + 1);
						join[0] = '\0';

						/* 1. Join base words. (Could just use the unsplit_word.) */
						for (wgaltp = wgp, m = 0; m < mcnt; wgaltp++, m++)
						{
							add_morpheme_unmarked(sent, join, cdjp[i+m]->word_string,
							                      (*wgaltp)->morpheme_type);
						}

						strcat(join, subscript_mark_str()); /* tentative */

						/* 2. Join subscripts. */
						for (wgaltp = wgp, m = 0; m < mcnt; wgaltp++, m++)
						{
							Gword *wg_placeholder;

							/* Cannot NULLify the word - we may have links to it. */
							if (m != mcnt-1)
							{
								chosen_words[i+m] = "";

								if (show_word[i+m])
								{
									/* We have undiscardable links to this word.
									 * Add a display wordgraph placeholder so
									 * get_word_*() will work and return the original
									 * morpheme positions. */
									wg_placeholder = wordgraph_link_placeholder(sent, *wgaltp);
									gwordlist_append(&n_lwg_path, wg_placeholder);
								}
							}

							const char *sm =
								get_word_subscript(cdjp[i+m]->word_string);

							if (NULL != sm)
							{
								/* Supposing stem subscript is .=x (x optional) */
								if (MT_STEM == (*wgaltp)->morpheme_type)
								{
									sm += 1 + STEM_MARK_L; /* sm+strlen(".=") */
									if ('\0' == *sm) sm = NULL;
#if 0
									if ((cnt-1) == m)
									{
										/* Support a prefix-stem combination. In that case
										 * we have just nullified the combined word, so we
										 * need to move it to the position of the prefix.
										 * FIXME: May still not be good enough. */
										move_combined_word = i+m-1;

										/* And the later chosen_word assignment should be:
										 * chosen_words[-1 != move_combined_word ?
										 *    move_combined_word : i] = t;
										 */
									}
									else
									{
										move_combined_word = -1;
									}
#endif
								}
							}
							if (NULL != sm)
							{
								strcat(join, sm+1);
								strcat(join, subscript_sep_str);
							}
						}

						/* Remove an extra mark, if any */
						join_len = strlen(join);
						if ((SUBSCRIPT_SEP == join[join_len-1]) ||
							 (SUBSCRIPT_MARK == join[join_len-1]))
							join[join_len-1] = '\0';

						gwordlist_append(&n_lwg_path, w->unsplit_word);
						t = string_set_add(join, sent->string_set);

						i += mcnt-1;
					}
				}
			}

			if (!join_alt) gwordlist_append(&n_lwg_path, *wgp);

			/*
			 * Add guess marks in [] square brackets, if needed, at the
			 * end of the base word. Convert the badly-printing
			 * SUBSCRIPT_MARK (hex 03 or ^C) into a period.
			 */
			if (t)
			{

				s = strdupa(t);
				char *sm = get_word_subscript(s);
				if (sm) *sm = SUBSCRIPT_DOT;

				if ((!(w->status & WS_GUESS) && (w->status & WS_INDICT))
				    || !DISPLAY_GUESS_MARKS)
				{
					t = string_set_add(s, sent->string_set);
				}
				else
				{
					const char *regex_name = w->regex_name;
					/* 4 = 1(null) + 1(guess_mark) + 2 (sizeof "[]") */
					int baselen = NULL == sm ? strlen(t) : (size_t)(sm-s);
					char guess_mark = 0;

					switch (w->status & WS_GUESS)
					{
						case WS_SPELL:
							guess_mark = GM_SPELL;
							break;
						case WS_RUNON:
							guess_mark = GM_RUNON;
							break;
						case WS_REGEX:
							guess_mark = GM_REGEX;
							break;
						case 0:
							guess_mark = GM_UNKNOWN;
							break;
						default:
							assert(0, "Missing 'case: %2x'", w->status & WS_GUESS);
					}

					/* In the case of display_morphology==0, the guess indication of
					 * the last subword is used as the guess indication of the whole
					 * word.
					 * FIXME? The guess indications of other subwords are ignored in
					 * this mode. This implies that if a first or middle subword has
					 * a guess indication but the last subword doesn't have, no guess
					 * indication would be shown at all. */

					if ((NULL == regex_name) || !display_morphology) regex_name = "";
					s = alloca(strlen(t) + strlen(regex_name) + 4);
					strncpy(s, t, baselen);
					s[baselen] = '[';
					s[baselen + 1] = guess_mark;
					strcpy(s + baselen + 2, regex_name);
					strcat(s, "]");
					if (NULL != sm) strcat(s, sm);
					t = string_set_add(s, sent->string_set);
				}
			}
		}

		assert(t != NULL, "Word %zu: NULL", i);
		chosen_words[i] = t;
	}

	/* Conditional test removal of quotation marks and the "capdict" tokens,
	 * to facilitate using diff on sentence batch runs. */
	if (test_enabled("removeZZZ"))
	{
		if (sent->dict->zzz_connector)
		{
			for (i=0; i<linkage->num_links; i++)
			{
				Link *lnk = &(linkage->link_array[i]);

				if (0 == strcmp(sent->dict->zzz_connector, lnk->link_name))
					chosen_words[lnk->rw] = NULL;
			}
		}
	}

	/* We alloc a little more than needed, but so what... */
	linkage->word = (const char **) exalloc(linkage->num_words*sizeof(char *));

	/* Copy over the chosen words, dropping the discarded words.
	 * However, don't discard existing words (chosen_words[i][0]).
	 * Note that if a word only has morphology links and is not combined with
	 * another word, then it will get displayed with no links at all (e.g.
	 * when explicitly specifying root and suffix for debug: root.= =suf */
	Disjunct **cdj = linkage->chosen_disjuncts;
	for (i=0, j=0; i<linkage->num_words; ++i)
	{
		if (chosen_words[i] &&
		    (chosen_words[i][0] || (!HIDE_MORPHO || show_word[i])))
		{
			//const char *cwtmp = linkage->word[j];
			linkage->word[j] = chosen_words[i];
			//chosen_words[i] = cwtmp;

#if USE_SAT_SOLVER
			Disjunct *cdtmp = cdj[j];
#endif
			cdj[j] = cdj[i];
#if USE_SAT_SOLVER
			cdj[i] = cdtmp; /* The SAT parser frees chosen_disjuncts elements. */
#endif

			remap[i] = j;
			j++;
		}
		else
		{
			remap[i] = -1;
		}
	}
	linkage->num_words = j;

	remap_linkages(linkage, remap); /* Update linkage->link_array / num_links. */

	linkage->wg_path_display = n_lwg_path;

	if (verbosity_level(D_CCW))
		print_lwg_path(n_lwg_path, "Display");
}
#undef D_CCW

#define D_CGW 5
void compute_generated_words(Sentence sent, Linkage linkage)
{
	Disjunct **cdjp = linkage->chosen_disjuncts;

	linkage->word = malloc(linkage->num_words * sizeof(char *));

	for (WordIdx i = 0; i < linkage->num_words; i++)
	{
		assert(cdjp[i] != NULL, "NULL disjunct in generated sentence");
		const char *word;

		Disjunct *cdj = cdjp[i];

		if (cdj->is_category == 0)
		{
			word = cdj->word_string;
		}
		else
		{
			assert(cdj->num_categories > 0, "zero categories in disjunct");
			word = linkage_get_disjunct_str(linkage, i);
			size_t len = strlen(word) + sizeof("<>");
			char *disjunct_string = alloca(len);
			snprintf(disjunct_string, len, "<%s>", word);
			word = string_set_add(disjunct_string, sent->string_set);
		}

		linkage->word[i] = word;
	}
}
#undef D_CGW

Linkage linkage_create(LinkageIdx k, Sentence sent, Parse_Options opts)
{
	Linkage linkage;

#if USE_SAT_SOLVER
	if (opts->use_sat_solver)
	{
		linkage = sat_create_linkage(k, sent, opts);
		if (!linkage) return NULL;
	}
	else
#endif
	{
		/* Cannot create a Linkage for a discarded linkage. */
		if (sent->num_linkages_post_processed <= k) return NULL;
		linkage = &sent->lnkages[k];
	}

	/* Perform remaining initialization we haven't done yet...*/
	if (!IS_GENERATION(sent->dict))
		compute_chosen_words(sent, linkage, opts);

	linkage->is_sent_long = (linkage->num_words >= opts->twopass_length);

	return linkage;
}

void linkage_delete(Linkage linkage)
{
	/* Currently a no-op */
}

size_t linkage_get_num_words(const Linkage linkage)
{
	if (!linkage) return 0;
	return linkage->num_words;
}

size_t linkage_get_num_links(const Linkage linkage)
{
	if (!linkage) return 0;
	return linkage->num_links;
}

static inline bool verify_link_index(const Linkage linkage, LinkIdx index)
{
	if (!linkage) return false;
	if (index >= linkage->num_links) return false;
	return true;
}

int linkage_get_link_length(const Linkage linkage, LinkIdx index)
{
	Link *link;
	if (!verify_link_index(linkage, index)) return -1;
	link = &(linkage->link_array[index]);
	return link->rw - link->lw;
}

WordIdx linkage_get_link_lword(const Linkage linkage, LinkIdx index)
{
	if (!verify_link_index(linkage, index)) return SIZE_MAX;
	return linkage->link_array[index].lw;
}

WordIdx linkage_get_link_rword(const Linkage linkage, LinkIdx index)
{
	if (!verify_link_index(linkage, index)) return SIZE_MAX;
	return linkage->link_array[index].rw;
}

const char * linkage_get_link_label(const Linkage linkage, LinkIdx index)
{
	if (!verify_link_index(linkage, index)) return NULL;
	return linkage->link_array[index].link_name;
}

const char * linkage_get_link_llabel(const Linkage linkage, LinkIdx index)
{
	if (!verify_link_index(linkage, index)) return NULL;
	return connector_string(linkage->link_array[index].lc);
}

const char * linkage_get_link_rlabel(const Linkage linkage, LinkIdx index)
{
	if (!verify_link_index(linkage, index)) return NULL;
	return connector_string(linkage->link_array[index].rc);
}

const char ** linkage_get_words(const Linkage linkage)
{
	return linkage->word;
}

const char * linkage_get_disjunct_str(const Linkage linkage, WordIdx w)
{
	if (NULL == linkage) return "";
	if (linkage->num_words <= w) return NULL; /* bounds-check */

	/* dj will be null if the word wasn't used in the parse. */
	Disjunct *dj = linkage->chosen_disjuncts[w];
	if (NULL == dj) return "";

	if (NULL == linkage->disjunct_list_str)
		lg_compute_disjunct_strings(linkage);

	return linkage->disjunct_list_str[w];
}

float linkage_get_disjunct_cost(const Linkage linkage, WordIdx w)
{
	Disjunct *dj;

	if (linkage->num_words <= w) return 0.0; /* bounds-check */

	dj = linkage->chosen_disjuncts[w];

	/* dj may be null, if the word didn't participate in the parse. */
	if (NULL == dj)
		return 0.0;

	if (dj->is_category)
		return dj->category[0].cost;

	return dj->cost;
}

const char * linkage_get_word(const Linkage linkage, WordIdx w)
{
	if (!linkage) return NULL;
	if (linkage->num_words <= w) return NULL; /* bounds-check */
	return linkage->word[w];
}

int linkage_unused_word_cost(const Linkage linkage)
{
	/* The sat solver (currently) fails to fill in info */
	if (!linkage) return 0;
	return linkage->lifo.unused_word_cost;
}

float linkage_disjunct_cost(const Linkage linkage)
{
	/* The sat solver (currently) fails to fill in info */
	if (!linkage) return 0.0;
	return linkage->lifo.disjunct_cost;
}

int linkage_link_cost(const Linkage linkage)
{
	/* The sat solver (currently) fails to fill in info */
	if (!linkage) return 0;
	return linkage->lifo.link_cost;
}

/* =========== Get word sentence positions ============================== */

WordIdx linkage_get_word_byte_start(const Linkage linkage, WordIdx w)
{
	if (linkage->num_words <= w) return (WordIdx)-1; /* bounds-check */
	return linkage->wg_path_display[w]->start - linkage->sent->orig_sentence;
}

WordIdx linkage_get_word_byte_end(const Linkage linkage, WordIdx w)
{
	if (linkage->num_words <= w) return (WordIdx)-1; /* bounds-check */
	return linkage->wg_path_display[w]->end - linkage->sent->orig_sentence;
}

/* The character position is computed in a straightforward way, which may
 * not be efficient if more than one position is needed. If needed, it can
 * be changed to use caching of already-calculated positions. */

WordIdx linkage_get_word_char_start(const Linkage linkage, WordIdx w)
{
	if (linkage->num_words <= w) return (WordIdx)-1; /* bounds-check */
	int pos = (int)(linkage->wg_path_display[w]->start - linkage->sent->orig_sentence);
	char *sentchunk = strndupa(linkage->sent->orig_sentence, pos);
	return utf8_strlen(sentchunk);
}

WordIdx linkage_get_word_char_end(const Linkage linkage, WordIdx w)
{
	if (linkage->num_words <= w) return (WordIdx)-1; /* bounds-check */
	int pos = (int)(linkage->wg_path_display[w]->end - linkage->sent->orig_sentence);
	char *sentchunk = strndupa(linkage->sent->orig_sentence, pos);
	return utf8_strlen(sentchunk);
}

const Category_cost *linkage_get_categories(const Linkage linkage, WordIdx w)
{
	if (NULL == linkage) return NULL;
	if (linkage->num_words <= w) return NULL; /* bounds-check */

	Disjunct *dj = linkage->chosen_disjuncts[w];
	if (dj->is_category == 0) return NULL;
	return dj->category;
}