{$mode tp}(*$V-,R+,B- *)

Program very_tiny_prolog ;

uses Crt; (* necessary for ClrEol *)


(* Copyright 1986 - MicroExpert Systems
                    Box 430 R.D. 2
                    Nassau, NY 12123       *)


(* VTPROLOG implements the data base searching and pattern matching of
   PROLOG. It is described in "PROLOG from the Bottom Up" in issues
   1 and 2 of AI Expert.
   This program has been tested using Turbo ver 3.01A on an IBM PC. It has
   been run under both DOS 2.1 and Concurrent 4.1 .
   We would be pleased to hear your comments, good or bad, or any applications
   and modifications of the program. Contact us at:
     AI Expert
     CL Publications Inc.
     650 Fifth St.
     Suite 311
     San Francisco, CA 94107
   or on the AI Expert BBS. Our id is BillandBev Thompson. You can also
   contact us on BIX, our id is bbt.
   Bill and Bev Thompson    *)

Const 
   debug      = false ;
   back_space = ^H ;
   tab	      = ^I ;
   eof_mark   = ^Z ;
   esc	      = #27 ;
   quote_char = #39 ;
   left_arrow = #75 ;
   end_key    = #79 ;
   del_line   = ^X ;
   return     = ^M ;
   bell	      = ^G ;

Type 
  counter = 0 .. maxint ;
  string80 = string[80] ;
  string132 = string[132] ;
  string255 = string[255] ;
  text_file = text ;
  char_set = SET Of char ;
  node_type = (cons_node,func,variable,constant,free_node) ;
  node_ptr = ^node ;
  node = Record
    in_use : boolean ;
    Case tag : node_type Of 
      cons_node : (tail_ptr : node_ptr ;
                   head_ptr : node_ptr) ;
      func,
      constant,
      variable  : (string_data : string80) ;
      free_node : (next_free : node_ptr ;
                   block_cnt : counter) ;
  End ;


(* node is the basic allocation unit for lists. The fields are used as
   follows:
    in_use     - in_use = false tells the garbage collector that this node
                 is available for re-use.
    tag        - which kind of node this is.
    cons_node  - cons_nodes consist of two pointers. one to the head (first item)
                 the other to the rest of the list. They are the "glue" which
                 holds the list together. The list (A B C) would be stored as
                   -------         --------          --------
                   | .| . |----->  |  .| . |------> |  .| . |---> NIL
                   --|-----         --|------        --|-----
                     |                |                |
                     V                V                V
                     A                B                C
                 The boxes are the cons nodes, the first part of the box
                 holds the head pointer, then second contains the tail.
    constant   - holds string values, we don't actually use the entire 80
                 characters in most cases.
    variable   - also conatins a string value, these nodes will be treated as
                 PROLOG variables rather than constants.
    free_node  - the garbage collector gathers all unused nodes and puts
                 them on a free list. It also compacts the free space into
                 contiguous blocks. next_free points to the next free block.
                 block_cnt contains a count of the number of contiguous 8 byte free
                 blocks which follow this one.    *)


Var 
  line,saved_line : string132 ;
  token : string80 ;
  source_file : text_file ;
  error_flag,in_comment : boolean ;
  delim_set,text_chars : char_set ;
  data_base, initial_heap, free, saved_list, HeapPtr : node_ptr ;
  total_free : real ;


(* The important globals are:
   source_file  - text file containing PROLOG statements.
   line         - line buffer for reading in the text file
   saved_list   - list of all items that absolutely must be saved if garbage
                  collection occurs. Usually has at least the data_base and
                  the currents query attached to it.
   initial_heap - the value of the heap pointer at the start of the program.
                  used by the garbage collector
   free         - the list of free nodes.
   total_free   - total number of free blocks on the free list.
   data_base    - a pointer to the start of the data base. It points to a
                  node pointing to the first sentence in the data base. Nodes
                  pointing to sentences are linked together to form the data
                  base.
   delim_set    - set of characters which delimit tokens. *)



(* ----------------------------------------------------------------------
        Utility Routines
   ---------------------------------------------------------------------- *)

Procedure noise ;
  (* Make a noise on the terminal - used for warnings. *)
Begin
  write(bell) ;
End ;
(* noise *)

Function open(Var f : text_file ; f_name : string80) : boolean ;

(* open a file - returns true if the file exists and was opened properly
     f      - file pointer
     f_name - external name of the file *)
Begin
  assign(f,f_name) ;
   (*$I- *)
  reset(f) ;
   (*$I+ *)
  open := (ioresult = 0) ;
End ;
(* open *)


Function is_console(Var f : text_file) : boolean ;

(* return true if f is open on the system console
     for details of fibs and fib_ptrs see the Turbo Pascal ver 3.0 reference
     manual chapter 20. This should work under CP/M-86 or 80, but we haven't
     tried it. *)

Type 
  fib = ARRAY [0 .. 75] Of byte ;

Var 
  fib_ptr : ^fib ;
  dev_type : byte ;
Begin
  fib_ptr := addr(f) ;
  dev_type := fib_ptr^[2] And $07 ;
  is_console := (dev_type = 1) Or (dev_type = 2) ;
End ;
(* is_console *)


Procedure strip_leading_blanks(Var s : string80) ;
Begin
  If length(s) > 0
    Then
    If (s[1] = ' ') Or (s[1] = tab)
      Then
      Begin
        delete(s,1,1) ;
        strip_leading_blanks(s) ;
      End ;
End ;
(* strip_leading_blanks *)


Procedure strip_trailing_blanks(Var s : string80) ;
Begin
  If length(s) > 0
    Then
    If (s[length(s)] = ' ') Or (s[length(s)] = tab)
      Then
      Begin
        delete(s,length(s),1) ;
        strip_trailing_blanks(s) ;
      End ;
End ;
(* strip_trailing_blanks *)



Function toupper(s : string80) : string80 ;
  (* returns s converted to upper case *)

Var 
  i : byte ;
Begin
  If length(s) > 0
    Then
    For i := 1 To length(s) Do
      s[i] := upcase(s[i]) ;
  toupper := s ;
End ;
(* toupper *)


Function is_number(s : string80) : boolean ;

(* checks to see if s contains a legitimate numerical string.
     It ignores leading and trailing blanks *)

Var 
  num : real ;
  code : integer ;
Begin
  strip_trailing_blanks(s) ;
  strip_leading_blanks(s) ;
  If s <> ''
    Then val(s,num,code)
  Else code := -1 ;
  is_number := (code = 0) ;
End ;
(* is_number *)


Function head(list : node_ptr) : node_ptr ;
  (* returns a pointer to the first item in the list.
     If the list is empty, it returns NIL.  *)
Begin
  If list = Nil
    Then head := Nil
  Else head := list^.head_ptr ;
End ;
(* head *)


Function tail(list : node_ptr) : node_ptr ;

(* returns a pointer to a list starting at the second item in the list.
     Note - tail( (a b c) ) points to the list (b c), but
            tail( ((a b) c d) ) points to the list (c d) .  *)
Begin
  If list = Nil
    Then tail := Nil
  Else
    Case list^.tag Of 
      cons_node : tail := list^.tail_ptr ;
      free_node : tail := list^.next_free ;
      Else        tail := Nil ;
    End ;
End ;
(* tail *)


Function allocation_size(x : counter) : counter ;

(* Turbo 3.0 allocates memory in 8 byte blocks, this routine calculates the
     actual number of bytes returned for a request of x bytes.  *)
Begin
  allocation_size := (((x - 1) Div 8) + 1) * 8 ;
End ;
(* allocation_size *)


Function node_size : counter ;

(* calculates the base size of a node. Add the rest of the node to this
     to get the actual size of a node *)
Begin
  node_size := 2 * sizeof(node_ptr) + sizeof(boolean) + sizeof(node_type) ;
End ;
(* node_size *)


Function normalize(pt : node_ptr) : node_ptr ;

(* returns a normalized pointer. Pointers are 32 bit addresses. The first
     16 bits contain the segment number and the second 16 bits contain the
     offset within the segment. Normalized pointers have offsets in the range
     $0 to $F (0 .. 15)    *)

Var 
  pt_seg,pt_ofs : integer ;
Begin
  pt_seg := seg(pt^) + (ofs(pt^) Div 16) ;
  pt_ofs := ofs(pt^) Mod 16 ;

(* Johnicholas says: so ptr is some primitive that builds a (32-bit) pointer from a pair of (16-bit) ints?
      *  Maybe I just need to cast the FarPointer to the appropriate type? *)
  normalize := node_ptr(ptr(pt_seg, pt_ofs)) ;
End ;
(* normalize *)


Function string_val(list : node_ptr) : string80 ;

(* returns the string pointed to by list. If list points to a number
     node, it returns a string representing that number *)

Var 
  s : string[15] ;
Begin
  If list = Nil
    Then string_val := ''
  Else If list^.tag In [constant,variable,func]
         Then string_val := list^.string_data
  Else string_val := '' ;
End ;
(* string_val *)


Function tag_value(list : node_ptr) : node_type ;
  (* returns the value of the tag for a node.     *)
Begin
  If list = Nil
    Then tag_value := free_node
  Else tag_value := list^.tag ;
End ;
(* tag_value *)


Procedure print_list(list : node_ptr) ;

(* recursively traverses the list and prints its elements. This is
     not a pretty printer, so the lists may look a bit messy.  *)

Var 
  p : node_ptr ;
Begin
  If list <> Nil
    Then
    Case list^.tag Of 
      constant,
      func,
      variable  : write(string_val(list),' ') ;
      cons_node :
                  Begin
                    write('(') ;
                    p := list ;
                    While p <> Nil Do
                      Begin
                        print_list(head(p)) ;
                        p := tail(p) ;
                      End ;
                    write(') ') ;
                  End ;
    End ;
End ;
(* print_list *)


Procedure get_memory(Var p : node_ptr ; size : counter) ;

(* On exit p contains a pointer to a block of allocation_size(size) bytes.
     If possible this routine tries to get memory from the free list before
     requesting it from the heap *)

Var 
  blks : counter ;
  allocated : boolean ;

Procedure get_from_free(Var list : node_ptr) ;

(* Try and get need memory from the free list. This routine uses a
      first-fit algorithm to get the space. It takes the first free block it
      finds with enough storage. If the free block has more storage than was
      requested, the block is shrunk by the requested amount.  *)
Begin
  If list <> Nil
    Then
    If list^.block_cnt >= (blks - 1)
      Then
      Begin
        p := normalize(node_ptr(ptr(seg(list^), ofs(list^) +
             (list^.block_cnt - blks + 1) * 8))) ;
        If list^.block_cnt = blks - 1
          Then list := list^.next_free
        Else list^.block_cnt := list^.block_cnt - blks ;
        allocated := true ;
        total_free := total_free - (blks * 8.0) ;
      End
  Else get_from_free(list^.next_free) ;
End ;
(* get_from_free *)

Begin
  blks := ((size - 1) Div 8) + 1 ;
  allocated := false ;
  get_from_free(free) ;
  If Not allocated
    Then getmem(p,blks * 8) ;
End ;
(* get_memory *)


Function alloc_str(typ : node_type ; s : string80) : node_ptr ;

(* Allocate storage for a string and return a pointer to the new node.
     This routine only allocates enough storage for the actual number of
     characters in the string plus one for the length. Because of this,
     concatenating anything to the end of a string stored in a symbol node
     will lead to disaster. Copy the string to a new string do the
     concatenation and then allocate a new node.  *)

Var 
  pt : node_ptr ;
Begin
  get_memory(pt,allocation_size(sizeof(node_type) + sizeof(boolean) +
  length(s) + 1)) ;
  pt^.tag := typ   ;
  pt^.string_data := s ;
  alloc_str := pt ;
End ;
(* alloc_str *)


Function cons(new_node,list : node_ptr) : node_ptr ;

(* Construct a list. This routine allocates storage for a new cons node.
     new_node points to the new head of the list. The tail pointer of the
     new node points to list. This routine adds the new cons node to the
     beginning of the list and returns a pointer to it. The list described
     in the comments at the beginning of the program could be constructed
     as cons(alloc_str('A'),cons(alloc_str('B'),cons(alloc_str('C'),NIL))). *)

Var 
  p : node_ptr ;
Begin
  get_memory(p,allocation_size(node_size)) ;
  p^.tag := cons_node ;
  p^.head_ptr := new_node ;
  p^.tail_ptr := list ;
  cons := p ;
End ;
(* cons *)


Function append_list(list1,list2 : node_ptr) : node_ptr ;

(* Append list2 to list1. This routine returns a pointer to the
     combined list. Appending is done by consing each item on the first
     list to the second list. This routine is one of the major sources of
     garbage so if garbage collection becomes a problem, you may want to
     rewrite it. *)
Begin
  If list1 = Nil
    Then append_list := list2
  Else append_list := cons(head(list1),append_list(tail(list1),list2)) ;
End ;
(* append_list *)


Function list_length(list : node_ptr) : counter ;
  (* returns the length of a list.
     Note - both (A B C) and ( (A B) C D) have length 3.   *)
Begin
  If list = Nil
    Then list_length := 0
  Else list_length := 1 + list_length(list^.tail_ptr) ;
End ;
(* list_length *)


Procedure collect_garbage ;

(* This routine is specific to Turbo Pascal Ver 3.01
     It depends upon the fact that Turbo allocates memory in 8 byte blocks
     on the PC. If you recompile this program on another system be very
     careful with this routine.
     Garbage collection proceeds in three phases:
      unmark  - free all memory between the initial_heap^ and the current
                top of the heap.
      mark    - mark everything on the saved_list as being in ues.
      release - gather all unmarked blocks and put them on the free list.
     The collector displays a '*' on the screen to let you know it is
      operating.  *)

Function lower(p1,p2 : node_ptr) : boolean ;
   (* returns true if p1 points to a lower memory address than p2 *)
Begin
  p1 := normalize(p1) ;
  p2 := normalize(p2) ;
  lower := (seg(p1^) < seg(p2^)) Or
           ((seg(p1^) = seg(p2^)) And (ofs(p1^) < ofs(p2^))) ;
End ;
(* lower *)

Procedure mark(list : node_ptr) ;

(* Mark the blocks on list as being in use. Since a node may be on several
      lists at one time, if it is already marked we don't continue processing
      the tail of the list. *)
Begin
  If list <> Nil
    Then
    Begin
      If Not list^.in_use
        Then
        Begin
          list^.in_use := true ;
          If list^.tag = cons_node
            Then
            Begin
              mark(head(list)) ;
              mark(tail(list)) ;
            End ;
        End ;
    End ;
End ;
(* mark *)

Procedure unmark_mem ;

(* Go through memory from initial_heap^ to HeapPtr^ and mark each node
      as not in use. The tricky part here is updating the pointer p to point
      to the next cell. *)

Var 
  p : node_ptr ;
  string_base,node_allocation : counter ;
Begin
  string_base := sizeof(node_type) + sizeof(boolean) ;
  p := normalize(initial_heap) ;
  node_allocation := allocation_size(node_size) ;

(* Johnicholas says: HeapPtr is something from the TP runtime - is there something analogous in the fpc runtime?
 *  Are we walking over all of memory, or just some specific region? *)
  While lower(p, HeapPtr) Do
    Begin
      p^.in_use := false ;
      Case p^.tag Of 
        cons_node : p := normalize(node_ptr(ptr(seg(p^),ofs(p^) + node_allocation))) ;
        free_node : p := normalize(node_ptr(ptr(seg(p^),ofs(p^) + (p^.block_cnt + 1) * 8))) ;
        func,
        constant,
        variable  : p := normalize(node_ptr(ptr(seg(p^),
                         ofs(p^) +
                         allocation_size(string_base +
                         length(p^.string_data) + 1)))) ;
      End ;
    End ;
End ;
(* unmark_mem *)

Procedure release_mem ;

(* This procedure does the actual collection and compaction of nodes.
      This is the slow phase of garbage collection because of all the pointer
      manipulation.  *)

Var 
  heap_top : node_ptr ;
  string_base,node_allocation,string_allocation,block_allocation : counter ;

Procedure free_memory(pt : node_ptr ; size : counter) ;

(* return size bytes pointed to by pt to the free list. If pt points to
       a block next to the head of the free list combine it with the top
       free node. total_free keeps track of the total number of free bytes. *)

Var 
  blks : counter ;
Begin
  blks := ((size - 1) Div 8) + 1 ;
  pt^.tag := free_node ;
  If normalize(node_ptr(ptr(seg(pt^),ofs(pt^) + 8 * blks))) = free
    Then
    Begin
      pt^.next_free := free^.next_free ;
      pt^.block_cnt := free^.block_cnt + blks ;
      free := pt ;
    End
  Else If normalize(node_ptr(ptr(seg(free^),ofs(free^) + 8 * (free^.block_cnt + 1)))) =
          normalize(pt)
         Then free^.block_cnt := free^.block_cnt + blks
  Else
    Begin
      pt^.next_free := free ;
      pt^.block_cnt := blks - 1 ;
      free := pt ;
    End ;
  total_free := total_free + (blks * 8.0) ;
End ;
(* free_memory *)

Procedure do_release ;
    (* This routine sweeps through memory and checks for nodes with
       in_use = false. *)

Var 
  p : node_ptr ;
Begin
  p := normalize(initial_heap) ;
  While lower(p,heap_top) Do
    Case p^.tag Of 
      cons_node :
                  Begin
                    If Not p^.in_use
                      Then free_memory(p,node_size) ;
                    p := normalize(node_ptr(ptr(seg(p^),ofs(p^) + node_allocation))) ;
                  End ;
      free_node :
                  Begin
                    block_allocation := (p^.block_cnt + 1) * 8 ;
                    free_memory(p,block_allocation) ;
                    p := normalize(node_ptr(ptr(seg(p^),ofs(p^) + block_allocation))) ;
                  End ;
      func,
      constant,
      variable  :
                  Begin
                    string_allocation := allocation_size(string_base +
                                         length(p^.string_data) + 1) ;
                    If Not p^.in_use
                      Then free_memory(p,string_base + length(p^.string_data)
                      + 1) ;
                    p := normalize(node_ptr(ptr(seg(p^),ofs(p^) + string_allocation))) ;
                  End ;
    End ;
End ;
(* do_release *)

Begin
  free := Nil ;
  total_free := 0.0 ;
  heap_top := HeapPtr ;
  string_base := sizeof(node_type) + sizeof(boolean) ;
  node_allocation := allocation_size(node_size) ;
  do_release ;
End ;
(* release_mem *)

Begin
  write('*') ;
  unmark_mem ;
  mark(saved_list) ;
  release_mem ;
  write(back_space) ;

  ClrEol ;

End ;
(* collect_garbage *)


Procedure test_memory ;

(* This routine activates the garbage collector, if the the total available
     memory (free_list + heap) is less than a specified amount. Lowering the
     minimum causes garbage collection to be called less often, but if you
     make it too small you may not have enough room left for recursion or any
     temporary lists you need. Using 10000 is probably being overly
     cautious.   *)
Begin
     (* Johnicholas says: is memavail something in the legacy context? yes
      *  The fpc docs say: The MemAvail and MaxAvail functions are no longer available.
      *  On modern operating systems, the idea of "Available Free Memory" is not valid for an application.
      *  The reasons are:
      *  1. One processor cycle after an application asked the OS how much memory was free, another application
      *  may have allocated everything.
      *  2. It is not clear what "free memory" means [...snip...]
      If (memavail * 16.0) + total_free < 10000 
      Then collect_garbage ;
      *)
End ;
(* test_memory *)


Procedure wait ;
  (* Just like it says. It waits for the user to press a key before
     continuing. *)

Var 
  ch : char ;
Begin
  writeln ;
  writeln ;
  write('Press any key to continue. ') ;
  read(Input,ch) ;
  write(return) ;

  ClrEol ;

End ;
(* wait *)



(* ------------------------------------------------------------------------
        End of utility routines
   ------------------------------------------------------------------------ *)

Procedure read_kbd(Var s : string80) ;
  (* Read a line from the keyboard *)
Begin
  write('-> ') ;
  readln(s) ;
End ;
(* read_kbd *)


Procedure read_from_file(Var f : text_file) ;

(* Read a line from file f and store it in the global variable line.
     It ignores blank lines and when the end of file is reached an
     eof_mark is returned. *)

Procedure read_a_line ;
Begin
    (*$I- *)
  readln(f,line) ;
    (*$I+ *)
  If ioresult <> 0
    Then line := eof_mark
  Else If eof(f)
         Then line := concat(line,eof_mark) ;
End ;
(* read_a_line *)

Begin
  line := '' ;
  If is_console(f)
    Then read_kbd(line)
  Else read_a_line ;
  If in_comment
    Then
    If pos('*)',line) > 0
      Then
      Begin
        delete(line,1,pos('*)',line) + 1) ;
        in_comment := false ;
      End
  Else read_from_file(f) ;
  saved_line := line ;
End ;
(* read_from_file *)


Procedure get_token(Var t_line : string132 ; Var token : string80) ;

(* Extract a token from t_line. Comments are ignored. A token is
     a string surrounded by delimiters or an end of line. Tokens may
     contain embedded spaces if they are surrounded by quote marks *)

Procedure get_word ;

Var 
  done : boolean ;
  cn : integer ;
  len : byte ;
Begin
  cn := 1 ;
  len := length(t_line) ;
  done := false ;
  While Not done Do
    If cn > len
      Then done := true
    Else If t_line[cn] In delim_set
           Then done := true
    Else cn := cn + 1 ;
  token := copy(t_line,1,cn-1) ;
  delete(t_line,1,cn-1) ;
End ;
(* get_word *)

Procedure comment ;
Begin
  If pos('*)',t_line) > 0
    Then
    Begin
      delete(t_line,1,pos('*)',t_line)+1) ;
      get_token(line,token) ;
    End
  Else
    Begin
      t_line := '' ;
      token := '' ;
      in_comment := true ;
    End ;
End ;
(* comment *)

Procedure get_quote ;
Begin
  delete(t_line,1,1) ;
  If pos(quote_char,t_line) > 0
    Then
    Begin
      token := concat(quote_char,copy(t_line,1,pos(quote_char,t_line) - 1)) ;
      delete(t_line,1,pos(quote_char,t_line)) ;
    End
  Else
    Begin
      token := t_line ;
      t_line := '' ;
    End ;
End ;
(* get_quote *)

Begin
  strip_leading_blanks(t_line) ;
  If length(t_line) > 0
    Then
    Begin
      If copy(t_line,1,2) = '(*'
        Then comment
      Else If (copy(t_line,1,2) = ':-') Or (copy(t_line,1,2) = '?-')
             Then
             Begin
               token := copy(t_line,1,2) ;
               delete(t_line,1,2) ;
             End
      Else If t_line[1] = quote_char
             Then get_quote
      Else If t_line[1] In delim_set
             Then
             Begin
               token := t_line[1] ;
               delete(t_line,1,1) ;
             End
      Else get_word ;
    End
  Else token := '' ;
End ;
(* get_token *)


Procedure scan(Var f : text_file ; Var token : string80) ;

(* Scan repeatedly calls get_token to retreive tokens. When the
     end of a line has been reached, read_from_file is called to
     get a new line. *)
Begin
  If length(line) > 0
    Then
    Begin
      get_token(line,token) ;
      If token = ''
        Then scan(f,token) ;
    End
  Else
    Begin
      read_from_file(f) ;
      scan(f,token) ;
    End ;
End ;
(* scan *)


Procedure compile(Var source : text_file) ;

(* The recursive descent compiler. It reads tokens until the token
     'EXIT' is found. If the token is '?-', a query is performed, a '@' token
     is the command to read a new file and source statements are read form that
     file, otherwise the token is assumed to be part of a sentence and the rest
     of the sentence is parsed. *)

Procedure error(error_msg : string80) ;

(* Signal an error. Prints saved_line to show where the error is located.
      saved_line contains the current line being parsed. it is required,
      because get_token chews up line as it reads tokens. *)

Procedure runout ;
Begin
  While (token <> '.') And (token <> eof_mark) Do
    scan(source,token) ;
End ;
(* runout *)

Begin
  error_flag := true ;
  writeln ;
  writeln(error_msg) ;
  writeln ;
  writeln(saved_line) ;
  writeln('' : length(saved_line) - length(line) - 1,'^') ; ;
  runout ;
  wait ;
End ;
(* error *)

Procedure goal(Var l_ptr : node_ptr) ;

(* Read a goal. The new goal is appended to l_ptr. Each goal is appended
      to l_ptr as a list. Thus, the sentence 'likes(john,X) :- likes(X,wine) .'
      becomes the list ( (likes john X) (likes X wine) ) *)

Var 
  goal_token : string80 ;

Procedure functor(Var f_ptr : node_ptr ; func_token : string80) ;

(* The current goal is a functor. This routine allocates a node
       to store the functor and then processes the components of the
       functor. On exit, f_ptr points to the list containing the functor
       and its components. func_token contains the functor name. *)

Var 
  c_ptr : node_ptr ;

Procedure components(Var cm_ptr : node_ptr) ;

(* Process the components of the functor. The components are terms
        seperated by commas. On exit, cm_ptr points to the list of
        components. *)

Procedure term(Var t_ptr : node_ptr) ;
      (* Process a single term. The new term is appended to t_ptr. *)

Var 
  t_token : string80 ;

Procedure quoted_str(Var q_ptr : node_ptr) ;
       (* Process a quote *)
Begin
  q_ptr := append_list(q_ptr,cons(alloc_str(constant,
           copy(token,2,length(token) - 1)),
           Nil)) ;
  scan(source,token) ;
End ;
(* quoted_str *)

Procedure varbl(Var v_ptr : node_ptr) ;
       (* The current token is a varaible, allocate a node and return
          a pointer to it. *)
Begin
  v_ptr := append_list(v_ptr,cons(alloc_str(variable,token),Nil)) ;
  scan(source,token) ;
End ;
(* varbl *)

Procedure number(Var n_ptr : node_ptr) ;

(* Numbers are treated as string constants. This isn't particularly
          efficent, but it is easy. *)
Begin
  n_ptr := append_list(n_ptr,cons(alloc_str(constant,token),Nil)) ;
  scan(source,token) ;
End ;
(* handle_number *)

Begin
  If token[1] In ['A' .. 'Z','_']
    Then varbl(t_ptr)
  Else If token[1] = quote_char
         Then quoted_str(t_ptr)
  Else If is_number(token)
         Then number(t_ptr)
  Else If token[1] In ['a' .. 'z']
         Then
         Begin
           t_token := token ;
           scan(source,token) ;
           If token = '('
             Then functor(t_ptr,t_token)
           Else t_ptr := append_list(t_ptr,
                         cons(alloc_str(constant,t_token),Nil)) ;
         End
  Else error('Illegal Symbol.') ;
End ;
(* term *)

Begin
  term(cm_ptr) ;
  If token = ','
    Then
    Begin
      scan(source,token) ;
      components(cm_ptr) ;
    End ;
End ;
(* components *)

Begin
  c_ptr := cons(alloc_str(func,func_token),Nil) ;
  scan(source,token) ;
  components(c_ptr) ;
  If token = ')'
    Then
    Begin
      f_ptr := append_list(f_ptr,cons(c_ptr,Nil)) ;
      scan(source,token) ;
    End
  Else error('Missing '')''.') ;
End ;
(* functor *)

Begin
  If token[1] In ['a' .. 'z',quote_char]
    Then
    Begin
      If token[1] = quote_char
        Then
        Begin
          l_ptr := append_list(l_ptr,
                   cons(cons(alloc_str(constant,
                   copy(token,2,length(token) - 1)),Nil),Nil)) ;
          scan(source,token) ;
        End
      Else
        Begin
          goal_token := token ;
          scan(source,token) ;
          If token = '('
            Then functor(l_ptr,goal_token)
          Else l_ptr := append_list(l_ptr,
                        cons(cons(alloc_str(constant,goal_token),
                        Nil),Nil)) ;
        End
    End
  Else error('A goal must begin with ''a .. z'' or be a quoted string.') ;
End ;
(* goal *)

Procedure tail_list(Var t_ptr : node_ptr) ;

(* Process the tail of a rule. Since the a query is syntactically identical
      to the tail of a rule, this routine is used to compile queries.
      On exit, t_ptr points to the list containing the tail. *)
Begin
  goal(t_ptr) ;
  If token = ','
    Then
    Begin
      scan(source,token) ;
      tail_list(t_ptr) ;
    End ;
End ;
(* tail *)

Procedure rule ;

(* Procees a rule, actually any sentence. If no error occurs the
      new sentence is appended to the data base. *)

Var 
  r_ptr : node_ptr ;

Procedure head_list(Var h_ptr : node_ptr) ;
Begin
  goal(h_ptr) ;
End ;
(* head *)

Begin
  saved_list := cons(data_base,Nil) ;
  test_memory ;
  r_ptr := Nil ;
  head_list(r_ptr) ;
  If token = ':-'
    Then
    Begin
      scan(source,token) ;
      tail_list(r_ptr) ;
    End ;
  If token <> '.'
    Then error('''.'' expected.') ;
  If Not error_flag
    Then data_base := append_list(data_base,cons(r_ptr,Nil)) ;
End ;
(* rule *)

Procedure query ;

(* Process a query. Compile the query, and then call solve to search the
      data base. q_ptr points to the compiled query and solved is a boolean
      indicating whether the query was successfully solved. *)

Var 
  q_ptr : node_ptr ;
  solved : boolean ;

Procedure solve(list,env : node_ptr ; level : counter) ;

(* This is where all the hard work is done. This routine follows the
       steps outlined in the article. list is the query to be soved, env is
       the current environment and level is the recursion level. level can
       only get to 32767, but you'll run out of stack space long before you
       get that far.
       solve saves list and env on the saved list so that they won't be
       destroyed by garbage collection. The data base is always on the
       saved list. At the end of solve, list and env are removed from
       saved_list. *)

Var 
  new_env,p : node_ptr ;

Function look_up(var_str : string80 ; environ : node_ptr) : node_ptr ;

(* Search the environment list pointed to by environ for the variable,
        var_str. If found return a pointer to var_str's binding, otherwise
        return NIL *)

Var 
  found : boolean ;
  p : node_ptr ;
Begin
  p := environ ;
  found := false ;
  While (p <> Nil) And (Not found) Do
    Begin
      If var_str = string_val(head(head(p)))
        Then
        Begin
          found := true ;
          look_up := tail(head(p)) ;
        End
      Else p := tail(p) ;
    End ;
  If Not found
    Then look_up := Nil ;
End ;
(* look_up *)

Procedure check_continue ;

(* Print the bindings and see if the user is satisfied. If nothing
        is printed from the environment, then print 'Yes' to indicate
        that the query was successfully satisfied. *)

Var 
  printed : boolean ;
  ch : char ;

Procedure print_bindings(list : node_ptr) ;

(* Print the bindings for level 0 variables only, intermediate variables
         aren't of interest. The routine recursivley searches for the
         end of the environments list and then prints the binding. This
         is so that variables bound first are printed first. *)

Procedure print_functor(l : node_ptr) ;
FORWARD ;

Procedure print_variable(var_str : string80) ;

(* The varaible in question was bound to another varaible, so look
          up that variable's binding and print it. If a match can't be found
          print '_' to tell the user that the variable is anonymous. *)

Var 
  var_ptr : node_ptr ;
Begin
  var_ptr := look_up(var_str,env) ;
  If var_ptr <> Nil
    Then
    Case tag_value(head(var_ptr)) Of 
      constant  : write(string_val(head(var_ptr)),' ') ;
      variable  : print_variable(string_val(head(var_ptr))) ;
      cons_node : print_functor(head(var_ptr)) ;
    End
  Else write('_ ') ;
End ;
(* print_variable *)

Procedure print_functor (l : node_ptr) ;
       (* The variable was bound to a functor. Print the functor and its
          components. *)

Procedure print_components(p : node_ptr) ;

(* Print the components of a functor. These may be variables or
           other functors, so call the approriate routines to print them. *)
Begin
  If p <> Nil
    Then
    Begin
      Case tag_value(head(p)) Of 
        constant  : write(string_val(head(p)),' ') ;
        variable  : print_variable(string_val(head(p))) ;
        cons_node : print_functor(head(p)) ;
      End ;
      If tail(p) <> Nil
        Then
        Begin
          write(',') ;
          print_components(tail(p)) ;
        End ;
    End ;
End ;
(* print_components *)

Begin
  If l <> Nil
    Then
    Begin
      write(string_val(head(l))) ;
      If tail(l) <> Nil
        Then
        Begin
          write('(') ;
          print_components(tail(l)) ;
          write(')') ;
        End ;
    End ;
End ;
(* print_functor *)

Begin
  If list <> Nil
    Then
    Begin
      print_bindings(tail(list)) ;
      If pos('#',string_val(head(head(list)))) = 0
        Then
        Begin
          printed := true ;
          writeln ;
          write(string_val(head(head(list))),' = ') ;
          Case tag_value(head(tail(head(list)))) Of 
            constant  : write(string_val(head(tail(head(list)))),' ') ;
            variable  : print_variable(string_val(head(tail(head(list))))) ;
            cons_node : print_functor(head(tail(head(list)))) ;
          End ;
        End ;
    End ;
End ;
(* print_bindings *)

Begin
  printed := false ;
  print_bindings(env) ;
  If Not printed
    Then
    Begin
      writeln ;
      write('Yes ') ;
    End ;
  Repeat
    read(Input,ch) ;
  Until ch In [return,';'] ;
  solved := (ch = return) ;
  writeln ;
End ;
(* check_continue *)

Function copy_list(list : node_ptr ; copy_level : counter) : node_ptr ;
     (* Copy a list and append the copy_level (recursion level) to all
        variables. *)

Var 
  temp_list,p : node_ptr ;
  level_str : string[6] ;

Procedure list_copy(from_list : node_ptr ; Var to_list : node_ptr) ;
Begin
  If from_list <> Nil
    Then
    Case from_list^.tag Of 
      variable : to_list := alloc_str(variable,
                            concat(from_list^.string_data,
                            level_str)) ;
      func,
      constant  : to_list := from_list ;
      cons_node :
                  Begin
                    list_copy(tail(from_list),to_list) ;
                    to_list := cons(copy_list(head(from_list),copy_level),
                               to_list) ;
                  End ;
    End ;
End ;
(* list_copy *)

Begin
  str(copy_level,level_str) ;
  level_str := concat('#',level_str) ;
  temp_list := Nil ;
  list_copy(list,temp_list) ;
  copy_list := temp_list ;
End ;
(* copy_list *)

Function unify(list1,list2,environ : node_ptr ; Var new_environ : node_ptr) :
                                                                              boolean ;

(* Unify two lists and return any new bindings at the front of the
        environment list. Returns true if the lists could be unified. This
        routine implements the unification table described in the article.
        Unification is straight forward, but the details of matching the
        lists get a little messy in this routine. There are better ways to
        do all of this, we just haven't gotten around to trying them. If
        you implement any other unification methods, we would be glad to
        hear about it.
        Unify checks to see if both lists are NIL, this is a successful
        unification. If one list is NIL, unification fails. Otherwise check
        what kind on node the head of list1 is and call the appropriate
        routine to perform the unification. Variables are unified by
        looking up the binding of the variable. If none is found, make
        a binding for the variable, otherwise try to unify the binding
        with list2. *)

Var 
  var_ptr : node_ptr ;

Procedure make_binding(l1,l2 : node_ptr) ;

(* Bind a variable to the environment. Anonymous variables are not bound.
         l1 points to the variable and l2 points to its binding. *)
Begin
  If copy(string_val(head(l1)),1,1) <> '_'
    Then new_environ := cons(cons(head(l1),l2),environ)
  Else new_environ := environ ;
  unify := true ;
End ;
(* make_binding *)

Procedure fail ;
      (* Unification failed. *)
Begin
  unify := false ;
  new_environ := environ ;
End ;
(* fail *)

Procedure unify_constant ;

(* List1 contains a constant. Try to unify it with list2. The 4 cases
         are:
          list2 contains
           constant - unify if constants match
           variable - look up binding, if no current binding bind the
                      constant to the variable, otherwise unify list1
                      with the binding.
           cons_node,
           func     - these can't be unified with a constant. A cons_node
                      indicates an expression. *)
Begin
  Case tag_value(head(list2)) Of 
    constant  : If string_val(head(list1)) = string_val(head(list2))
                  Then
                  Begin
                    unify := true ;
                    new_environ := environ ;
                  End
                Else fail ;
    variable  :
                Begin
                  var_ptr := look_up(string_val(head(list2)),environ) ;
                  If var_ptr = Nil
                    Then make_binding(list2,list1)
                  Else unify := unify(list1,var_ptr,environ,new_environ) ;
                End ;
    cons_node,
    func      : fail ;
  End ;
End ;
(* unify_constant *)

Procedure unify_func ;

(* List1 contains a functor. Try to unify it with list2. The 4 cases
         are:
          list2 contains
           constant  - can't be unified.
           variable  - look up binding, if no current binding bind the
                       functor to the variable, otherwise unify list1
                       with the binding.
           cons_node - fail
           func      - if the functors match, then true to unify the component
                       lists (tail of the list) term by term. *)

Procedure unify_tail ;
       (* This routine does the term by term unification of the component
          lists *)

Var 
  p,q : node_ptr ;
  unified : boolean ;
Begin
  p := tail(list1) ;
  q := tail(list2) ;
  unified := true ;
  new_environ := environ ;
  While (p <> Nil) And unified Do
    Begin
      unified := unified And unify(cons(head(p),Nil),cons(head(q),Nil),
                 new_environ,new_environ) ;
      p := tail(p) ;
      q := tail(q) ;
    End ;
  If Not unified
    Then fail ;
End ;
(* unify_tail *)

Begin
  Case tag_value(head(list2)) Of 
    constant  : fail ;
    variable  :
                Begin
                  var_ptr := look_up(string_val(head(list2)),environ) ;
                  If var_ptr = Nil
                    Then make_binding(list2,list1)
                  Else unify := unify(list1,var_ptr,environ,new_environ) ;
                End ;
    func      : If string_val(head(list1)) = string_val(head(list2))
                  Then
                  If list_length(tail(list1)) = list_length(tail(list2))
                    Then unify_tail
                Else fail
                Else fail ;
    cons_node : fail ;
  End ;
End ;
(* unify_func *)

Procedure unify_expr ;

(* List1 contains an expression. Try to unify it with list2. The 4 cases
         are:
          list2 contains
           constant  - can't be unified.
           variable  - look up binding, if no current binding bind the
                       functor to the variable, otherwise unify list1
                       with the binding.
           cons_node - If the heads can be unified, the unify the tails.
           func      - fail *)
Begin
  Case tag_value(head(list2)) Of 
    constant  : fail ;
    variable  :
                Begin
                  var_ptr := look_up(string_val(head(list2)),environ) ;
                  If var_ptr = Nil
                    Then make_binding(list2,list1)
                  Else unify := unify(list1,var_ptr,environ,new_environ) ;
                End ;
    func      : fail ;
    cons_node : If unify(head(list1),head(list2),environ,new_environ)
                  Then unify := unify(tail(list1),tail(list2),new_environ,
                                new_environ)
                Else fail ;
  End ;
End ;
(* unify_expr *)

Begin
  If (list1 = Nil) And (list2 = Nil)
    Then
    Begin
      unify := true ;
      new_environ := environ ;
    End
  Else If list1 = Nil
         Then fail
  Else If list2 = Nil
         Then fail
  Else
    Case tag_value(head(list1)) Of 
      constant  : unify_constant ;
      variable  :
                  Begin
                    var_ptr := look_up(string_val(head(list1)),environ) ;
                    If var_ptr = Nil
                      Then make_binding(list1,list2)
                    Else unify := unify(var_ptr,list2,environ,new_environ) ;
                  End ;
      func      : unify_func ;
      cons_node : unify_expr ;
    End ;
End ;
(* unify *)

Begin
  saved_list := cons(list,cons(env,saved_list)) ;
  If list = Nil
    Then check_continue
  Else
    Begin
      p := data_base ;
      While (p <> Nil) And (Not solved) Do
        Begin
          test_memory ;
          If unify(copy_list(head(head(p)),level),head(list),env,new_env)
            Then solve(append_list(copy_list(tail(head(p)),level),tail(list)),
            new_env,level + 1) ;
          p := tail(p) ;
        End ;
    End ;
  saved_list := tail(tail(saved_list)) ;
End ;
(* solve *)

Begin
  q_ptr := Nil ;
  tail_list(q_ptr) ;
  If token <> '.'
    Then error('''.'' expected.')
  Else If Not error_flag
         Then
         Begin
           solved := false ;
           saved_list := cons(data_base,Nil) ;
           solve(q_ptr,Nil,0) ;
           If Not solved
             Then writeln('No') ;
         End ;
End ;
(* query *)

Procedure read_new_file ;

(* Read source statements from a new file. When all done, close file
      and continue reading from the old file. Files may be nested, but you
      will run into trouble if you nest them deaper than 15 levels. This
      is Turbo's default for open files. *)

Var 
  new_file : text_file ;
  old_line,old_save : string132 ;
  f_name : string80 ;
Begin
  If token[1] = quote_char
    Then delete(token,1,1) ;
  If pos('.',token) = 0
    Then f_name := concat(token,'.PRO')
  Else f_name := token ;
  If open(new_file,f_name)
    Then
    Begin
      old_line := line ;
      old_save := saved_line ;
      line := '' ;
      compile(new_file) ;
      close(new_file) ;
      line := old_line ;
      saved_line := old_save ;
      scan(source,token) ;
      If token <> '.'
        Then error('''.'' expected.') ;
    End
  Else error(concat('Unable to open ',f_name)) ;
End ;
(* read_new_file *)

Procedure do_exit ;

(* Exit the program. This really should be a built-in function and handled
      in solve, but this does the trick. *)
Begin
  scan(source,token) ;
  If token <> '.'
    Then error('''.'' expected.')
  Else halt
End ;
(* do_exit *)

Begin
  scan(source,token) ;
  While token <> eof_mark Do
    Begin
      error_flag := false ;
      If token = '?-'
        Then
        Begin
          scan(source,token) ;
          query ;
        End
      Else If token = '@'
             Then
             Begin
               scan(source,token) ;
               read_new_file ;
             End
      Else If toupper(token) = 'EXIT'
             Then do_exit
      Else rule ;
      scan(source,token) ;
    End ;
End ;
(* compile *)


Procedure initialize ;
  (* Write a heading line and initialize the global variables *)
Begin
  clrscr ;
  writeln ;
  writeln('Very Tiny Prolog - Version 1.0     [c] 1986 MicroExpert Systems') ;
  writeln ;
  in_comment := false ;
  delim_set := [' ',')','(',',','[',']',eof_mark,tab,quote_char,':',
               '@','.','?'] ;
  text_chars := [' ' .. '~'] ;
  line := '' ;
  data_base := Nil ;
  free := Nil ;
  saved_list := Nil ;
  total_free := 0.0 ;
  initial_heap := HeapPtr ;
End ;
(* initialize *)


Begin
  initialize ;
  compile(Input) ;
End.