/*@z33.c:Database Service:OldCrossDb(), NewCrossDb(), SymToNum()@*************/
/*                                                                           */
/*  THE LOUT DOCUMENT FORMATTING SYSTEM (VERSION 3.43)                       */
/*  COPYRIGHT (C) 1991, 2008 Jeffrey H. Kingston                             */
/*                                                                           */
/*  Jeffrey H. Kingston (jeff@it.usyd.edu.au)                                */
/*  School of Information Technologies                                       */
/*  The University of Sydney 2006                                            */
/*  AUSTRALIA                                                                */
/*                                                                           */
/*  This program is free software; you can redistribute it and/or modify     */
/*  it under the terms of the GNU General Public License as published by     */
/*  the Free Software Foundation; either Version 3, or (at your option)      */
/*  any later version.                                                       */
/*                                                                           */
/*  This program is distributed in the hope that it will be useful,          */
/*  but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            */
/*  GNU General Public License for more details.                             */
/*                                                                           */
/*  You should have received a copy of the GNU General Public License        */
/*  along with this program; if not, write to the Free Software              */
/*  Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA   */
/*                                                                           */
/*  FILE:         z33.c                                                      */
/*  MODULE:       Database Service                                           */
/*  EXTERNS:      OldCrossDb, NewCrossDb, DbCreate(), DbInsert(),            */
/*                DbConvert(), DbClose(), DbLoad(), DbRetrieve(),            */
/*                DbRetrieveNext()                                           */
/*                                                                           */
/*****************************************************************************/
#define INIT_DBCHECK_NUM	107
#include "externs.h"


/*****************************************************************************/
/*                                                                           */
/*  DBCHECK_TABLE                                                            */
/*                                                                           */
/*  A symbol table holding all non-galley cross references, basically        */
/*  implementing a function (sym, tag) -> fpos (if any).                     */
/*                                                                           */
/*     dtab_new(newsize)                New empty table, newsize capacity    */
/*     dtab_insert(x, S)                Insert new (sym, tag) pair x into S  */
/*     dtab_retrieve(sym, tag, S)       Retrieve (sym, tag) pair from S      */
/*     dtab_debug(S, fp)                Debug print of table S to file fp    */
/*                                                                           */
/*****************************************************************************/

typedef struct
{ int dbchecktab_size;				/* size of table             */
  int dbchecktab_count;				/* number of objects held    */
  OBJECT dbchecktab_item[1];
} *DBCHECK_TABLE;

#define	dtab_size(S)	(S)->dbchecktab_size
#define	dtab_count(S)	(S)->dbchecktab_count
#define	dtab_item(S, i)	(S)->dbchecktab_item[i]

#define hash(pos, sym, tag, S)						\
{ FULL_CHAR *p = tag;							\
  pos = (unsigned long) sym;						\
  while( *p ) pos += *p++;						\
  pos = pos % dtab_size(S);						\
}

static DBCHECK_TABLE dtab_new(int newsize)
{ DBCHECK_TABLE S;  int i;
  ifdebug(DMA, D, DebugRegisterUsage(MEM_DBCHECK, 1,
    2*sizeof(int) + newsize * sizeof(OBJECT)));
  S = (DBCHECK_TABLE)
	  malloc(2*sizeof(int) + newsize * sizeof(OBJECT));
  if( S == (DBCHECK_TABLE) NULL )
    Error(33, 1, "run out of memory enlarging dbcheck table", FATAL, no_fpos);
  dtab_size(S) = newsize;
  dtab_count(S) = 0;
  for( i = 0;  i < newsize;  i++ )  dtab_item(S, i) = nilobj;
  return S;
} /* end dtab_new */

static void dtab_insert(OBJECT x, DBCHECK_TABLE *S);

static DBCHECK_TABLE dtab_rehash(DBCHECK_TABLE S, int newsize)
{ DBCHECK_TABLE NewS;  int i;  OBJECT link, z;
  NewS = dtab_new(newsize);
  for( i = 0;  i < dtab_size(S);  i++ )
  { if( dtab_item(S, i) != nilobj )
    { OBJECT ent = dtab_item(S, i);
      assert( type(ent) == ACAT, "dtab_rehash: ACAT!" );
      for( link = Down(ent);  link != ent;  link = NextDown(link) )
      { Child(z, link)
          ;
	dtab_insert(z, &NewS);
      }
      DisposeObject(ent);
    }
  }
  ifdebug(DMA, D, DebugRegisterUsage(MEM_DBCHECK, -1,
    -(2*sizeof(int) + dtab_size(S) * sizeof(OBJECT))));
  free(S);
  return NewS;
} /* end dtab_rehash */

static void dtab_insert(OBJECT x, DBCHECK_TABLE *S)
{ unsigned long pos;  OBJECT z, link, y;
  if( dtab_count(*S) == dtab_size(*S) - 1 )	/* one less since 0 unused */
    *S = dtab_rehash(*S, 2*dtab_size(*S));
  dtab_count(*S)++;
  hash(pos, db_checksym(x), string(x), *S);
  if( dtab_item(*S, pos) == nilobj )  New(dtab_item(*S, pos), ACAT);
  z = dtab_item(*S, pos);
  for( link = Down(z);  link != z;  link = NextDown(link) )
  { Child(y, link)
      ;
    if( db_checksym(x) == db_checksym(y) && StringEqual(string(x), string(y)) )
    { assert(FALSE, "Dbcheck: entry inserted twice");
    }
  }
  Link(dtab_item(*S, pos), x);
} /* end dtab_insert */

static OBJECT dtab_retrieve(OBJECT sym, FULL_CHAR *tag, DBCHECK_TABLE S)
{ OBJECT x, link, y;  unsigned long pos;
  hash(pos, sym, tag, S);
  x = dtab_item(S, pos);
  if( x == nilobj )  return nilobj;
  for( link = Down(x);  link != x;  link = NextDown(link) )
  { Child(y, link)
      ;
    if( sym == db_checksym(y) && StringEqual(tag, string(y)) )
      return y;
  }
  return nilobj;
} /* end dtab_retrieve */

#if DEBUG_ON
static void dtab_debug(DBCHECK_TABLE S, FILE *fp)
{ int i;  OBJECT x, link, y;
  fprintf(fp, "  table size: %d;  current number of items: %d%s",
    dtab_size(S), dtab_count(S), STR_NEWLINE);
  for( i = 0;  i < dtab_size(S);  i++ )
  { x = dtab_item(S, i);
    fprintf(fp, "dtab_item(S, %d) =", i);
    if( x == nilobj )
      fprintf(fp, " <nilobj>");
    else if( type(x) != ACAT )
      fprintf(fp, " not ACAT!");
    else for( link = Down(x);  link != x;  link = NextDown(link) )
    { Child(y, link)
        ;
      fprintf(fp, " %s&&%s",
	is_word(type(y)) ? SymName(db_checksym(y)) : AsciiToFull("?"),
	is_word(type(y)) ? string(y) : AsciiToFull("not-WORD!"));
    }
    fprintf(fp, "%s", STR_NEWLINE);
  }
} /* end dtab_debug */
#endif

static DBCHECK_TABLE DbCheckTable;		/* the dbcheck table         */
static BOOLEAN	     DbCheckTableInit;		/* TRUE if table inited	     */
static int extra_seq;


/*****************************************************************************/
/*                                                                           */
/*  OldCrossDb     Database containing cross references from previous run.   */
/*  NewCrossDb     Writable database of cross references from this run.      */
/*                                                                           */
/*****************************************************************************/

OBJECT OldCrossDb, NewCrossDb;


/*****************************************************************************/
/*                                                                           */
/*  void DbInit(void)                                                        */
/*                                                                           */
/*  Initialize this module..                                                 */
/*                                                                           */
/*****************************************************************************/

void DbInit(void)
{
  DbCheckTable = NULL;
  DbCheckTableInit = FALSE;
  OldCrossDb = NewCrossDb = nilobj;
  extra_seq = 0;
}


/*****************************************************************************/
/*                                                                           */
/*  #define SymToNum(db, sym, num, gall)                                     */
/*                                                                           */
/*  Set num to the number used to refer to sym in database db.  If sym is    */
/*  not currently referred to in db, create a new number and record sym.     */
/*  If gall is true, sym is the target of galleys stored in this database.   */
/*  Store in boolean fields db_targ(link) and is_extern_target(sym).         */
/*                                                                           */
/*****************************************************************************/

#define SymToNum(db, sym, num, gall)					\
{ OBJECT link, yy;  int count;						\
  count = 0;								\
  for( link = Down(db);  link != db;  link = NextDown(link) )		\
  { Child(yy, link)							\
      ;									\
    assert(type(yy)==CROSS_SYM || type(yy)==ACAT, "SymToNum: yy!");	\
    if( type(yy) != CROSS_SYM )  continue;				\
    if( symb(yy) == sym )  break;					\
    if( number(link) > count )  count = number(link);			\
  }									\
  if( link == db )							\
  { if( cross_sym(sym) == nilobj )  CrossInit(sym);			\
    Link(db, cross_sym(sym));						\
    link = LastDown(db);						\
    number(link) = count + 1;						\
    db_targ(link) = FALSE;						\
  }									\
  num = number(link);							\
  if( gall )  db_targ(link) = is_extern_target(sym) =			\
				uses_extern_target(sym) = TRUE;		\
} /* end SymToNum */


/*@::NumToSym(), DbCreate()@**************************************************/
/*                                                                           */
/*  #define NumToSym(db, num, sym)                                           */
/*                                                                           */
/*  Set sym to the symbol which is referred to in database db by num.        */
/*                                                                           */
/*****************************************************************************/

#define NumToSym(db, num, sym)						\
{ OBJECT link, y = nilobj;						\
  for( link = Down(db);  link != db;  link = NextDown(link) )		\
  { Child(y, link)							\
      ;									\
    if( type(y) == CROSS_SYM && number(link) == num )  break;		\
  }									\
  assert( link != db, "NumToSym: no sym");				\
  assert( type(y) == CROSS_SYM, "NumToSym: y!" );			\
  sym = symb(y);							\
} /* end NumToSym */


/*****************************************************************************/
/*                                                                           */
/*  OBJECT DbCreate(x)                                                       */
/*                                                                           */
/*  Create a new writable database with name (i.e. file stem) x and file     */
/*  position fpos for error messages.                                        */
/*                                                                           */
/*****************************************************************************/

OBJECT DbCreate(OBJECT x)
{ OBJECT db = x;
  debug1(DBS, DD, "DbCreate(%s)", string(db));
  assert( is_word(type(x)), "DbCreate: !is_word(type(x))" );
  reading(db) = FALSE;  db_filep(db) = null;
  debug1(DBS, DD, "DbCreate returning %s", EchoObject(db));
  return db;
} /* end DbCreate */


/*@::DbInsert()@**************************************************************/
/*                                                                           */
/*  DbInsert(db, gall, sym, tag, tagfpos, seq, dfnum, dlnum, dfpos)          */
/*                                                                           */
/*  Insert a new entry into writable database db.  The primary key of the    */
/*  entry has these three parts:                                             */
/*                                                                           */
/*      gall        TRUE if inserting a galley                               */
/*      sym         The symbol which is the target of this entry             */
/*      tag         The tag of this target (must be a non-null string)       */
/*                                                                           */
/*  tagfpos is the file position that the tag originated from.               */
/*  There is also an auxiliary key, seq, which enforces an ordering on       */
/*  entries with equal primary keys but is not itself ever retrieved.  This  */
/*  ordering is used for sorted galleys.  The value of the entry has the     */
/*  following parts:                                                         */
/*                                                                           */
/*      dfnum       The file containing the object                           */
/*      dfpos       The position of the object in that file                  */
/*      dlnum       The line number of the object in the file                */
/*                                                                           */
/*  If check is TRUE, we need to check whether an entry with this key has    */
/*  been inserted before.  This will never be the case with galley entries.  */
/*                                                                           */
/*****************************************************************************/

void DbInsert(OBJECT db, BOOLEAN gall, OBJECT sym, FULL_CHAR *tag,
FILE_POS *tagfpos, FULL_CHAR *seq, FILE_NUM dfnum, long dfpos, int dlnum,
BOOLEAN check)
{ int symnum;  OBJECT chk;
  FULL_CHAR buff[MAX_BUFF];
  assert( is_word(type(db)), "DbInsert: db!" );
  assert( tag[0] != '\0', "DbInsert: null tag!" );
  assert( seq[0] != '\0', "DbInsert: null seq!" );
  ifdebug(DPP, D, ProfileOn("DbInsert"));
  debug6(DBS, DD, "DbInsert(%s, %s, %s, %s, %s, %s, dlnum, dfpos)",
	string(db), bool_str(gall), SymName(sym), tag, seq,
	dfnum == NO_FILE ? AsciiToFull(".") : FileName(dfnum));
  assert(!reading(db), "DbInsert: insert into reading database");

  /* if required, check that (sym, tag) not already inserted */
  if( check )
  { 
    debug2(DBS, DD, "  checking %s&&%s, DbCheckTable =", SymName(sym), tag);
    if( !DbCheckTableInit )
    { DbCheckTable = dtab_new(INIT_DBCHECK_NUM);
      DbCheckTableInit = TRUE;
    }
    ifdebug(DBS, DD, dtab_debug(DbCheckTable, stderr));
    chk = dtab_retrieve(sym, tag, DbCheckTable);
    if( chk == nilobj )
    { chk = MakeWord(WORD, tag, tagfpos);
      db_checksym(chk) = sym;
      dtab_insert(chk, &DbCheckTable);
    }
    else
    { if( file_num(fpos(chk)) > 0 )
        Error(33, 4, "cross reference %s&&%s used previously, at%s",
          WARN, tagfpos, SymName(sym), tag, EchoFilePos(&fpos(chk)));
      else Error(33, 5, "cross reference %s&&%s used previously",
	  WARN, tagfpos, SymName(sym), tag);
    }
  }

  /* open database index file if not already done */
  if( db_filep(db) == null )
  { if( StringLength(string(db)) + StringLength(NEW_INDEX_SUFFIX) >= MAX_BUFF )
      Error(33, 2, "database file name %s%s is too long",
	FATAL, no_fpos, string(db), NEW_INDEX_SUFFIX);
    StringCopy(buff, string(db));
    StringCat(buff, NEW_INDEX_SUFFIX);
    db_filep(db) = StringFOpen(buff, WRITE_FILE);
    if( db_filep(db) == null )
      Error(33, 3, "cannot write to database file %s", FATAL, &fpos(db), buff);
  }

  /* work out database index file entry and append it to file */
  if( dfnum != NO_FILE )
  { StringCopy(buff, FileName(dfnum));
    StringCopy(&buff[StringLength(buff)-StringLength(DATA_SUFFIX)], STR_EMPTY);
  }
  else StringCopy(buff, AsciiToFull("."));
  SymToNum(db, sym, symnum, gall);
  ifdebug(DBS, DD,
    fprintf(stderr, "  -> %s%d&%s\t%s\t%ld\t%d\t%s%s", gall ? "0" : "", symnum,
      tag, seq, dfpos, dlnum, buff, STR_NEWLINE);
  );
  fprintf(db_filep(db), "%s%d&%s\t%s\t%s\t%ld\t%d\t%s%s", gall ? "0" : "",
    symnum, tag, seq, StringFiveInt(++extra_seq), dfpos, dlnum, buff,
    STR_NEWLINE);

  /* return */
  debug0(DBS, DD, "DbInsert returning.");
  ifdebug(DPP, D, ProfileOff("DbInsert"));
} /* end DbInsert */


/*@::DbConvert(), DbClose()@**************************************************/
/*                                                                           */
/*  DbConvert(db, full_name)                                                 */
/*                                                                           */
/*  Convert database db from writable to readable, then dispose it.          */
/*  full_name is TRUE if symbols are to be known by their full path name.    */
/*                                                                           */
/*****************************************************************************/

void DbConvert(OBJECT db, BOOLEAN full_name)
{ FULL_CHAR oldname[MAX_BUFF+10], newname[MAX_BUFF];
  OBJECT link, y;
  ifdebug(DPP, D, ProfileOn("DbConvert"));
  debug2(DBS, DD, "DbConvert( %ld %s )", (long) db, string(db));
  assert( !reading(db), "DbConvert: reading database");
  StringCopy(newname, string(db));
  StringCat(newname, INDEX_SUFFIX);
  StringCopy(oldname, string(db));
  StringCat(oldname, NEW_INDEX_SUFFIX);
  if( db_filep(db) != null )
  {
    fprintf(db_filep(db), "00 %s %s%s", LOUT_VERSION, "database index file",
      (char *) STR_NEWLINE);
    for( link = Down(db);  link != db;  link = NextDown(link) )
    { Child(y, link)
        ;
      assert( type(y) == CROSS_SYM || type(y) == ACAT, "DbConvert: y!" );
      if( type(y) != CROSS_SYM )  continue;
      fprintf(db_filep(db), "%s %d %s%s",
	db_targ(link) ? "00target" : "00symbol", number(link),
	full_name ? FullSymName(symb(y), AsciiToFull(" ")) : SymName(symb(y)),
	(char *) STR_NEWLINE);
    }
    fclose(db_filep(db));
    debug2(DBS, DD, "  calling SortFile(%s, %s)", oldname, newname);
    SortFile(oldname, newname);
  }
  else StringRemove(newname);
  StringRemove(oldname);
  DeleteNode(db);
  debug0(DBS, DD, "DbConvert returning.");
  ifdebug(DPP, D, ProfileOff("DbConvert"));
} /* end DbConvert */


/*****************************************************************************/
/*                                                                           */
/*  DbClose(db)                                                              */
/*                                                                           */
/*  Close readable database db.                                              */
/*                                                                           */
/*****************************************************************************/

void DbClose(OBJECT db)
{ if( db != nilobj && !in_memory(db) && db_filep(db) != NULL )
  {  fclose(db_filep(db));
     db_filep(db) = NULL;
  }
} /* end DbClose */


/*@::DbLoad()@****************************************************************/
/*                                                                           */
/*  OBJECT DbLoad(stem, fpath, create, symbs, in_mem)                        */
/*                                                                           */
/*  Open for reading the database whose index file name is string(stem).li.  */
/*  This file has not yet been defined; its search path is fpath.  If it     */
/*  will not open and create is true, try creating it from string(stem).ld.  */
/*                                                                           */
/*  symbs is an ACAT of CLOSUREs showing the symbols that the database may   */
/*  contain; or nilobj if the database may contain any symbol.               */
/*                                                                           */
/*  If in_mem is true, this database index is to be kept in internal memory, */
/*  rather than an external file, as a speed optimization.                   */
/*                                                                           */
/*****************************************************************************/

OBJECT DbLoad(OBJECT stem, int fpath, BOOLEAN create, OBJECT symbs,
  BOOLEAN in_mem)
{ FILE *fp;  OBJECT db, t, res, tag, par, sym, link, y;
  int i, lnum, dlnum, num, count, leftp;
  FILE_NUM index_fnum, dfnum;  long dfpos;
  BOOLEAN gall;  FULL_CHAR line[MAX_BUFF], sym_name[MAX_BUFF]; int status;
  ifdebug(DPP, D, ProfileOn("DbLoad"));
  debug3(DBS, DD, "[ DbLoad(%s, %d, %s, -)", string(stem), fpath, bool_str(create));

  /* open or else create index file fp */
  debug0(DFS, D, "  calling DefineFile from DbLoad (1)");
  index_fnum = DefineFile(string(stem), INDEX_SUFFIX, &fpos(stem), INDEX_FILE,
		 fpath);
  fp = OpenFile(index_fnum, create, FALSE);

  /* read first line of database index file, which should have the version */
  if( fp != null )
  { if( ReadOneLine(fp, line, MAX_BUFF) == 0 ||
        !StringBeginsWith(&line[3], LOUT_VERSION) )
    {
      /* out of date, pretend it isn't there at all */
      StringRemove(FileName(index_fnum));
      fp = null;
    }
  }

  if( fp == null && create )
  { db = nilobj;
    debug0(DFS, D, "  calling DefineFile from DbLoad (2)");
    dfnum = DefineFile(string(stem), DATA_SUFFIX, &fpos(stem),
      DATABASE_FILE, fpath);
    dfpos = 0L;  LexPush(dfnum, 0, DATABASE_FILE, 1, FALSE);
    t = LexGetToken();
    dlnum = line_num(fpos(t));
    while( type(t) == LBR )
    { res = Parse(&t, StartSym, FALSE, FALSE);
      if( t != nilobj || type(res) != CLOSURE )
	Error(33, 6, "syntax error in database file %s",
	  FATAL, &fpos(res), FileName(dfnum));
      assert( symbs != nilobj, "DbLoad: create && symbs == nilobj!" );
      if( symbs != nilobj )
      {	for( link = Down(symbs);  link != symbs;  link = NextDown(link) )
	{ Child(y, link)
	    ;
	  if( type(y) == CLOSURE && actual(y) == actual(res) )  break;
	}
	if( link == symbs )
	  Error(33, 7, "%s found in database but not declared in %s line",
	    FATAL, &fpos(res), SymName(actual(res)), KW_DATABASE);
      }
      for( tag = nilobj, link = Down(res); link != res; link = NextDown(link) )
      {	Child(par, link)
          ;
	if( type(par) == PAR && is_tag(actual(par)) && Down(par) != par )
	{ Child(tag, Down(par))
	    ;
	  break;
	}
      }
      if( tag == nilobj )
	Error(33, 8, "database symbol %s has no tag",
	  FATAL, &fpos(res), SymName(actual(res)));
      tag = ReplaceWithTidy(tag, WORD_TIDY);  /* && */
      if( !is_word(type(tag)) )
	Error(33, 9, "database symbol tag is not a simple word",
	  FATAL, &fpos(res));
      if( StringEqual(string(tag), STR_EMPTY) )
	Error(33, 10, "database symbol tag is an empty word", FATAL,&fpos(res));
      if( db == nilobj )
      {	StringCopy(line, FileName(dfnum));
	i = StringLength(line) - StringLength(INDEX_SUFFIX);
	assert( i > 0, "DbLoad: FileName(dfnum) (1)!" );
	StringCopy(&line[i], STR_EMPTY);
	db = DbCreate(MakeWord(WORD, line, &fpos(stem)));
      }
      DbInsert(db, FALSE, actual(res), string(tag), &fpos(tag), STR_ZERO,
	NO_FILE, dfpos, dlnum, TRUE);
      DisposeObject(res);  dfpos = LexNextTokenPos();  t = LexGetToken();
      dlnum = line_num(fpos(t));
    }
    if( type(t) != END )
      Error(33, 11, "%s or end of file expected here", FATAL, &fpos(t), KW_LBR);
    LexPop();
    if( db == nilobj )
    { StringCopy(line, FileName(dfnum));
      i = StringLength(line) - StringLength(INDEX_SUFFIX);
      assert( i > 0, "DbLoad: FileName(dfnum) (2)!" );
      StringCopy(&line[i], STR_EMPTY);
      db = DbCreate(MakeWord(WORD, line, &fpos(stem)));
    }
    DbConvert(db, FALSE);
    if( (fp = OpenFile(index_fnum, FALSE, FALSE)) == null ||
        ReadOneLine(fp, line, MAX_BUFF) == 0 ||
        !StringBeginsWith(&line[3], LOUT_VERSION) )
      Error(33, 12, "cannot open database file %s",
        FATAL, &fpos(db), FileName(index_fnum));
  }

  /* set up database record */
  StringCopy(line, FileName(index_fnum));
  i = StringLength(line) - StringLength(INDEX_SUFFIX);
  assert( i > 0, "DbLoad: FileName(index_fnum)!" );
  StringCopy(&line[i], STR_EMPTY);
  db = MakeWord(WORD, line, &fpos(stem));
  reading(db) = TRUE;
  in_memory(db) = in_mem;
  if( symbs != nilobj )
  { assert( type(symbs) == ACAT, "DbLoad: type(symbs)!" );
    Link(db, symbs);
  }
  if( fp == null )
  { debug1(DBS, DD, "] DbLoad returning (empty) %s", string(db));
    db_filep(db) = null;
    db_lines(db) = (LINE *) NULL;
    ifdebug(DPP, D, ProfileOff("DbLoad"));
    return db;
  }

  /* read header lines of index file, find its symbols */
  leftp = 0;  lnum = 1;
  while( (status = ReadOneLine(fp, line, MAX_BUFF)) != 0 )
  {
    debug1(DBS, D, "ReadOneLine returning \"%s\"", line);
    if( line[0] != '0' || line[1] != '0' )  break;
    lnum++;
    leftp = (int) ftell(fp);
    gall = StringBeginsWith(line, AsciiToFull("00target "));
    sscanf( (char *) line, gall ? "00target %d" : "00symbol %d", &num);
    for( i = 9;  line[i] != CH_SPACE && line[i] != '\0';  i++ );
    if( symbs == nilobj )
    {
      /* any symbols are possible, full path names in index file required */
      count = 0;  sym = StartSym;
      while( line[i] != '\0' )
      {	PushScope(sym, FALSE, FALSE);  count++;
	sscanf( (char *) &line[i+1], "%s", sym_name);
	sym = SearchSym(sym_name, StringLength(sym_name));
	i += StringLength(sym_name) + 1;
      }
      for( i = 1;  i <= count;  i++ )  PopScope();
    }
    else
    {
      /* only symbs symbols possible, full path names not required */
      sym = nilobj;
      sscanf( (char *) &line[i+1], "%s", sym_name);
      for( link = Down(symbs);  link != symbs;  link = NextDown(link) )
      {	Child(y, link)
          ;
	assert( type(y) == CLOSURE, "DbLoad: type(y) != CLOSURE!" );
	if( StringEqual(sym_name, SymName(actual(y))) )
	{ sym = actual(y);
	  break;
	}
      }
    }
    if( sym != nilobj && sym != StartSym )
    { if( cross_sym(sym) == nilobj )  CrossInit(sym);
      Link(db, cross_sym(sym));
      link = LastDown(db);
      number(link) = num;  db_targ(link) = gall;
      if( gall )  is_extern_target(sym) = uses_extern_target(sym) = TRUE;
    }
    else
    { Error(33, 13, "undefined symbol in database file %s (line %d)",
	WARN, &fpos(db), FileName(index_fnum), lnum);
      debug1(DBS, DD, "] DbLoad returning %s (error)", string(db));
      fclose(fp);
      in_memory(db) = FALSE;
      db_filep(db) = null;  /* subsequently treated like an empty database */
      ifdebug(DPP, D, ProfileOff("DbLoad"));
      return db;
    }
  }

  /* if in_memory, go on to read the entire database index into memory */
  if( in_memory(db) )
  {
    if( status == 0 )
      db_lines(db) = 0;
    else
    {
      int len;
      db_lines(db) = ReadLines(fp, FileName(index_fnum), line, &len);
      db_lineslen(db) = len;
      SortLines(db_lines(db), db_lineslen(db));
    }
  }
  else /* external, save leftpos and file pointer */
  { db_filep(db) = fp;
    left_pos(db) = leftp;
  }

  /* return */
  debug1(DBS, DD, "] DbLoad returning %s", string(db));
  ifdebug(DPP, D, ProfileOff("DbLoad"));
  return db;
} /* end DbLoad */


/*@::SearchFile()@************************************************************/
/*                                                                           */
/*  static BOOLEAN SearchFile(fp, left, right, str, line)                    */
/*                                                                           */
/*  File fp is a text file.  left is the beginning of a line, right is the   */
/*  end of a line.   Search the file by binary search for a line beginning   */
/*  with str.  If found, return it in line, else return FALSE.               */
/*                                                                           */
/*  NB if the line endings consist of two characters, the end of the line    */
/*  is at the second character.                                              */
/*                                                                           */
/*****************************************************************************/

static BOOLEAN SearchFile(FILE *fp, int left, int right,
FULL_CHAR *str, FULL_CHAR *line)
{ int l, r, mid, mid_end;  FULL_CHAR buff[MAX_BUFF];  BOOLEAN res;
  int ch;
  ifdebug(DPP, D, ProfileOn("SearchFile"));
  debug3(DBS, D, "SearchFile(fp, %d, %d, %s, line)", left, right, str);

  l = left;  r = right;
  while( l <= r )
  {
    /* loop invt: (l==0 or fp[l-1]==end_of_line) and (fp[r] == end_of_line)  */
    /* and first key >= str lies in the range fp[l..r+1]                     */

    /* find line near middle of the range; mid..mid_end brackets it */
    debug2(DBS, DD, "  start loop: l = %d, r = %d", l, r);
    mid = (l + r)/2;
    fseek(fp, (long) mid, SEEK_SET);
    do { mid++; } while( (ch = getc(fp)) != CH_CR && ch != CH_LF );
    if( ch == CH_CR )
    {
      ch = getc(fp);
      if( ch != CH_LF )
	ungetc(ch, fp);
      else
	mid++;
    }
    else /* ch == CH_LF */
    {
      ch = getc(fp);
      if( ch != CH_CR )
	ungetc(ch, fp);
      else
	mid++;
    }
    if( mid == r + 1 )
    { mid = l;
      fseek(fp, (long) mid, SEEK_SET);
    }
    ReadOneLine(fp, line, MAX_BUFF);
    mid_end = (int) ftell(fp) - 1;
    debug3(DBS, DD, "  mid: %d, mid_end: %d, line: %s", mid, mid_end, line);
    assert( l <= mid,      "SearchFile: l > mid!"        );
    assert( mid < mid_end, "SearchFile: mid >= mid_end!" );
    assert( mid_end <= r,  "SearchFile: mid_end > r!"    );

    /* compare str with this line and prepare next step */
    if( TabbedStringLessEqual(str, line) )
    {
      debug2(DBS, D, "  left after comparing key %s with line %s", str, line);
      r = mid - 1;
    }
    else
    {
      debug2(DBS, D, "  right after comparing key %s with line %s", str, line);
      l = mid_end + 1;
    }
  } /* end while */

  /* now first key >= str lies in fp[l]; compare it with str */
  if( l < right )
  { fseek(fp, (long) l, SEEK_SET);
    ReadOneLine(fp, line, MAX_BUFF);
    sscanf( (char *) line, "%[^\t]", buff);
    res = StringEqual(str, buff);
  }
  else res = FALSE;
  debug1(DBS, D, "SearchFile returning %s", bool_str(res));
  ifdebug(DPP, D, ProfileOff("SearchFile"));
  return res;
} /* end SearchFile */


/*@::SearchLines()@***********************************************************/
/*                                                                           */
/*  static BOOLEAN SearchLines(LINE *lines, int left, int right, str, lnum)  */
/*                                                                           */
/*  Search the sorted array of LINE arrays lines[left..right] for a line     */
/*  beginning with str, and return TRUE if found else FALSE.                 */
/*                                                                           */
/*  If TRUE is returned then the number of the line is in *lnum.             */
/*                                                                           */
/*****************************************************************************/

static BOOLEAN SearchLines(LINE *lines, int left, int right, FULL_CHAR *str,
  int *lnum)
{ int l, r, mid;  FULL_CHAR buff[MAX_BUFF];
  BOOLEAN res;
  debug3(DBS, D, "SearchLines(lines, %d, %d, %s, lnum)", left, right, str);
  if( right < left )
  {
    debug0(DBS, D, "SearchLines returning FALSE (empty lines)");
    return FALSE;
  }
  l = left;
  r = right - 1;
  while( l <= r )
  {
    /* loop invt: first key >= str (if any) lies in the range lines[l..r+1] */
    /* and left <= l <= right and r < right                                 */
    mid = (l + r) / 2;
    debug4(DBS, D, "  [l %d, r %d] examining lines[%d] = %s", l, r, mid,
      lines[mid]);
    if( TabbedStringLessEqual(str, (FULL_CHAR *) lines[mid]) )  r = mid - 1;
    else l = mid + 1;
  }
  sscanf( (char *) lines[l], "%[^\t]", buff);
  if( StringEqual(str, buff) )
  {
    res = TRUE;
    *lnum = l;
    debug1(DBS, D, "SearchLines returning TRUE (lnum %d)", *lnum);
  }
  else
  { res = FALSE;
    debug0(DBS, D, "SearchLines returning FALSE");
  }
  return res;
} /* end SearchLines */


/*@::DbRetrieve()@************************************************************/
/*                                                                           */
/*  BOOLEAN DbRetrieve(db, gall, sym, tag, seq, dfnum, dfpos, dlnum, cont)   */
/*                                                                           */
/*  Retrieve the first entry of database db with the given gall, sym and     */
/*  tag.  Set *seq, *dfnum, *dlnum, *dfpos to the associated value.          */
/*  Set *cont to a private value for passing to DbRetrieveNext.              */
/*                                                                           */
/*****************************************************************************/

BOOLEAN DbRetrieve(OBJECT db, BOOLEAN gall, OBJECT sym, FULL_CHAR *tag,
  FULL_CHAR *seq, FILE_NUM *dfnum, long *dfpos, int *dlnum, long *cont)
{ int symnum, lnum;  FULL_CHAR line[MAX_BUFF], buff[MAX_BUFF];
  ifdebug(DPP, D, ProfileOn("DbRetrieve"));
  debug4(DBS, DD, "DbRetrieve(%s, %s%s&%s)", string(db), gall ? "0" : "",
	SymName(sym), tag);

  /* check OK to proceed */
  if( !reading(db) || db_filep(db) == null )
  { debug0(DBS, DD, "DbRetrieve returning FALSE (empty or not reading)");
    ifdebug(DPP, D, ProfileOff("DbRetrieve"));
    return FALSE;
  }

  /* convert parameters into search key */
  SymToNum(db, sym, symnum, FALSE);
  sprintf( (char *) buff, "%s%d&%s", gall ? "0" : "", symnum, tag);

  if( in_memory(db) )
  {
    /* search internal table, return if not found; set *cont to continuation */
    if( !SearchLines(db_lines(db), 0, db_lineslen(db) - 1, buff, &lnum) )
    { debug0(DBS, DD, "DbRetrieve returning FALSE (key not present)");
      ifdebug(DPP, D, ProfileOff("DbRetrieve"));
      return FALSE;
    }
    sscanf( (char *) db_lines(db)[lnum],
      "%*[^\t]\t%[^\t]\t%*[^\t]\t%ld\t%d\t%[^\n\f]", seq, dfpos, dlnum, buff);
    *cont = lnum+1;
  }
  else
  {
    /* search for key in file, return if not found; set *cont to continuatn */
    fseek(db_filep(db), 0L, SEEK_END);
    if( !SearchFile(db_filep(db), (int) left_pos(db),
	   (int) ftell(db_filep(db)) - 1, buff, line) )
    { debug0(DBS, DD, "DbRetrieve returning FALSE (key not present)");
      ifdebug(DPP, D, ProfileOff("DbRetrieve"));
      return FALSE;
    }
    sscanf( (char *) line,
      "%*[^\t]\t%[^\t]\t%*[^\t]\t%ld\t%d\t%[^\n\f]", seq, dfpos, dlnum, buff);
    *cont = ftell(db_filep(db));
  }

  /* work out file name if . abbreviation used, and possibly define file */
  if( StringEqual(buff, AsciiToFull(".")) )
  { StringCopy(buff, string(db));
  }
  *dfnum = FileNum(buff, DATA_SUFFIX);
  if( *dfnum == NO_FILE )  /* can only occur in cross reference database */
  { debug0(DFS, D, "  calling DefineFile from DbRetrieve");
    *dfnum = DefineFile(buff, DATA_SUFFIX, &fpos(db),
      DATABASE_FILE, SOURCE_PATH);
  }

  /* return */
  debug3(DBS, DD, "DbRetrieve returning TRUE (in %s at %ld, line %d)",
    FileName(*dfnum), *dfpos, *dlnum);
  ifdebug(DPP, D, ProfileOff("DbRetrieve"));
  return TRUE;
} /* end DbRetrieve */


/*@::DbRetrieveNext()@********************************************************/
/*                                                                           */
/*  BOOLEAN DbRetrieveNext(db, gall, sym, tag, seq, dfnum, dfpos,dlnum,cont) */
/*                                                                           */
/*  Retrieve the entry of database db pointed to by *cont.                   */
/*  Set *gall, *sym, *tag, *seq, *dfnum, *dlnum, *dfpos to the value.        */
/*  Reset *cont to the next entry for passing to the next DbRetrieveNext.    */
/*                                                                           */
/*****************************************************************************/

BOOLEAN DbRetrieveNext(OBJECT db, BOOLEAN *gall, OBJECT *sym, FULL_CHAR *tag,
  FULL_CHAR *seq, FILE_NUM *dfnum, long *dfpos, int *dlnum, long *cont)
{ FULL_CHAR line[MAX_BUFF], *cline, fname[MAX_BUFF]; int symnum;
  char format[MAX_FORMAT];
  ifdebug(DPP, D, ProfileOn("DbRetrieveNext"));
  debug2(DBS, DD, "DbRetrieveNext( %s, %ld )", string(db), *cont);
  assert(reading(db), "DbRetrieveNext: not reading");

  /* check OK to proceed */
  if( db_filep(db) == null )
  { debug0(DBS, DD, "DbRetrieveNext returning FALSE (empty database)");
    ifdebug(DPP, D, ProfileOff("DbRetrieveNext"));
    return FALSE;
  }

  sprintf(format, "%%d&%%%d[^\t]\t%%%d[^\t]\t%%*[^\t]\t%%ld\t%%d\t%%%d[^\n\f]", MAX_BUFF-1, MAX_BUFF-1, MAX_BUFF-1);

  if( in_memory(db) )
  {
    /* get next entry from internal database */
    if( *cont >= db_lineslen(db) )
    { debug0(DBS, DD, "DbRetrieveNext returning FALSE (no successor)");
      ifdebug(DPP, D, ProfileOff("DbRetrieveNext"));
      return FALSE;
    }
    cline = (FULL_CHAR *) db_lines(db)[*cont];
    *gall = (cline[0] == '0' ? 1 : 0);
    sscanf((char *)&cline[*gall], format,
      &symnum, tag, seq, dfpos, dlnum, fname);
    *cont = *cont + 1;
  }
  else
  {
    /* use *cont to find position of next entry; advance *cont */
    fseek(db_filep(db), *cont == 0L ? (long) left_pos(db) : *cont, SEEK_SET);
    if( ReadOneLine(db_filep(db), line, MAX_BUFF) == 0 )
    { debug0(DBS, DD, "DbRetrieveNext returning FALSE (no successor)");
      ifdebug(DPP, D, ProfileOff("DbRetrieveNext"));
      return FALSE;
    }
    *gall = (line[0] == '0' ? 1 : 0);
    sscanf((char *)&line[*gall], format,
      &symnum, tag, seq, dfpos, dlnum, fname);
    *cont = ftell(db_filep(db));
  }

  /* work out file name if . abbreviation used, and possibly define file */
  if( StringEqual(fname, AsciiToFull(".")) )
  { StringCopy(fname, string(db));
  }
  *dfnum = FileNum(fname, DATA_SUFFIX);
  if( *dfnum == NO_FILE )  /* can only occur in cross reference database */
  { debug0(DFS, D, "  calling DefineFile from DbRetrieveNext");
    *dfnum = DefineFile(fname, DATA_SUFFIX, &fpos(db),
      DATABASE_FILE, SOURCE_PATH);
  }
  NumToSym(db, symnum, *sym);

  /* return */
  debug3(DBS, DD, "DbRetrieveNext returning TRUE (in %s at %ld, line %d)",
    FileName(*dfnum), *dfpos, *dlnum);
  ifdebug(DPP, D, ProfileOff("DbRetrieveNext"));
  return TRUE;
} /* end DbRetrieveNext */
