############################################################################
#
#	File:     xcodes.icn
#
#	Subject:  Procedures to save and restore Icon data
#
#	Author:   Bob Alexander
#
#	Date:     November 19, 1997
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#	Contributor:  Ralph E. Griswold
#
############################################################################
#
#  Note:  This version handles the encoding of records using canonical
#  names:  record0, record1, ... . This allows programs to decode files
#  by providing declarations for these names when the original declarations
#  are not available.  This version also provides for procedures and
#  files present in the encoded file that are not in the decoding program.
#
#  This version should be merged with the ordinary version.
#
#  Description
#  -----------
#
#     These procedures provide a way of storing Icon values in files
#  and retrieving them.  The procedure xencode(x,f) stores x in file f
#  such that it can be converted back to x by xdecode(f).  These
#  procedures handle several kinds of values, including structures of
#  arbitrary complexity and even loops.  The following sequence will
#  output x and recreate it as y:
#
#	f := open("xstore","w")
#	xencode(x,f)
#	close(f)
#	f := open("xstore")
#	y := xdecode(f)
#	close(f)
#
#  For "scalar" types -- null, integer, real, cset, and string, the
#  above sequence will result in the relationship
#
#	x === y
#
#     For structured types -- list, set, table, and record types --
#  y is, for course, not identical to x, but it has the same "shape" and
#  its elements bear the same relation to the original as if they were
#  encoded and decoded individually.
#
#     Files, co-expressions, and windows cannot generally be restored in any
#  way that makes much sense.  These objects are restored as empty lists so
#  that (1) they will be unique objects and (2) will likely generate
#  run-time errors if they are (probably erroneously) used in
#  computation.  However, the special files &input, &output, and &errout are
#  restored.
#
#     Not much can be done with functions and procedures, except to preserve
#  type and identification.
#
#     The encoding of strings and csets handles all characters in a way
#  that it is safe to write the encoding to a file and read it back.
#
#     xdecode() fails if given a file that is not in xcode format or it
#  the encoded file contains a record for which there is no declaration
#  in the program in which the decoding is done.  Of course, if a record
#  is declared differently in the encoding and decoding programs, the
#  decoding may be bogus.
#
#     xencoden() and xdecoden() perform the same operations, except
#  xencoden() and xdecoden() take the name of a file, not a file.
#
#     xencodet() and xdecodet() are like xencode() and xdecode()
#  except that the trailing argument is a type name.  If the encoded
#  decoded value is not of that type, they fail.  xencodet() does
#  not take an opt argument.
#
############################################################################
#
#  Complete calling sequences
#  --------------------------
#
#	xencode(x, f, p) # returns f
#
#	where
#
#		x is the object to encode
#
#		f is the file to write (default &output)
#
#		p is a procedure that writes a line on f using the
#		  same interface as write() (the first parameter is
#		  always a the value passed as "file") (default: write)
#
#
#	xencode(f, p) # returns the restored object
#
#	where
#
#		f is the file to read (default &input)
#
#		p is a procedure that reads a line from f using the
#		  same interface as read() (the parameter is
#		  always a the value passed as "file") (default: read)
#
#
#  The "p" parameter is not normally used for storage in text files, but
#  it provides the flexibility to store the data in other ways, such as
#  a string in memory.  If "p" is provided, then "f" can be any
#  arbitrary data object -- it need not be a file.
#
#  For example, to "write" x to an Icon string:
#
#	record StringFile(s)
#
#	procedure main()
#	   ...
#	   encodeString := xencode(x,StringFile(""),WriteString).s
#	   ...
#	end
#
#	procedure WriteString(f,s[])
#	  every f.s ||:= !s
#	  f.s ||:= "\n"
#	  return
#	end
#
############################################################################
#
#  Notes on the encoding
#  ---------------------
#
#     Values are encoded as a sequence of one or more lines written to
#  a plain text file.  The first or only line of a value begins with a
#  single character that unambiguously indicates its type.  The
#  remainder of the line, for some types, contains additional value
#  information.  Then, for some types, additional lines follow
#  consisting of additional object encodings that further specify the
#  object.  The null value is a special case consisting of an empty
#  line.
#
#     Each object other than &null is assigned an integer tag as it is
#  encoded.  The tag is not, however, written to the output file.  On
#  input, tags are assigned in the same order as objects are decoded, so
#  each restored object is associated with the same integer tag as it
#  was when being written.  In encoding, any recurrence of an object is
#  represented by the original object's tag.  Tag references are
#  represented as integers, and are easily recognized since no object's
#  representation begins with a digit.
#
#     Where a structure contains elements, the encodings of the
#  elements follow the structure's specification on following lines.
#  Note that the form of the encoding contains the information needed to
#  separate consecutive elements.
#
#     Here are some examples of values and their encodings:
#
#       x                     encode(x)
#  -------------------------------------------------------
#
#       1                     N1
#       2.0                   N2.0
#       &null                 
#       "\377"                "\377"
#       '\376\377'            '\376\377'
#       procedure main        p
#                             "main"
#       co-expression #1 (0)  C
#       []                    L
#                             N0
#       set()                 "S"
#                             N0
#       table("a")            T
#                             N0
#                             "a"
#       ["hi","there"]        L
#                             N2
#                             "hi"
#                             "there"
#
#  A loop is illustrated by
#
#       L2 := []
#       put(L2,L2)
#
#  for which
#
#       x                     encode(x)
#  -------------------------------------------------------
#
#       L2                    L
#                             N1
#                             2
#
#  The "2" on the third line is a tag referring to the list L2.  The tag
#  ordering specifies that an object is tagged *after* its describing
#  objects, thus the list L2 has the tag 2 (the integer 1 has tag 1).
#
#     Of course, you don't have to know all this to use xencode and
#  xdecode.
#
############################################################################
#
#  Links:  escape
#
############################################################################
#
#  See also:  codeobj.icn
#
############################################################################

link escape

record xcode_rec(file,ioProc,done,nextTag)

procedure xencode(x,file,writeProc)	#: write structure to file

   /file := &output
   return xencode_1(
      xcode_rec(
	 file,
	 (\writeProc | write) \ 1,
	 table(),
	 0),
      x)
end

procedure xencode_1(data,x)
   local tp,wr,f,im
   wr := data.ioProc
   f := data.file
   #
   #  Special case for &null.
   #
   if /x then {
      wr(f)
      return f
      }
   #
   #  If this object has already been output, just write its tag.
   #
   if tp := \data.done[\x] then {
      wr(f,tp)
      return f
      }
   #
   #  Check to see if it's a "distinguished" that is represented by
   #  a keyword (special files and csets).  If so, just use the keyword
   #  in the output.
   #
   im := image(x)
   if match("integer(", im) then im := string(x)
   else if match("&",im) then {
      wr(f,im)
      data.done[x] := data.nextTag +:= 1
      return f
      }
   #
   #  Determine the type and handle accordingly.
   #
   tp := case type(x) of {
     "cset" | "string": ""
     "file" | "window": "f"
     "integer" | "real": "N"
     "co-expression": "C"
     "procedure": "p"
     "list": "L"
     "set": "S"
     "table": "T"
     default: "R"
   }
   case tp of {
      #
      #  String, cset, or numeric outputs its string followed by its
      #  image.
      #
      "" | "N": wr(f,tp,im)
      #
      #  Procedure writes "p" followed (on subsequent line) by its name
      #  as a string object.
      #
      "p": {
	 wr(f,tp)
	 im ? {
	    while tab(find(" ") + 1)
	    xencode_1(data,tab(0))
	    }
	 }
      #
      #  Co-expression, or file just outputs its letter.
      #
      !"CEf": wr(f,tp)
      #
      #  Structured type outputs its letter followed (on subsequent
      #  lines) by additional data.  A record writes its type as a
      #  string object; other type writes its size as an integer object.
      #  Structure elements follow on subsequent lines (alternating keys
      #  and values for tables).
      #
      default: {
	 wr(f,tp)
	 case tp of {
	    !"LST": {
	       im ? {
		  tab(find("(") + 1)
		  xencode_1(data,integer(tab(-1)))
		  }
	       if tp == "T" then xencode_1(data,x[[]])
	       }
	    default: xencode_1(data, "record" || *x)	# record -- fake it
	    }
	 #
	 #  Create the tag.  It's important that the tag is assigned
	 #  *after* other other objects that describe this object (e.g.
	 #  the length of a list) are output (and tagged), but *before*
	 #  the structure elements; otherwise decoding would be
	 #  difficult.
	 #
	 data.done[x] := data.nextTag +:= 1
	 #
	 #  Output the elements of the structure.
	 #
	 every xencode_1(data,
	       !case tp of {"S": sort(x); "T": sort(x,3); default: x})
	 }
      }
   #
   #  Tag the object if it's not already tagged.
   #
   /data.done[x] := data.nextTag +:= 1
   return f
end

procedure xdecode(file,readProc)	#: read structure from file

   /file := &input

   return xdecode_1(
      xcode_rec(
	 file,
	 (\readProc | read) \ 1,
	 []))
end

#  This procedure fails if it encounters bad data

procedure xdecode_1(data)
   local x,tp,sz, i, s
   data.ioProc(data.file) ? {
      if any(&digits) then {
	 #
	 #  It's a tag -- return its value from the object table.
	 #
	 return data.done[tab(0)]
	 }
      if tp := move(1) then {
	 x := case tp of {
	    "N": numeric(tab(0))
	    "\"": escape(tab(-1))
	    "'": cset(escape(tab(-1)))
	    "p": proc(xdecode_1(data) | main)
	    "L": list(xdecode_1(data)) | stop("bad list")
	    "S": {sz := xdecode_1(data) | stop("bad set"); set()}
	    "T": {sz := xdecode_1(data) | stop("bad table"); table(xdecode_1(data)) | stop("bad table")}
	    "R": proc(s := xdecode_1(data))() | stop("*** bad record: ", image(s))
	    "&": case tab(0) of {
	       #
	       #  Special csets.
	       #
	       "cset":		&cset
	       "ascii":		&ascii
	       "digits":	&digits
	       "letters":	&letters
	       "lcase":		&lcase
	       "ucase":		&ucase
	       #
	       #  Special files.
	       #
	       "input":		&input
	       "output":	&output
	       "errout":	&errout
	       default:		[] # so it won't crash if new keywords arise
	       }
            "f":  &input	# to allow decoding
            "C":  &main		# to allow decoding
	    default: stop("unknown type")
	    }
	 put(data.done,x)
	 case tp of {
	    !"LR": every i := 1 to *x do
              x[i] := xdecode_1(data) | stop("bad list or record")
	    "T": every 1 to sz do
               insert(x,xdecode_1(data),xdecode_1(data)) | fail
	    "S": every 1 to sz do
               insert(x,xdecode_1(data)) | fail
	    }
	 return x
	 }
      else return
      }

end

procedure xencoden(x, name, opt)
   local output

   /opt := "w"

   output := open(name, opt) | stop("*** xencoden(): cannot open ", name)
   xencode(x, output)
   close(output)

   return

end

procedure xencodet(x, file, typ)

   if type(x) === typ then return xencode(x, file)
   else fail

end

procedure xdecodet(file, typ)
   local x

   x := xdecode(file)

   if type(x) == typ then return x
   else fail

end

procedure xdecoden(name)
   local input, x

   input := open(name) | stop("*** xdecoden(): cannot open ", name)
   if x := xdecode(input) then {
      close(input)
      return x
      }
   else {
      close(input)
      fail
      }

end
