emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/wisi c7f61e5 26/35: In ada-mode, wisi; release ada-mode


From: Stefan Monnier
Subject: [elpa] externals/wisi c7f61e5 26/35: In ada-mode, wisi; release ada-mode 6.1.1, wisi 2.1.1
Date: Sat, 28 Nov 2020 14:47:56 -0500 (EST)

branch: externals/wisi
commit c7f61e51e53df391c9a239186ab5681bede6a299
Author: Stephen Leake <stephen_leake@stephe-leake.org>
Commit: Stephen Leake <stephen_leake@stephe-leake.org>

    In ada-mode, wisi; release ada-mode 6.1.1, wisi 2.1.1
---
 NEWS                                               |   37 +-
 README                                             |    2 +-
 ...da_containers-gen_doubly_linked_lists_image.adb |   50 +
 ...da_containers-gen_doubly_linked_lists_image.ads |   17 +-
 ...ors-gen_image_aux.ads => sal-ada_containers.ads |   44 +-
 sal-gen_bounded_definite_queues.adb                |  154 +
 sal-gen_bounded_definite_queues.ads                |   99 +
 sal-gen_bounded_definite_vectors-gen_sorted.adb    |   15 +-
 sal-gen_bounded_definite_vectors-gen_sorted.ads    |   10 +-
 sal-gen_graphs.adb                                 |  718 +++++
 sal-gen_graphs.ads                                 |  190 ++
 sal-gen_unbounded_definite_stacks.adb              |   34 +-
 sal-gen_unbounded_definite_stacks.ads              |   37 +-
 ...en_unbounded_definite_vectors-gen_image_aux.adb |   11 +-
 ...en_unbounded_definite_vectors-gen_image_aux.ads |    9 +-
 sal-gen_unbounded_definite_vectors.adb             |   22 +
 sal-gen_unbounded_definite_vectors.ads             |    3 +
 sal-generic_decimal_image.adb                      |   48 +
 sal-generic_decimal_image.ads                      |   37 +
 sal.adb                                            |    2 +-
 wisi-elisp-parse.el                                | 3407 ++++++++++----------
 wisi-parse-common.el                               |   53 +-
 wisi-process-parse.el                              |  107 +-
 wisi-run-indent-test.el                            |  300 ++
 wisi-tests.el                                      |  146 +
 wisi.adb                                           |  383 ++-
 wisi.ads                                           |   99 +-
 wisi.el                                            | 3157 +++++++++---------
 wisitoken-bnf-generate.adb                         |  239 +-
 wisitoken-bnf-generate_grammar.adb                 |  172 +-
 wisitoken-bnf-generate_utils.adb                   |  105 +-
 wisitoken-bnf-generate_utils.ads                   |   32 +-
 wisitoken-bnf-output_ada.adb                       |  117 +-
 wisitoken-bnf-output_ada_common.adb                |  187 +-
 wisitoken-bnf-output_ada_emacs.adb                 |  739 ++++-
 wisitoken-bnf-output_elisp.adb                     |    4 +-
 wisitoken-bnf-output_elisp_common.adb              |   19 +-
 wisitoken-bnf.adb                                  |   19 +-
 wisitoken-bnf.ads                                  |  112 +-
 wisitoken-gen_token_enum.ads                       |   64 +-
 wisitoken-generate-lr-lalr_generate.adb            | 1207 +++----
 wisitoken-generate-lr-lalr_generate.ads            |   14 +-
 wisitoken-generate-lr-lr1_generate.adb             |  652 ++--
 wisitoken-generate-lr-lr1_generate.ads             |   14 +-
 wisitoken-generate-lr.adb                          |  966 ++++--
 wisitoken-generate-lr.ads                          |   59 +-
 wisitoken-generate-lr1_items.adb                   |    3 +-
 wisitoken-generate-lr1_items.ads                   |    2 +-
 wisitoken-generate-packrat.adb                     |  494 +--
 wisitoken-generate-packrat.ads                     |  150 +-
 wisitoken-generate.adb                             |  977 +++---
 wisitoken-generate.ads                             |  310 +-
 wisitoken-lexer-re2c.adb                           |    2 +-
 wisitoken-lexer-re2c.ads                           |    2 +-
 wisitoken-lexer-regexp.adb                         |    6 +-
 wisitoken-lexer-regexp.ads                         |    2 +-
 wisitoken-lexer.ads                                |    4 +-
 wisitoken-parse-lr-mckenzie_recover-base.adb       |  875 ++---
 wisitoken-parse-lr-mckenzie_recover-base.ads       |  365 +--
 wisitoken-parse-lr-mckenzie_recover-explore.adb    | 3074 ++++++++++--------
 wisitoken-parse-lr-mckenzie_recover-explore.ads    |   56 +-
 wisitoken-parse-lr-mckenzie_recover-parse.adb      |  629 ++--
 wisitoken-parse-lr-mckenzie_recover-parse.ads      |  157 +-
 wisitoken-parse-lr-mckenzie_recover.adb            | 2389 +++++++-------
 wisitoken-parse-lr-mckenzie_recover.ads            |  500 +--
 wisitoken-parse-lr-parser.adb                      | 2282 ++++++-------
 wisitoken-parse-lr-parser.ads                      |   50 +-
 wisitoken-parse-lr-parser_lists.adb                |   18 +-
 wisitoken-parse-lr-parser_lists.ads                |    1 +
 wisitoken-parse-lr-parser_no_recover.adb           | 1060 +++---
 wisitoken-parse-lr.adb                             | 1764 +++++-----
 wisitoken-parse-lr.ads                             | 1310 ++++----
 wisitoken-parse-packrat-procedural.adb             |  512 +--
 wisitoken-parse-packrat-procedural.ads             |  166 +-
 wisitoken-parse_table-mode.el                      |    3 +-
 wisitoken-semantic_checks.adb                      |  304 +-
 wisitoken-semantic_checks.ads                      |  204 +-
 wisitoken-syntax_trees.adb                         | 2838 +++++++++-------
 wisitoken-syntax_trees.ads                         |  998 +++---
 wisitoken.adb                                      |   30 +
 wisitoken.ads                                      |  922 +++---
 wisitoken_grammar_actions.adb                      |   79 +-
 wisitoken_grammar_actions.ads                      |  117 +-
 wisitoken_grammar_main.adb                         |  656 +++-
 wisitoken_grammar_main.ads                         |    2 +-
 wisitoken_grammar_re2c.c                           | 2521 +++++++++++----
 wisitoken_grammar_re2c_c.ads                       |    2 +-
 wisitoken_grammar_runtime.adb                      | 2434 +++++++++++++-
 wisitoken_grammar_runtime.ads                      |   67 +-
 89 files changed, 25557 insertions(+), 16692 deletions(-)

diff --git a/NEWS b/NEWS
index 22fe4e4..28f737c 100644
--- a/NEWS
+++ b/NEWS
@@ -1,15 +1,48 @@
 GNU Emacs wisi NEWS -- history of user-visible changes.
 
-Copyright (C) 2018 Free Software Foundation, Inc.
+Copyright (C) 2019 Free Software Foundation, Inc.
 See the end of the file for license conditions.
 
 Please send wisi bug reports to bug-gnu-emacs@gnu.org, with
 'wisi' in the subject. If possible, use M-x report-emacs-bug.
 
 
+* wisi
+11 Jul 2019
+
+** parser process protocol version 3
+
+** User variable wisi-mckenzie-cost-limit is deleted; set
+   wisi-mckenzie-enqueue-limit instead, it gives better results.
+
+** `wisi-show-parse-errors' now shows errors in a dedicated window, so
+   it will not be resized or reused.
+
+** New grammar action `wisi-name-action'; sets a name that will be
+   fontified. This replaces the previous 'name' navigate class.
+
+** Support 'nil' as an indent argument; useful for the first token,
+   whose indent is typically unknown. Indent functions that do not
+   accumulate now only check for "nil", not "zero or nil".
+
+** New file wisi-xref.el provides a completion table built from tokens
+   marked by the new grammar action `wisi-name-action'.
+
+** The process parser checks the parser process protocol version, to
+   ensure the elisp code matches the process code.
+
+** The process parser supports passing data to a language-specific
+   elisp function, via the `language-action-table' field of the
+   `wisi-process--parser' struct.
+
+** New user option `wisi-parse-max-parallel' to set the number of
+   parallel parsers used; Java needs more than Ada.
+
 * wisi 2.1.0
 21 Mar 2019
 
+** parser process protocol version 2
+
 ** Add support for partial parsing; useful in very large files. Files
    larger than wisi-partial-parse-threshold (default 100_001) will be
    parsed partially.
@@ -23,6 +56,8 @@ Please send wisi bug reports to bug-gnu-emacs@gnu.org, with
 * wisi 2.0.1
 8 Dec 2018
 
+** parser process protocol version 1
+
 ** Assign copyright in Ada files to FSF
 
 ** Update user guide, include it in elpa package
diff --git a/README b/README
index db1af26..f7fc4eb 100644
--- a/README
+++ b/README
@@ -1,4 +1,4 @@
-Emacs wisi package 2.1.0
+Emacs wisi package 2.1.1
 
 The wisi package provides utilities for using generalized LALR parsers
 (in elisp or external processes) to do indentation, fontification, and
diff --git a/sal-ada_containers-gen_doubly_linked_lists_image.adb 
b/sal-ada_containers-gen_doubly_linked_lists_image.adb
new file mode 100644
index 0000000..a5dd042
--- /dev/null
+++ b/sal-ada_containers-gen_doubly_linked_lists_image.adb
@@ -0,0 +1,50 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Strings.Unbounded;
+function SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image
+  (Item   : in Lists.List;
+   Strict : in Boolean := False)
+  return String
+is
+   use all type Ada.Containers.Count_Type;
+   use Ada.Strings;
+   use Ada.Strings.Unbounded;
+   Result     : Unbounded_String := To_Unbounded_String ("(");
+   Need_Comma : Boolean          := False;
+begin
+   if Strict and Item.Length = 0 then
+      return "(1 .. 0 => <>)";
+
+   elsif Strict and Item.Length = 1 then
+      return "(1 => " & Element_Image (Lists.Element (Item.First)) & ")";
+
+   else
+      for El of Item loop
+         if Need_Comma then
+            Result := Result & ", ";
+         else
+            Need_Comma := True;
+         end if;
+         Result := Result & Element_Image (El);
+      end loop;
+      Result := Result & ")";
+      return To_String (Result);
+   end if;
+end SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image;
diff --git a/sal-gen_unbounded_definite_vectors-gen_image_aux.ads 
b/sal-ada_containers-gen_doubly_linked_lists_image.ads
similarity index 56%
copy from sal-gen_unbounded_definite_vectors-gen_image_aux.ads
copy to sal-ada_containers-gen_doubly_linked_lists_image.ads
index 0be7c41..39e9b9e 100644
--- a/sal-gen_unbounded_definite_vectors-gen_image_aux.ads
+++ b/sal-ada_containers-gen_doubly_linked_lists_image.ads
@@ -1,8 +1,8 @@
 --  Abstract :
 --
---  Image with auxiliary data for instantiations of parent.
+--  Image for normal Ada array types
 --
---  Copyright (C) 2018 Stephen Leake All Rights Reserved.
+--  Copyright (C) 2019 Free Software Foundation, Inc.
 --
 --  This library is free software;  you can redistribute it and/or modify it
 --  under terms of the  GNU General Public License  as published by the Free
@@ -16,8 +16,13 @@
 --  version 3.1, as published by the Free Software Foundation.
 
 pragma License (Modified_GPL);
-
+with Ada.Containers.Doubly_Linked_Lists;
 generic
-   type Aux_Data (<>) is private;
-   with function Element_Image (Item : in Element_Type; Aux : in Aux_Data) 
return String;
-function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux (Item : in Vector; 
Aux : in Aux_Data) return String;
+   type Element_Type is private;
+   with function "=" (Left, Right : Element_Type) return Boolean is <>;
+   with package Lists is new Ada.Containers.Doubly_Linked_Lists (Element_Type, 
"=");
+   with function Element_Image (Item : in Element_Type) return String;
+function SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image
+  (Item   : in Lists.List;
+   Strict : in Boolean := False)
+  return String;
diff --git a/sal-gen_unbounded_definite_vectors-gen_image_aux.ads 
b/sal-ada_containers.ads
similarity index 64%
copy from sal-gen_unbounded_definite_vectors-gen_image_aux.ads
copy to sal-ada_containers.ads
index 0be7c41..4afad1e 100644
--- a/sal-gen_unbounded_definite_vectors-gen_image_aux.ads
+++ b/sal-ada_containers.ads
@@ -1,23 +1,21 @@
---  Abstract :
---
---  Image with auxiliary data for instantiations of parent.
---
---  Copyright (C) 2018 Stephen Leake All Rights Reserved.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-generic
-   type Aux_Data (<>) is private;
-   with function Element_Image (Item : in Element_Type; Aux : in Aux_Data) 
return String;
-function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux (Item : in Vector; 
Aux : in Aux_Data) return String;
+--  Abstract :
+--
+--  Root of extensions to Ada.Containers.
+--
+--  Copyright (C) 2019 Free Software Foundation All Rights Reserved.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+package SAL.Ada_Containers is
+
+end SAL.Ada_Containers;
diff --git a/sal-gen_bounded_definite_queues.adb 
b/sal-gen_bounded_definite_queues.adb
new file mode 100644
index 0000000..61e5c12
--- /dev/null
+++ b/sal-gen_bounded_definite_queues.adb
@@ -0,0 +1,154 @@
+--  Abstract:
+--
+--  See spec.
+--
+--  Copyright (C) 2004, 2008, 2009, 2011, 2017, 2019 Free Software Foundation 
All Rights Reserved.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+--
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+package body SAL.Gen_Bounded_Definite_Queues is
+
+   --  Local subprograms
+
+   function Wrap (Queue : in Queue_Type; I : in Integer) return Integer
+   is begin
+      if I > Queue.Size then
+         return I - Queue.Size;
+      elsif I < 1 then
+         return Queue.Size + I;
+      else
+         return I;
+      end if;
+   end Wrap;
+
+   ----------
+   --  Public subprograms
+
+   function Get_Overflow_Handling (Queue : in Queue_Type) return 
Overflow_Action_Type
+   is begin
+      return Queue.Overflow_Handling;
+   end Get_Overflow_Handling;
+
+   procedure Set_Overflow_Handling (Queue : in out Queue_Type; Handling : in 
Overflow_Action_Type)
+   is begin
+      Queue.Overflow_Handling := Handling;
+   end Set_Overflow_Handling;
+
+   procedure Clear (Queue : in out Queue_Type) is
+   begin
+      Queue.Count := 0;
+   end Clear;
+
+   function Count (Queue : in Queue_Type) return Natural is
+   begin
+      return Queue.Count;
+   end Count;
+
+   function Is_Empty (Queue : in Queue_Type) return Boolean is
+   begin
+      return Queue.Count = 0;
+   end Is_Empty;
+
+   function Is_Full (Queue : in Queue_Type) return Boolean is
+   begin
+      return Queue.Count = Queue.Size;
+   end Is_Full;
+
+   function Remove (Queue : in out Queue_Type) return Item_Type
+   is begin
+      if Queue.Count = 0 then
+         raise Container_Empty;
+      end if;
+
+      return Item : constant Item_Type := Queue.Data (Queue.Head)
+      do
+         Queue.Count := Queue.Count - 1;
+
+         if Queue.Count > 0 then
+            Queue.Head := Wrap (Queue, Queue.Head + 1);
+         end if;
+      end return;
+   end Remove;
+
+   procedure Drop (Queue : in out Queue_Type)
+   is begin
+      if Queue.Count = 0 then
+         raise Container_Empty;
+      end if;
+
+      Queue.Count := Queue.Count - 1;
+
+      if Queue.Count > 0 then
+         Queue.Head := Wrap (Queue, Queue.Head + 1);
+      end if;
+   end Drop;
+
+   function Peek (Queue : in Queue_Type; N : Integer := 0) return Item_Type
+   is begin
+      if Queue.Count = 0 then
+         raise Container_Empty;
+      end if;
+
+      return Queue.Data (Wrap (Queue, Queue.Head + N));
+   end Peek;
+
+   procedure Add (Queue : in out Queue_Type; Item : in Item_Type) is
+   begin
+      if Queue.Count = Queue.Size then
+         case Queue.Overflow_Handling is
+         when Error =>
+            raise Container_Full;
+         when Overwrite =>
+            Queue.Count := Queue.Count - 1;
+            Queue.Head  := Wrap (Queue, Queue.Head + 1);
+         end case;
+      end if;
+
+      if Queue.Count = 0 then
+         Queue.Tail     := 1;
+         Queue.Head     := 1;
+         Queue.Count    := 1;
+         Queue.Data (1) := Item;
+      else
+         Queue.Tail              := Wrap (Queue, Queue.Tail + 1);
+         Queue.Data (Queue.Tail) := Item;
+         Queue.Count             := Queue.Count + 1;
+      end if;
+   end Add;
+
+   procedure Add_To_Head (Queue : in out Queue_Type; Item : in Item_Type) is
+   begin
+      if Queue.Count = Queue.Size then
+         case Queue.Overflow_Handling is
+         when Error =>
+            raise Container_Full;
+         when Overwrite =>
+            Queue.Count := Queue.Count - 1;
+            Queue.Tail  := Wrap (Queue, Queue.Tail + 1);
+         end case;
+      end if;
+
+      if Queue.Count = 0 then
+         Queue.Tail     := 1;
+         Queue.Head     := 1;
+         Queue.Count    := 1;
+         Queue.Data (1) := Item;
+      else
+         Queue.Head              := Wrap (Queue, Queue.Head - 1);
+         Queue.Data (Queue.Head) := Item;
+         Queue.Count             := Queue.Count + 1;
+      end if;
+   end Add_To_Head;
+
+end SAL.Gen_Bounded_Definite_Queues;
diff --git a/sal-gen_bounded_definite_queues.ads 
b/sal-gen_bounded_definite_queues.ads
new file mode 100644
index 0000000..0b286f8
--- /dev/null
+++ b/sal-gen_bounded_definite_queues.ads
@@ -0,0 +1,99 @@
+--  Abstract:
+--
+--  A generic queue, allowing definite non-limited item types.
+--
+--  Copyright (C) 2004, 2008, 2009, 2011, 2017, 2019 Free Software Foundation 
All Rights Reserved.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+--
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+generic
+   type Item_Type is private;
+package SAL.Gen_Bounded_Definite_Queues is
+   pragma Pure;
+
+   type Queue_Type (Size : Positive) is tagged private;
+   --  Size is maximum number of items in the queue.
+   --  Tagged to allow Object.Method syntax.
+
+   function Get_Overflow_Handling (Queue : in Queue_Type) return 
Overflow_Action_Type;
+   procedure Set_Overflow_Handling (Queue : in out Queue_Type; Handling : in 
Overflow_Action_Type);
+   --  See Add for meaning of Overflow_Handling. Default is Error.
+
+   procedure Clear (Queue : in out Queue_Type);
+   --  Empty Queue of all items.
+
+   function Count (Queue : in Queue_Type) return Natural;
+   --  Returns count of items in the Queue
+
+   function Is_Empty (Queue : in Queue_Type) return Boolean;
+   --  Returns true if no items are in Queue.
+
+   function Is_Full (Queue : in Queue_Type) return Boolean;
+   --  Returns true if Queue is full.
+
+   function Remove (Queue : in out Queue_Type) return Item_Type;
+   --  Remove head item from Queue, return it.
+   --
+   --  Raises Container_Empty if Is_Empty.
+
+   function Get (Queue : in out Queue_Type) return Item_Type renames Remove;
+
+   procedure Drop (Queue : in out Queue_Type);
+   --  Remove head item from Queue, discard it.
+   --
+   --  Raises Container_Empty if Is_Empty.
+
+   function Peek (Queue : in Queue_Type; N : Integer := 0) return Item_Type;
+   --  Return a copy of a queue item, without removing it. N = 0 is
+   --  the queue head.
+
+   procedure Add (Queue : in out Queue_Type; Item : in Item_Type);
+   --  Add Item to the tail of Queue.
+   --
+   --  If Queue is full, result depends on Queue.Overflow_Handling:
+   --
+   --  when Overwrite, an implicit Remove is done (and the data
+   --  discarded), then Add is done.
+   --
+   --  when Error, raises Container_Full.
+
+   procedure Put (Queue : in out Queue_Type; Item : in Item_Type) renames Add;
+
+   procedure Add_To_Head (Queue : in out Queue_Type; Item : in Item_Type);
+   --  Add Item to the head of Queue.
+   --
+   --  If Queue is full, result depends on Queue.Overflow_Handling:
+   --
+   --  when Overwrite, an implicit Remove is done (and the data
+   --  discarded), then Add is done.
+   --
+   --  when Error, raises Container_Full.
+
+private
+
+   type Item_Array_Type is array (Positive range <>) of Item_Type;
+
+   type Queue_Type (Size : Positive) is tagged record
+      Overflow_Handling : Overflow_Action_Type := Error;
+
+      Head  : Natural := 0;
+      Tail  : Natural := 0;
+      Count : Natural := 0;
+      Data  : Item_Array_Type (1 .. Size);
+      --  Add at Tail + 1, remove at Head. Count is current count;
+      --  easier to keep track of that than to compute Is_Empty for
+      --  each Add and Remove.
+   end record;
+
+end SAL.Gen_Bounded_Definite_Queues;
diff --git a/sal-gen_bounded_definite_vectors-gen_sorted.adb 
b/sal-gen_bounded_definite_vectors-gen_sorted.adb
index 5a8c550..b77f06c 100644
--- a/sal-gen_bounded_definite_vectors-gen_sorted.adb
+++ b/sal-gen_bounded_definite_vectors-gen_sorted.adb
@@ -2,7 +2,7 @@
 --
 --  See spec.
 --
---  Copyright (C) 2018 Free Software Foundation, Inc.
+--  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
 --
 --  This library is free software;  you can redistribute it and/or modify it
 --  under terms of the  GNU General Public License  as published by the Free
@@ -39,8 +39,9 @@ package body SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted is
    end Insert;
 
    procedure Insert
-     (Container : in out Vector;
-      New_Item  : in     Element_Type)
+     (Container       : in out Vector;
+      New_Item        : in     Element_Type;
+      Ignore_If_Equal : in     Boolean := False)
    is
       K : constant Base_Peek_Type := To_Peek_Index (Container.Last);
       J : Base_Peek_Type := K;
@@ -62,8 +63,12 @@ package body SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted is
          when Less =>
             J := J - 1;
          when Equal =>
-            --  Insert after J
-            exit;
+            if Ignore_If_Equal then
+               return;
+            else
+               --  Insert after J
+               exit;
+            end if;
          when Greater =>
             --  Insert after J
             exit;
diff --git a/sal-gen_bounded_definite_vectors-gen_sorted.ads 
b/sal-gen_bounded_definite_vectors-gen_sorted.ads
index 917a344..e6e101f 100644
--- a/sal-gen_bounded_definite_vectors-gen_sorted.ads
+++ b/sal-gen_bounded_definite_vectors-gen_sorted.ads
@@ -2,7 +2,7 @@
 --
 --  Add sorted behavior to parent.
 --
---  Copyright (C) 2018 Free Software Foundation, Inc.
+--  Copyright (C) 2018, 2019 Free Software Foundation, Inc.
 --
 --  This library is free software;  you can redistribute it and/or modify it
 --  under terms of the  GNU General Public License  as published by the Free
@@ -41,10 +41,12 @@ package SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted is
 
    not overriding
    procedure Insert
-     (Container : in out Vector;
-      New_Item  : in     Element_Type);
+     (Container       : in out Vector;
+      New_Item        : in     Element_Type;
+      Ignore_If_Equal : in     Boolean := False);
    --  Insert New_Item in sorted position. Items are sorted in increasing
    --  order according to Element_Compare. New_Item is inserted after
-   --  Equal items.
+   --  Equal items, unless Ignore_If_Equal is true, in which case
+   --  New_Item is not inserted.
 
 end SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted;
diff --git a/sal-gen_graphs.adb b/sal-gen_graphs.adb
new file mode 100644
index 0000000..284b26c
--- /dev/null
+++ b/sal-gen_graphs.adb
@@ -0,0 +1,718 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 2019 Free Software Foundation All Rights Reserved.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Strings.Unbounded;
+with Ada.Text_IO;
+with SAL.Gen_Bounded_Definite_Queues;
+with SAL.Gen_Unbounded_Definite_Stacks;
+package body SAL.Gen_Graphs is
+
+   package Vertex_Queues is new SAL.Gen_Bounded_Definite_Queues (Vertex_Index);
+   package Vertex_Stacks is new SAL.Gen_Unbounded_Definite_Stacks 
(Vertex_Index);
+
+   function Find (Data : in Edge_Data; List : in Edge_Node_Lists.List) return 
Edge_Node_Lists.Cursor
+   is begin
+      for I in List.Iterate loop
+         if Edge_Node_Lists.Element (I).Data = Data then
+            return I;
+         end if;
+      end loop;
+      return Edge_Node_Lists.No_Element;
+   end Find;
+
+   ----------
+   --  Visible subprograms
+
+   procedure Add_Edge
+     (Graph    : in out Gen_Graphs.Graph;
+      Vertex_A : in     Vertex_Index;
+      Vertex_B : in     Vertex_Index;
+      Data     : in     Edge_Data)
+   is
+      Multigraph : Boolean := False;
+
+      procedure Update_First_Last (Vertex : in Vertex_Index)
+      is
+         use all type Ada.Containers.Count_Type;
+      begin
+         if Graph.Vertices.Length = 0 then
+            Graph.Vertices.Set_First_Last (Vertex, Vertex);
+         else
+            if Vertex < Graph.Vertices.First_Index then
+               Graph.Vertices.Set_First (Vertex);
+            end if;
+            if Vertex > Graph.Vertices.Last_Index then
+               Graph.Vertices.Set_Last (Vertex);
+            end if;
+         end if;
+      end Update_First_Last;
+
+   begin
+      Update_First_Last (Vertex_A);
+      Update_First_Last (Vertex_B);
+
+      Graph.Last_Edge_ID := Graph.Last_Edge_ID + 1;
+      if (for some E of Graph.Vertices (Vertex_A) => E.Vertex_B = Vertex_B) 
then
+         Multigraph       := True;
+         Graph.Multigraph := True;
+      end if;
+
+      Graph.Vertices (Vertex_A).Append ((Graph.Last_Edge_ID, Vertex_B, 
Multigraph, Data));
+   end Add_Edge;
+
+   function Count_Nodes (Graph : in Gen_Graphs.Graph) return 
Ada.Containers.Count_Type
+   is begin
+      return Graph.Vertices.Length;
+   end Count_Nodes;
+
+   function Count_Edges (Graph : in Gen_Graphs.Graph) return 
Ada.Containers.Count_Type
+   is
+      use Ada.Containers;
+      Result : Count_Type := 0;
+   begin
+      for Edges of Graph.Vertices loop
+         Result := Result + Edges.Length;
+      end loop;
+      return Result;
+   end Count_Edges;
+
+   function Multigraph (Graph : in Gen_Graphs.Graph) return Boolean
+   is begin
+      return Graph.Multigraph;
+   end Multigraph;
+
+   function "+" (Right : in Edge_Item) return Edge_Lists.List
+   is
+      use Edge_Lists;
+   begin
+      return Result : List do
+         Append (Result, Right);
+      end return;
+   end "+";
+
+   function Edges (Graph : in Gen_Graphs.Graph; Vertex : in Vertex_Index) 
return Edge_Lists.List
+   is begin
+      return Result : Edge_Lists.List do
+         for E of Graph.Vertices (Vertex) loop
+            Result.Append ((E.ID, E.Data));
+         end loop;
+      end return;
+   end Edges;
+
+   function Image (Item : in Path) return String
+   is
+      use Ada.Strings.Unbounded;
+
+      Result : Unbounded_String := To_Unbounded_String ("(");
+   begin
+      for I in Item'Range loop
+         Result := Result & Trimmed_Image (Item (I).Vertex) & " " &
+           Image ((if I = Item'Last then Item (Item'First).Edges else Item (I 
+ 1).Edges)) & " -> ";
+      end loop;
+      Result := Result & ")";
+      return To_String (Result);
+   end Image;
+
+   function "<" (Left, Right : in Path) return Boolean
+   is begin
+      for I in Left'Range loop
+         if I > Right'Last then
+            return False;
+         elsif Left (I).Vertex < Right (I).Vertex then
+            return True;
+         elsif Left (I).Vertex > Right (I).Vertex then
+            return False;
+         else
+            --  =; check remaining elements
+            null;
+         end if;
+      end loop;
+
+      if Left'Last < Right'Last then
+         return True;
+      else
+         --  All =
+         return False;
+      end if;
+   end "<";
+
+   function Find_Paths
+     (Graph : in out Gen_Graphs.Graph;
+      From  : in     Vertex_Index;
+      To    : in     Edge_Data)
+     return Path_Arrays.Vector
+   is
+      Vertex_Queue  : Vertex_Queues.Queue_Type
+        (Size => Integer (Graph.Vertices.Last_Index - 
Graph.Vertices.First_Index + 1));
+
+      type Colors is (White, Gray, Black);
+
+      type Aux_Node is record
+         Color       : Colors            := Colors'First;
+         D           : Natural           := Natural'Last;
+         Parent      : Vertex_Index'Base := Invalid_Vertex;
+         Parent_Set  : Boolean           := False;
+         Parent_Edge : Edge_Node_Lists.Cursor := Edge_Node_Lists.No_Element;
+      end record;
+
+      package Aux_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Vertex_Index, Aux_Node, (others => <>));
+      Aux : Aux_Arrays.Vector;
+
+      function Build_Path
+        (Tail_Vertex : in Vertex_Index;
+         Tail_Edge   : in Edge_Node_Lists.Cursor)
+        return Path
+      is
+      begin
+         return Result : Path (1 .. Aux (Tail_Vertex).D + 1)
+         do
+            declare
+               use Edge_Node_Lists;
+               V_Index   : Vertex_Index := Tail_Vertex;
+               Last_Edge : Cursor       := Tail_Edge;
+            begin
+               for I in reverse 1 .. Result'Length loop
+                  declare
+                     V : Aux_Node renames Aux (V_Index);
+                  begin
+                     if Last_Edge = No_Element then
+                        Result (I) := (V_Index, Edge_Lists.Empty_List);
+                     else
+                        Result (I) := (V_Index, +(Element (Last_Edge).ID, 
Element (Last_Edge).Data));
+                     end if;
+
+                     if V.Parent_Set then
+                        Last_Edge := V.Parent_Edge;
+                        V_Index   := V.Parent;
+                     end if;
+                  end;
+               end loop;
+            end;
+         end return;
+      end Build_Path;
+
+      Result_List : Path_Arrays.Vector;
+      Result_Edge : Edge_Node_Lists.Cursor;
+   begin
+      --  [1] figure 22.3 breadth-first search; 'From' = s.
+
+      Aux.Set_First_Last (Graph.Vertices.First_Index, 
Graph.Vertices.Last_Index);
+
+      for I in Aux.First_Index .. Aux.Last_Index loop
+         if I = From then
+            Aux (I).Color      := Gray;
+            Aux (I).D          := 0;
+            Aux (I).Parent_Set := False;
+
+         else
+            Aux (I).Color      := White;
+            Aux (I).D          := Natural'Last;
+            Aux (I).Parent_Set := False;
+         end if;
+      end loop;
+
+      Vertex_Queue.Put (From);
+
+      while not Vertex_Queue.Is_Empty loop
+         declare
+            U_Index : constant Vertex_Index := Vertex_Queue.Get;
+            U       : Aux_Node renames Aux (U_Index);
+         begin
+            Edges :
+            for C in Graph.Vertices (U_Index).Iterate loop
+               declare
+                  use all type Edge_Node_Lists.Cursor;
+                  V_Index : constant Vertex_Index := Edge_Node_Lists.Element 
(C).Vertex_B;
+                  V       : Aux_Node renames Aux (V_Index);
+               begin
+                  if V.Color = White then
+                     V.Color       := Gray;
+                     V.D           := U.D + 1;
+                     V.Parent      := U_Index;
+                     V.Parent_Edge := C;
+                     V.Parent_Set  := True;
+
+                     Result_Edge := Find (To, Graph.Vertices (V_Index));
+                     if Result_Edge /= Edge_Node_Lists.No_Element then
+                        Result_List.Append (Build_Path (V_Index, Result_Edge));
+                     end if;
+
+                     Vertex_Queue.Put (V_Index);
+                  end if;
+               end;
+            end loop Edges;
+            U.Color := Black;
+         end;
+      end loop;
+      return Result_List;
+   end Find_Paths;
+
+   function Find_Cycles_Tiernan (Graph : in Gen_Graphs.Graph)
+     return Path_Arrays.Vector
+   is
+      --  Implements [2] "Algorithm EC"
+      --
+      --  vertex 0 = Invalid_Vertex
+      --  vertex 1 = Graph.Vertices.First_Index
+      --  vertex N = Graph.Vertices.Last_Index
+
+      First : Vertex_Index renames Graph.Vertices.First_Index;
+      Last  : Vertex_Index renames Graph.Vertices.Last_Index;
+
+      G : Vertex_Arrays.Vector renames Graph.Vertices;
+      P : Path (1 .. Integer (Last - First + 1));
+      K : Positive := 1; -- ie P_Last
+
+      type H_Row is array (G.First_Index .. G.Last_Index) of Vertex_Index'Base;
+      H : array (G.First_Index .. G.Last_Index) of H_Row := (others => (others 
=> Invalid_Vertex));
+
+      Next_Vertex_Found : Boolean;
+
+      Result : Path_Arrays.Vector;
+
+      function Contains (P : in Path; V : in Vertex_Index) return Boolean
+      is (for some N of P => N.Vertex = V);
+
+      function Contains (Row : in H_Row; V : in Vertex_Index) return Boolean
+      is (for some N of Row => N = V);
+
+      function Contains (Edges : in Edge_Lists.List; ID : in Edge_ID) return 
Boolean
+        is (for some E of Edges => E.ID = ID);
+
+      procedure Add_Alternate_Edges (P : in out Path)
+      is
+         function Dec (I : in Positive) return Positive
+           is (if I = P'First then P'Last else I - 1);
+      begin
+         for I in P'Range loop
+            for New_Edge of G (P (Dec (I)).Vertex) loop
+               if New_Edge.Vertex_B = P (I).Vertex and (not Contains (P 
(I).Edges, New_Edge.ID)) then
+                  P (I).Edges.Append ((New_Edge.ID, New_Edge.Data));
+               end if;
+            end loop;
+         end loop;
+      end Add_Alternate_Edges;
+
+   begin
+      P (1) := (First, Edge_Lists.Empty_List);
+
+      All_Initial_Vertices :
+      loop
+         Explore_Vertex :
+         loop
+            Path_Extension :
+            loop  -- EC2 Path Extension
+               Next_Vertex_Found := False;
+
+               Find_Next_Vertex :
+               for Edge of G (P (K).Vertex) loop
+                  declare
+                     Next_Vertex : constant Vertex_Index := Edge.Vertex_B; -- 
ie G[P[k],j]
+                  begin
+                     if Next_Vertex > P (1).Vertex and -- (1)
+                       (not Contains (P, Next_Vertex)) and -- (2)
+                       (not Contains (H (P (K).Vertex), Next_Vertex))
+                     then
+                        K     := K + 1;
+                        P (K) := (Next_Vertex, +(Edge.ID, Edge.Data));
+
+                        Next_Vertex_Found := True;
+                        exit Find_Next_Vertex;
+                     end if;
+                  end;
+               end loop Find_Next_Vertex;
+
+               exit Path_Extension when not Next_Vertex_Found;
+            end loop Path_Extension;
+
+            --  EC3 Circuit Confirmation
+            for Edge of G (P (K).Vertex) loop
+               if Edge.Vertex_B = P (1).Vertex then
+                  P (1).Edges := +(Edge.ID, Edge.Data);
+                  if Graph.Multigraph then
+                     Add_Alternate_Edges (P (1 .. K));
+                  end if;
+                  Result.Append (P (1 .. K));
+                  exit;
+               end if;
+            end loop;
+
+            --  EC4 Vertex Closure
+            exit Explore_Vertex when K = 1;
+
+            H (P (K).Vertex) := (others => Invalid_Vertex);
+            for M in H (P (K - 1).Vertex)'Range loop
+               if H (P (K - 1).Vertex)(M) = Invalid_Vertex then
+                  H (P (K - 1).Vertex)(M) := P (K).Vertex;
+                  P (K) := (Invalid_Vertex, Edge_Lists.Empty_List);
+                  exit;
+               end if;
+            end loop;
+            K := K - 1;
+         end loop Explore_Vertex;
+
+         --  EC5 Advance Initial Index
+         exit All_Initial_Vertices when P (1).Vertex = 
Graph.Vertices.Last_Index;
+
+         P (1) := (P (1).Vertex + 1, Edge_Lists.Empty_List);
+         pragma Assert (K = 1);
+         H := (others => (others => Invalid_Vertex));
+      end loop All_Initial_Vertices;
+
+      --  EC6 Terminate
+      return Result;
+   end Find_Cycles_Tiernan;
+
+   function Find_Cycles (Graph : in Gen_Graphs.Graph) return Path_Arrays.Vector
+   is
+      --  Implements Circuit-Finding Algorithm from [3]
+
+      use all type Ada.Containers.Count_Type;
+
+      pragma Warnings (Off, """Edited_Graph"" is not modified, could be 
declared constant");
+      Edited_Graph : Gen_Graphs.Graph := Graph;
+
+      Result : Path_Arrays.Vector;
+
+      A_K     : Adjacency_Structures.Vector;
+      B       : Adjacency_Structures.Vector;
+      Blocked : array (Graph.Vertices.First_Index .. 
Graph.Vertices.Last_Index) of Boolean := (others => False);
+
+      Stack : Vertex_Stacks.Stack;
+      S     : Vertex_Index := Graph.Vertices.First_Index;
+
+      Dummy : Boolean;
+      pragma Unreferenced (Dummy);
+
+      function Circuit (V : in Vertex_Index) return Boolean
+      is
+         F : Boolean := False;
+
+         procedure Unblock (U : in Vertex_Index)
+         is begin
+            Blocked (U) := False;
+            declare
+               use Vertex_Lists;
+               Cur  : Cursor := B (U).First;
+               Temp : Cursor;
+               W    : Vertex_Index;
+            begin
+               loop
+                  exit when not Has_Element (Cur);
+                  W := Element (Cur);
+                  Temp := Cur;
+                  Next (Cur);
+                  B (U).Delete (Temp);
+                  if Blocked (W) then
+                     Unblock (W);
+                  end if;
+               end loop;
+            end;
+         end Unblock;
+
+         procedure Add_Result
+         is
+            Cycle : Path (1 .. Integer (Stack.Depth));
+         begin
+            for I in 1 .. Stack.Depth loop
+               Cycle (Integer (Stack.Depth - I + 1)) := (Stack.Peek (I), 
Edge_Lists.Empty_List);
+               --  We add the edge info later, after finding all the cycles.
+            end loop;
+            Result.Append (Cycle);
+            if Trace > 0 then
+               Ada.Text_IO.Put_Line ("cycle " & Image (Cycle));
+            end if;
+         end Add_Result;
+
+      begin
+         if Trace > 0 then
+            Ada.Text_IO.Put_Line ("circuit start" & V'Image);
+         end if;
+
+         Stack.Push (V);
+         Blocked (V) := True;
+         if V in A_K.First_Index .. A_K.Last_Index then
+            for W of A_K (V) loop
+               if W = S then
+                  Add_Result;
+                  F := True;
+               elsif not Blocked (W) then
+                  if Circuit (W) then
+                     F := True;
+                  end if;
+               end if;
+            end loop;
+         end if;
+         if F then
+            Unblock (V);
+         else
+            if V in A_K.First_Index .. A_K.Last_Index then
+               for W of A_K (V) loop
+                  if (for all V1 of B (W) => V /= V1) then
+                     B (W).Append (V);
+                  end if;
+               end loop;
+            end if;
+         end if;
+         Stack.Pop;
+         if Trace > 0 then
+            Ada.Text_IO.Put_Line ("circuit finish" & V'Image);
+         end if;
+         return F;
+      end Circuit;
+
+   begin
+      --  [3] restricts the graph to not have loops (edge v-v) or multiple
+      --  edges between two nodes. So we first delete any such edges.
+      Delete_Loops_Multigraph :
+      for V in Edited_Graph.Vertices.First_Index .. 
Edited_Graph.Vertices.Last_Index loop
+         declare
+            use Edge_Node_Lists;
+            Cur        : Cursor  := Edited_Graph.Vertices (V).First;
+            Temp       : Cursor;
+            Found_Loop : Boolean := False;
+         begin
+            loop
+               exit when not Has_Element (Cur);
+               if Element (Cur).Vertex_B = V then
+                  if not Found_Loop then
+                     --  This is a cycle we want in the result. Edge data is 
added to all
+                     --  cycles later.
+                     Result.Append (Path'(1 => (V, Edge_Lists.Empty_List)));
+                     Found_Loop := True;
+                  end if;
+                  Temp := Cur;
+                  Next (Cur);
+                  Edited_Graph.Vertices (V).Delete (Temp);
+               elsif Element (Cur).Multigraph then
+                  --  These will be added back from Graph after we find all 
cycles.
+                  Temp := Cur;
+                  Next (Cur);
+                  Edited_Graph.Vertices (V).Delete (Temp);
+               else
+                  Next (Cur);
+               end if;
+            end loop;
+         end;
+      end loop Delete_Loops_Multigraph;
+
+      B.Set_First_Last (Graph.Vertices.First_Index, Graph.Vertices.Last_Index);
+
+      --  Start of body of Circuit-Finding Algorithm from [3]
+      loop
+         exit when S = Graph.Vertices.Last_Index;
+         declare
+            use Component_Lists;
+            Subgraph         : Adjacency_Structures.Vector;
+            Components       : Component_Lists.List;
+            Cur              : Component_Lists.Cursor;
+            Least_Vertex_Cur : Component_Lists.Cursor;
+            Least_Vertex_V   : Vertex_Index := Vertex_Index'Last;
+
+            function Delete_Edges (Edges : in Edge_Node_Lists.List) return 
Vertex_Lists.List
+            is begin
+               return Result : Vertex_Lists.List do
+                  for Edge of Edges loop
+                     if Edge.Vertex_B >= S then
+                        Result.Append (Edge.Vertex_B);
+                     end if;
+                  end loop;
+               end return;
+            end Delete_Edges;
+         begin
+            Subgraph.Set_First_Last (S, Edited_Graph.Vertices.Last_Index);
+            for V in S .. Edited_Graph.Vertices.Last_Index loop
+               Subgraph (V) := Delete_Edges (Edited_Graph.Vertices (V));
+            end loop;
+
+            Components := Strongly_Connected_Components (Subgraph, 
Non_Trivial_Only => True);
+            Cur        := Components.First;
+            loop
+               exit when not Has_Element (Cur);
+
+               if Element (Cur).Length > 1 then
+                  declare
+                     Comp : Vertex_Lists.List renames 
Components.Constant_Reference (Cur);
+                  begin
+                     for W of Comp loop
+                        if W < Least_Vertex_V then
+                           Least_Vertex_Cur := Cur;
+                           Least_Vertex_V   := W;
+                        end if;
+                     end loop;
+                  end;
+               end if;
+               Next (Cur);
+            end loop;
+
+            A_K.Clear;
+            if Has_Element (Least_Vertex_Cur) then
+               declare
+                  Component : Vertex_Lists.List renames Components 
(Least_Vertex_Cur);
+                  Min : Vertex_Index := Vertex_Index'Last;
+                  Max : Vertex_Index := Vertex_Index'First;
+               begin
+                  if Trace > 0 then
+                     Ada.Text_IO.Put_Line ("strong component " & 
Least_Vertex_V'Image);
+                     Ada.Text_IO.Put_Line (Image (Component));
+                  end if;
+                  for V of Component loop
+                     if Min > V then
+                        Min := V;
+                     end if;
+                     if Max < V then
+                        Max := V;
+                     end if;
+                  end loop;
+                  A_K.Set_First_Last (Min, Max);
+                  for V of Component loop
+                     for Edge of Edited_Graph.Vertices (V) loop
+                        A_K (V).Append (Edge.Vertex_B);
+                     end loop;
+                  end loop;
+               end;
+            end if;
+         end;
+
+         if A_K.Length > 0 then
+            S := A_K.First_Index;
+            for I in A_K.First_Index .. A_K.Last_Index loop
+               Blocked (I) := False;
+               B (I).Clear;
+            end loop;
+            Dummy := Circuit (S);
+            S := S + 1;
+         else
+            S := Graph.Vertices.Last_Index;
+         end if;
+      end loop;
+
+      --  Add edge data.
+      for Cycle of Result loop
+         for I in Cycle'First .. Cycle'Last loop
+            declare
+               Prev_I : constant Positive := (if I = Cycle'First then 
Cycle'Last else I - 1);
+            begin
+               for Edge of Graph.Vertices (Cycle (Prev_I).Vertex) loop
+                  if Cycle (I).Vertex = Edge.Vertex_B then
+                     Cycle (I).Edges.Append ((Edge.ID, Edge.Data));
+                  end if;
+               end loop;
+            end;
+         end loop;
+      end loop;
+      return Result;
+   end Find_Cycles;
+
+   function Loops (Graph : in Gen_Graphs.Graph) return Vertex_Lists.List
+   is begin
+      return Result : Vertex_Lists.List do
+         for V in Graph.Vertices.First_Index .. Graph.Vertices.Last_Index loop
+            for Edge of Graph.Vertices (V) loop
+               if V = Edge.Vertex_B then
+                  Result.Append (V);
+                  exit;
+               end if;
+            end loop;
+         end loop;
+      end return;
+   end Loops;
+
+   function To_Adjancency (Graph : in Gen_Graphs.Graph) return 
Adjacency_Structures.Vector
+   is
+      function To_Vertex_List (Edges : in Edge_Node_Lists.List) return 
Vertex_Lists.List
+      is begin
+         return Result : Vertex_Lists.List do
+            for Edge of Edges loop
+               Result.Append (Edge.Vertex_B);
+            end loop;
+         end return;
+      end To_Vertex_List;
+   begin
+      return Result : Adjacency_Structures.Vector do
+         Result.Set_First_Last (Graph.Vertices.First_Index, 
Graph.Vertices.Last_Index);
+         for V in Graph.Vertices.First_Index .. Graph.Vertices.Last_Index loop
+            Result (V) := To_Vertex_List (Graph.Vertices (V));
+         end loop;
+      end return;
+   end To_Adjancency;
+
+   function Strongly_Connected_Components
+     (Graph            : in Adjacency_Structures.Vector;
+      Non_Trivial_Only : in Boolean := False)
+     return Component_Lists.List
+   is
+      --  Implements [4] section 4.
+
+      Low_Link : array (Graph.First_Index .. Graph.Last_Index) of 
Vertex_Index'Base := (others => Invalid_Vertex);
+
+      Number : array (Graph.First_Index .. Graph.Last_Index) of 
Vertex_Index'Base := (others => Invalid_Vertex);
+      --  Number is the order visited in the depth-first search.
+
+      Points : Vertex_Stacks.Stack;
+
+      I : Vertex_Index'Base := Graph.First_Index - 1;
+
+      Result : Component_Lists.List;
+
+      procedure Strong_Connect (V : in Vertex_Index)
+      is begin
+         I            := I + 1;
+         Number (V)   := I;
+         Low_Link (V) := I;
+         Points.Push (V);
+
+         for W of Graph (V) loop
+            if Number (W) = Invalid_Vertex then
+               --  (v, w) is a tree arc
+               Strong_Connect (W);
+               Low_Link (V) := Vertex_Index'Min (Low_Link (V), Low_Link (W));
+
+            elsif Number (W) < Number (V) then
+               --  (v, w) is a frond or cross-link
+               if (for some P of Points => P = W) then
+                  Low_Link (V) := Vertex_Index'Min (Low_Link (V), Low_Link 
(W));
+               end if;
+            end if;
+         end loop;
+         if Low_Link (V) = Number (V) then
+            --  v is the root of a component
+            declare
+               use all type Ada.Containers.Count_Type;
+               Component : Vertex_Lists.List;
+            begin
+               while (not Points.Is_Empty) and then Number (Points.Peek) >= 
Number (V) loop
+                  Component.Append (Points.Pop);
+               end loop;
+               if (not Non_Trivial_Only) or Component.Length > 1 then
+                  Result.Append (Component);
+               end if;
+            end;
+         end if;
+      end Strong_Connect;
+   begin
+      for W in Graph.First_Index .. Graph.Last_Index loop
+         if Number (W) = Invalid_Vertex then
+            Strong_Connect (W);
+         end if;
+      end loop;
+      return Result;
+   end Strongly_Connected_Components;
+
+end SAL.Gen_Graphs;
diff --git a/sal-gen_graphs.ads b/sal-gen_graphs.ads
new file mode 100644
index 0000000..9a66f5c
--- /dev/null
+++ b/sal-gen_graphs.ads
@@ -0,0 +1,190 @@
+--  Abstract :
+--
+--  Type and operations for graphs.
+--
+--  References:
+--
+--  [1] Introduction to Algorithms, Thomas H. Cormen, Charles E.
+--  Leiserson, Ronald L. Rivest, Clifford Stein.
+--
+--  [2] "An Efficient Search Algorithm to Find the Elementary Circuits
+--  of a Graph", James C. Tiernan, Communications of the ACM Volume 13
+--  Number 12 December 1970.
+--  
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.516.9454&rep=rep1&type=pdf
+--
+--  [3] "Finding all the Elementary Circuits of a Directed Graph",
+--  Donald B. Johnson, SIAM J. Comput. Vol 4, No. 1, March 1975.
+--  https://epubs.siam.org/doi/abs/10.1137/0204007
+--
+--  [4] "Depth-First Search and Linear Graph Algorithms", Robert
+--  Tarjan, SIAM J. Comput. Vol. 1, No 2, June 1972.
+--  https://epubs.siam.org/doi/abs/10.1137/0201010
+--
+--  Copyright (C) 2017, 2019 Free Software Foundation All Rights Reserved.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Containers.Doubly_Linked_Lists;
+with Ada.Containers.Indefinite_Vectors;
+with SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image;
+with SAL.Gen_Trimmed_Image;
+with SAL.Gen_Unbounded_Definite_Vectors;
+generic
+   type Edge_Data is private;
+   Default_Edge_Data : in Edge_Data;
+   type Vertex_Index is range <>;
+   Invalid_Vertex : in Vertex_Index'Base;
+
+   type Path_Index is range <>;
+
+   with function Edge_Image (Item : in Edge_Data) return String;
+
+package SAL.Gen_Graphs is
+
+   type Graph is tagged private;
+
+   procedure Add_Edge
+     (Graph    : in out Gen_Graphs.Graph;
+      Vertex_A : in     Vertex_Index;
+      Vertex_B : in     Vertex_Index;
+      Data     : in     Edge_Data);
+   --  Adds a directed edge from Vertex_A to Vertex_B.
+
+   function Count_Nodes (Graph : in Gen_Graphs.Graph) return 
Ada.Containers.Count_Type;
+   function Count_Edges (Graph : in Gen_Graphs.Graph) return 
Ada.Containers.Count_Type;
+
+   function Multigraph (Graph : in Gen_Graphs.Graph) return Boolean;
+   --  If more than one edge is added between two vertices, the graph is
+   --  a multigraph. The edges are given separate identifiers internally.
+
+   Multigraph_Error : exception;
+
+   type Base_Edge_ID is range 0 .. Integer'Last;
+   subtype Edge_ID is Base_Edge_ID range 1 .. Base_Edge_ID'Last;
+   Invalid_Edge_ID : constant Base_Edge_ID := 0;
+   --  Edge ids are unique graph-wide, assigned by Add_Edge.
+
+   type Edge_Item is record
+      ID   : Base_Edge_ID := Invalid_Edge_ID;
+      Data : Edge_Data    := Default_Edge_Data;
+   end record;
+   function Image (Item : in Edge_Item) return String
+     is (Edge_Image (Item.Data));
+
+   package Edge_Lists is new Ada.Containers.Doubly_Linked_Lists (Edge_Item);
+
+   function "+" (Right : in Edge_Item) return Edge_Lists.List;
+
+   function Edges (Graph : in Gen_Graphs.Graph; Vertex : in Vertex_Index) 
return Edge_Lists.List;
+   --  All edges from Vertex, as set by Add_Edge.
+
+   function Image is new SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image
+     (Element_Type  => Edge_Item,
+      Lists         => Edge_Lists,
+      Element_Image => Image);
+
+   type Path_Item is record
+      Vertex : Vertex_Index'Base := Invalid_Vertex;
+      Edges  : Edge_Lists.List;
+      --  Edges describe the edges leading from the previous vertex
+      --  in the path to Vertex. If this is the first vertex in an open
+      --  path, Edges is empty. If it is the first vertex in a
+      --  cycle, the edge are from the last vertex in the cycle.
+   end record;
+
+   type Path is array (Positive range <>) of Path_Item;
+
+   function Image (Item : in Path) return String;
+   --  For trace, debugging.
+
+   package Path_Arrays is new Ada.Containers.Indefinite_Vectors (Path_Index, 
Path);
+
+   function "<" (Left, Right : in Path) return Boolean;
+
+   package Sort_Paths is new Path_Arrays.Generic_Sorting;
+
+   function Find_Paths
+     (Graph : in out Gen_Graphs.Graph;
+      From  : in     Vertex_Index;
+      To    : in     Edge_Data)
+     return Path_Arrays.Vector;
+   --  Return all non-cyclic paths starting at From that lead to a To
+   --  edge, using algorithm [1]. First entry in each item in result is
+   --  From, with first edge. Last entry in result contains edge data for
+   --  To.
+   --
+   --  Raises Multigraph_Error if Graph is a multigraph.
+
+   function Find_Cycles_Tiernan (Graph : in Gen_Graphs.Graph) return 
Path_Arrays.Vector;
+   --  Return all cyclic paths in Graph, using algorithm [2] extended for
+   --  multigraphs.
+   --
+   --  Time complexity is exponential in the number of nodes. Used in
+   --  unit tests for Find_Cycles_Johnson, since [2] is easier to
+   --  implement.
+
+   function Find_Cycles (Graph : in Gen_Graphs.Graph) return 
Path_Arrays.Vector;
+   --  Return all cyclic paths in Graph, using algorithm [3] extended for
+   --  multigraphs.
+   --
+   --  Time complexity is linear in the number of nodes and edges.
+
+   package Vertex_Lists is new Ada.Containers.Doubly_Linked_Lists 
(Vertex_Index);
+   function Trimmed_Image is new SAL.Gen_Trimmed_Image (Vertex_Index);
+   function Image is new SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image
+     (Vertex_Index, "=", Vertex_Lists, Trimmed_Image);
+
+   function Loops (Graph : in Gen_Graphs.Graph) return Vertex_Lists.List;
+   --  List of vertices that have an edge to themselves.
+
+   package Adjacency_Structures is new SAL.Gen_Unbounded_Definite_Vectors
+     (Vertex_Index, Vertex_Lists.List, Vertex_Lists.Empty_List);
+   --  Graphs with no Edge_ID or Edge_Data; useful as intermediate results.
+
+   function To_Adjancency (Graph : in Gen_Graphs.Graph) return 
Adjacency_Structures.Vector;
+
+   package Component_Lists is new Ada.Containers.Doubly_Linked_Lists 
(Vertex_Lists.List, Vertex_Lists."=");
+
+   function Strongly_Connected_Components
+     (Graph            : in Adjacency_Structures.Vector;
+      Non_Trivial_Only : in Boolean := False)
+     return Component_Lists.List;
+   --  Find strongly connected components of Graph, using algorithm in [4].
+   --  If Non_Trivial_Only, don't include single-vertex components.
+
+   Trace : Integer := 0;
+   --  Some bodies output debug info to Text_IO.Current_Output for
+   --  non-zero values of Trace.
+private
+
+   type Edge_Node is record
+      --  Edge is from vertex contaning this Node to Vertex_B
+      ID         : Edge_ID;
+      Vertex_B   : Vertex_Index;
+      Multigraph : Boolean; -- Same Vertex_B as another edge in same vertex.
+      Data       : Edge_Data;
+   end record;
+
+   package Edge_Node_Lists is new Ada.Containers.Doubly_Linked_Lists 
(Edge_Node);
+
+   package Vertex_Arrays is new SAL.Gen_Unbounded_Definite_Vectors
+     (Vertex_Index, Edge_Node_Lists.List, Edge_Node_Lists.Empty_List);
+
+   type Graph is tagged record
+      Last_Edge_ID : Base_Edge_ID := Invalid_Edge_ID;
+      Multigraph   : Boolean      := False;
+      Vertices     : Vertex_Arrays.Vector;
+   end record;
+
+end SAL.Gen_Graphs;
diff --git a/sal-gen_unbounded_definite_stacks.adb 
b/sal-gen_unbounded_definite_stacks.adb
index 6b5337d..0a54eab 100644
--- a/sal-gen_unbounded_definite_stacks.adb
+++ b/sal-gen_unbounded_definite_stacks.adb
@@ -2,7 +2,7 @@
 --
 --  see spec
 --
---  Copyright (C) 1998, 2003, 2009, 2015, 2017, 2018 Free Software Foundation, 
Inc.
+--  Copyright (C) 1998, 2003, 2009, 2015, 2017 - 2019 Free Software 
Foundation, Inc.
 --
 --  SAL is free software; you can redistribute it and/or modify it
 --  under terms of the GNU General Public License as published by the
@@ -175,4 +175,36 @@ package body SAL.Gen_Unbounded_Definite_Stacks is
          Dummy => 1);
    end Constant_Ref;
 
+   function Constant_Ref
+     (Container : aliased in Stack'Class;
+      Position  :         in Cursor)
+     return Constant_Ref_Type
+   is begin
+      return
+        (Element => Container.Data (Container.Top - Position.Ptr + 1)'Access,
+         Dummy => 1);
+   end Constant_Ref;
+
+   function Has_Element (Position : in Cursor) return Boolean
+   is begin
+      return Position.Container.Depth >= Position.Ptr;
+   end Has_Element;
+
+   function Iterate (Container : aliased in Stack) return 
Iterator_Interfaces.Forward_Iterator'Class
+   is begin
+      return Iterator'(Container => Container'Unrestricted_Access);
+   end Iterate;
+
+   overriding function First (Object : Iterator) return Cursor
+   is begin
+      return (Object.Container, 1);
+   end First;
+
+   overriding function Next (Object : in Iterator; Position : in Cursor) 
return Cursor
+   is
+      pragma Unreferenced (Object);
+   begin
+      return (Position.Container, Position.Ptr + 1);
+   end Next;
+
 end SAL.Gen_Unbounded_Definite_Stacks;
diff --git a/sal-gen_unbounded_definite_stacks.ads 
b/sal-gen_unbounded_definite_stacks.ads
index 954be19..cb54a24 100644
--- a/sal-gen_unbounded_definite_stacks.ads
+++ b/sal-gen_unbounded_definite_stacks.ads
@@ -23,6 +23,7 @@
 pragma License (Modified_GPL);
 
 with Ada.Finalization;
+with Ada.Iterator_Interfaces;
 with Ada.Unchecked_Deallocation;
 generic
    type Element_Type is private;
@@ -32,7 +33,9 @@ package SAL.Gen_Unbounded_Definite_Stacks is
 
    type Stack is new Ada.Finalization.Controlled with private
    with
-     Constant_Indexing => Constant_Ref;
+     Constant_Indexing => Constant_Ref,
+     Default_Iterator  => Iterate,
+     Iterator_Element  => Element_Type;
 
    Empty_Stack : constant Stack;
 
@@ -115,6 +118,20 @@ package SAL.Gen_Unbounded_Definite_Stacks is
      return Constant_Ref_Type;
    pragma Inline (Constant_Ref);
 
+   type Cursor is private;
+
+   function Constant_Ref
+     (Container : aliased in Stack'Class;
+      Position  :         in Cursor)
+     return Constant_Ref_Type;
+   pragma Inline (Constant_Ref);
+
+   function Has_Element (Position : in Cursor) return Boolean;
+
+   package Iterator_Interfaces is new Ada.Iterator_Interfaces (Cursor, 
Has_Element);
+
+   function Iterate (Container : aliased in Stack) return 
Iterator_Interfaces.Forward_Iterator'Class;
+
 private
 
    type Element_Array is array (Peek_Type range <>) of aliased Element_Type;
@@ -129,6 +146,24 @@ private
       --  Data (1 .. Last_Index) has been set at some point.
    end record;
 
+   type Stack_Access is access all Stack;
+
    Empty_Stack : constant Stack := (Ada.Finalization.Controlled with 
Invalid_Peek_Index, null);
 
+   type Cursor is record
+      Container : Stack_Access;
+      Ptr       : Peek_Type;
+   end record;
+
+   type Iterator is new Iterator_Interfaces.Forward_Iterator with
+   record
+      Container : Stack_Access;
+   end record;
+
+   overriding function First (Object : Iterator) return Cursor;
+
+   overriding function Next
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
 end SAL.Gen_Unbounded_Definite_Stacks;
diff --git a/sal-gen_unbounded_definite_vectors-gen_image_aux.adb 
b/sal-gen_unbounded_definite_vectors-gen_image_aux.adb
index c498e0e..e0d90a6 100644
--- a/sal-gen_unbounded_definite_vectors-gen_image_aux.adb
+++ b/sal-gen_unbounded_definite_vectors-gen_image_aux.adb
@@ -2,7 +2,7 @@
 --
 --  See spec.
 --
---  Copyright (C) 2018 Stephen Leake All Rights Reserved.
+--  Copyright (C) 2018 - 2019 Stephen Leake All Rights Reserved.
 --
 --  This library is free software;  you can redistribute it and/or modify it
 --  under terms of the  GNU General Public License  as published by the Free
@@ -18,7 +18,11 @@
 pragma License (Modified_GPL);
 
 with Ada.Strings.Unbounded;
-function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux (Item : in Vector; 
Aux : in Aux_Data) return String
+function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux
+  (Item        : in Vector;
+   Aux         : in Aux_Data;
+   Association : in Boolean := False)
+  return String
 is
    use Ada.Strings.Unbounded;
    Result : Unbounded_String        := To_Unbounded_String ("(");
@@ -26,6 +30,9 @@ is
    Last   : constant Base_Peek_Type := To_Peek_Type (Item.Last_Index);
 begin
    for I in First .. Last loop
+      if Association then
+         Result := Result & Index_Trimmed_Image (To_Index_Type (I)) & " => ";
+      end if;
       Result := Result & Element_Image (Item.Elements (I), Aux);
       if I /= Last then
          Result := Result & ", ";
diff --git a/sal-gen_unbounded_definite_vectors-gen_image_aux.ads 
b/sal-gen_unbounded_definite_vectors-gen_image_aux.ads
index 0be7c41..eb8a89a 100644
--- a/sal-gen_unbounded_definite_vectors-gen_image_aux.ads
+++ b/sal-gen_unbounded_definite_vectors-gen_image_aux.ads
@@ -2,7 +2,7 @@
 --
 --  Image with auxiliary data for instantiations of parent.
 --
---  Copyright (C) 2018 Stephen Leake All Rights Reserved.
+--  Copyright (C) 2018 - 2019 Stephen Leake All Rights Reserved.
 --
 --  This library is free software;  you can redistribute it and/or modify it
 --  under terms of the  GNU General Public License  as published by the Free
@@ -19,5 +19,10 @@ pragma License (Modified_GPL);
 
 generic
    type Aux_Data (<>) is private;
+   with function Index_Trimmed_Image (Item : in Index_Type) return String;
    with function Element_Image (Item : in Element_Type; Aux : in Aux_Data) 
return String;
-function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux (Item : in Vector; 
Aux : in Aux_Data) return String;
+function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux
+  (Item        : in Vector;
+   Aux         : in Aux_Data;
+   Association : in Boolean := False)
+  return String;
diff --git a/sal-gen_unbounded_definite_vectors.adb 
b/sal-gen_unbounded_definite_vectors.adb
index 49d59a6..b5019bb 100644
--- a/sal-gen_unbounded_definite_vectors.adb
+++ b/sal-gen_unbounded_definite_vectors.adb
@@ -470,6 +470,28 @@ package body SAL.Gen_Unbounded_Definite_Vectors is
       end if;
    end Next;
 
+   function Prev (Position : in Cursor) return Cursor
+   is begin
+      if Position = No_Element then
+         return No_Element;
+      elsif Position.Index > To_Peek_Type (Position.Container.First) then
+         return (Position.Container, Position.Index - 1);
+      else
+         return No_Element;
+      end if;
+   end Prev;
+
+   procedure Prev (Position : in out Cursor)
+   is begin
+      if Position = No_Element then
+         null;
+      elsif Position.Index > To_Peek_Type (Position.Container.First) then
+         Position.Index := Position.Index - 1;
+      else
+         Position := No_Element;
+      end if;
+   end Prev;
+
    function To_Cursor
      (Container : aliased in Vector;
       Index     :         in Extended_Index)
diff --git a/sal-gen_unbounded_definite_vectors.ads 
b/sal-gen_unbounded_definite_vectors.ads
index ae472d8..8023794 100644
--- a/sal-gen_unbounded_definite_vectors.ads
+++ b/sal-gen_unbounded_definite_vectors.ads
@@ -158,6 +158,8 @@ package SAL.Gen_Unbounded_Definite_Vectors is
    function First (Container : aliased in Vector) return Cursor;
    function Next (Position : in Cursor) return Cursor;
    procedure Next (Position : in out Cursor);
+   function Prev (Position : in Cursor) return Cursor;
+   procedure Prev (Position : in out Cursor);
 
    function To_Cursor
      (Container : aliased in Vector;
@@ -223,5 +225,6 @@ private
    --  Visible for child package
 
    function To_Peek_Type (Item : in Extended_Index) return Base_Peek_Type with 
Inline;
+   function To_Index_Type (Item : in Base_Peek_Type) return Extended_Index;
 
 end SAL.Gen_Unbounded_Definite_Vectors;
diff --git a/sal-generic_decimal_image.adb b/sal-generic_decimal_image.adb
new file mode 100644
index 0000000..2699277
--- /dev/null
+++ b/sal-generic_decimal_image.adb
@@ -0,0 +1,48 @@
+--  Abstract:
+--
+--  see spec
+--
+--  Copyright (C) 2005, 2006, 2009 Stephen Leake.  All Rights Reserved.
+--
+--  This library is free software; you can redistribute it and/or
+--  modify it under 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 library 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
+--  distributed with this program; see file COPYING. If not, write to
+--  the Free Software Foundation, 59 Temple Place - Suite 330, Boston,
+--  MA 02111-1307, USA.
+--
+--  As a special exception, if other files instantiate generics from
+--  this unit, or you link this unit with other files to produce an
+--  executable, this  unit  does not  by itself cause  the resulting
+--  executable to be covered by the GNU General Public License. This
+--  exception does not however invalidate any other reasons why the
+--  executable file  might be covered by the  GNU Public License.
+
+pragma License (Modified_GPL);
+
+function SAL.Generic_Decimal_Image
+  (Item  : in Number_Type;
+   Width : in Natural)
+  return String
+is
+   pragma Warnings (Off);
+   --  Avoid warning about "abs applied to non-negative value has no
+   --  effect" for some instantiations.
+   Temp : Integer := abs Integer (Item);
+   --  IMPROVEME: need test for Decimal_Image, include constrained positive 
number_type
+   pragma Warnings (On);
+   Digit : Integer;
+   Image : String (1 .. Width);
+begin
+   for I in reverse Image'Range loop
+      Digit     := Temp mod 10;
+      Temp      := Temp / 10;
+      Image (I) := Character'Val (Character'Pos ('0') + Digit);
+   end loop;
+   return Image;
+end SAL.Generic_Decimal_Image;
diff --git a/sal-generic_decimal_image.ads b/sal-generic_decimal_image.ads
new file mode 100644
index 0000000..ec749f6
--- /dev/null
+++ b/sal-generic_decimal_image.ads
@@ -0,0 +1,37 @@
+--  Abstract:
+--
+--  Generic leading zero unsigned decimal image
+--
+--  Copyright (C) 2004, 2009, 2019 Free Software Foundation.  All Rights 
Reserved.
+--
+--  This library is free software; you can redistribute it and/or
+--  modify it under 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 library 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
+--  distributed with this program; see file COPYING. If not, write to
+--  the Free Software Foundation, 59 Temple Place - Suite 330, Boston,
+--  MA 02111-1307, USA.
+--
+--  As a special exception, if other files instantiate generics from
+--  this unit, or you link this unit with other files to produce an
+--  executable, this  unit  does not  by itself cause  the resulting
+--  executable to be covered by the GNU General Public License. This
+--  exception does not however invalidate any other reasons why the
+--  executable file  might be covered by the  GNU Public License.
+
+pragma License (Modified_GPL);
+
+generic
+   type Number_Type is range <>;
+function SAL.Generic_Decimal_Image
+  (Item  : in Number_Type;
+   Width : in Natural)
+  return String;
+--  Return a decimal unsigned image of Item, padded with leading zeros
+--  to Width. If Width is too small for Item, leading digits are
+--  silently truncated.
+pragma Pure (SAL.Generic_Decimal_Image);
diff --git a/sal.adb b/sal.adb
index 6a2dc71..ac3b037 100644
--- a/sal.adb
+++ b/sal.adb
@@ -26,7 +26,7 @@ package body SAL is
 
    function Version return String is
    begin
-      return "SAL 3.1";
+      return "SAL 3.2";
    end Version;
 
 end SAL;
diff --git a/wisi-elisp-parse.el b/wisi-elisp-parse.el
index e757ac8..2c93a37 100644
--- a/wisi-elisp-parse.el
+++ b/wisi-elisp-parse.el
@@ -1,1686 +1,1721 @@
-;; wisi-elisp-parse.el --- Wisi parser  -*- lexical-binding:t -*-
-
-;; Copyright (C) 2013-2015, 2017 - 2019  Free Software Foundation, Inc.
-
-;; This file is part of GNU Emacs.
-
-;; GNU Emacs 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 of the License, or
-;; (at your option) any later version.
-
-;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
-
-
-;;; Commentary:
-
-;; An extended LALR parser, that handles shift/reduce and
-;; reduce/reduce conflicts by spawning parallel parsers to follow each
-;; path.
-
-;;; Code:
-
-(require 'cl-lib)
-(require 'wisi-elisp-lexer)
-(require 'wisi-parse-common)
-
-(defvar wisi-elisp-parse-max-parallel-current (cons 0 0)
-  "Cons (count . point); Maximum number of parallel parsers used in most 
recent parse,
-point at which that max was spawned.")
-
-(defvar wisi-debug-identical 0
-  "Debug terminating identical parsers.
-0 - keep lower-numbered parser.
-1 - keep higher-numbered parser.
-2 - error.")
-
-(cl-defstruct (wisi-elisp-parser-state
-           (:copier nil))
-  label ;; integer identifying parser for debug
-
-  active
-  ;; 'shift  - need new token
-  ;; 'reduce - need reduce
-  ;; 'accept - parsing completed
-  ;; 'error  - failed, error not reported yet
-  ;; nil     - terminated
-  ;;
-  ;; 'pending-shift, 'pending-reduce - newly created parser
-
-  stack
-  ;; Each stack item takes two slots: wisi-tok, state
-
-  sp ;; stack pointer
-
-  pending
-  ;; list of (action-symbol stack-fragment)
-  )
-
-(cl-defstruct (wisi-elisp-parser (:include wisi-parser))
-  actions
-  gotos
-  next-token
-  )
-
-;;;###autoload
-(defun wisi-make-elisp-parser (automaton next-token)
-  "Return ‘wisi-parser’ object.
-
-- AUTOMATON is the parse table generated by `wisi-compile-grammar'.
-
-- NEXT-TOKEN is a function with no argument called by the parser to
-  obtain the next token from the current buffer after point, as a
-  ’wisi-tok’ object (normally ‘wisi-forward-token’)."
-  (make-wisi-elisp-parser
-   :actions (aref automaton 0)
-   :gotos (aref automaton 1)
-   :next-token next-token))
-
-(cl-defmethod wisi-parse-kill ((_parser wisi-elisp-parser))
-  nil)
-
-(defvar wisi-elisp-parse--indent nil
-  ;; not buffer-local; only let-bound in wisi-parse-current (elisp)
-  "A vector of indentation for all lines in buffer.
-Each element can be one of:
-- integer : indent
-
-- list ('anchor (start-id ...) indent)  :
-  indent for current line, base indent for following 'anchored
-  lines. Start-id is list of ids anchored at this line. For parens
-  and other uses.
-
-- list ('anchored id delta) :
-  indent = delta + 'anchor id line indent; for lines indented
-  relative to anchor.
-
-- list ('anchor (start-id ...) ('anchored id delta))
-  for nested anchors.")
-
-(cl-defmethod wisi-parse-current ((parser wisi-elisp-parser) _begin _send-end 
_parse-end)
-  "Parse entire current buffer.
-BEGIN, END are ignored"
-
-  (let ((actions (wisi-elisp-parser-actions parser))
-       (gotos   (wisi-elisp-parser-gotos parser))
-       (parser-states ;; vector of parallel parser states
-        (vector
-         (make-wisi-elisp-parser-state
-          :label 0
-          :active  'shift
-          :stack   (make-vector wisi-parse-max-stack-size nil)
-          :sp      0
-          :pending nil)))
-       (active-parser-count 1)
-       active-parser-count-prev
-       (active 'shift)
-       (token nil)
-       some-pending
-       wisi-elisp-parse--indent)
-
-    (cl-case wisi--parse-action
-      (indent
-       (let ((line-count (1+ (count-lines (point-min) (point-max)))))
-        (setq wisi-elisp-parse--indent (make-vector line-count 0))
-        (wisi-elisp-lexer-reset line-count wisi--lexer)))
-
-      (navigate
-       (setq wisi-end-caches nil))
-
-      (t nil))
-
-    (setf (wisi-parser-lexer-errors parser) nil)
-    (setf (wisi-parser-parse-errors parser) nil)
-
-    ;; We assume the lexer relies on syntax properties
-    (when (< emacs-major-version 25) (syntax-propertize (point-max)))
-
-    (goto-char (point-min))
-    (forward-comment (point-max))
-    (aset (wisi-elisp-parser-state-stack (aref parser-states 0)) 0 0)
-
-    (setq token (funcall (wisi-elisp-parser-next-token parser)))
-    (setq wisi-elisp-parse-max-parallel-current (cons 0 0))
-
-    (while (not (eq active 'accept))
-      (setq active-parser-count-prev active-parser-count)
-      (setq some-pending nil)
-      (dotimes (parser-index (length parser-states))
-       (when (eq active (wisi-elisp-parser-state-active (aref parser-states 
parser-index)))
-         (let* ((parser-state (aref parser-states parser-index))
-                (result (wisi-elisp-parse-1 token parser-state (> 
active-parser-count 1) actions gotos)))
-           (when result
-             ;; spawn a new parser
-             (when (= active-parser-count wisi-parse-max-parallel)
-               (let* ((state (aref (wisi-elisp-parser-state-stack parser-state)
-                                   (wisi-elisp-parser-state-sp parser-state)))
-                      (msg (wisi-error-msg (concat "too many parallel parsers 
required in grammar state %d;"
-                                                   " simplify grammar, or 
increase `wisi-elisp-parse-max-parallel'")
-                                           state)))
-                 (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
-                 (signal 'wisi-parse-error msg)))
-
-             (let ((j (wisi-elisp-parse-free-parser parser-states)))
-               (cond
-                ((= j -1)
-                 ;; Add to parser-states; the new parser won't be executed
-                 ;; again in this parser-index loop.
-                 (setq parser-states (vconcat parser-states (vector nil)))
-                 (setq j (1- (length parser-states))))
-                ((< j parser-index)
-                 ;; The new parser won't be executed again in this
-                 ;; parser-index loop; nothing to do.
-                 )
-                (t
-                 ;; Don't let the new parser execute again in this
-                 ;; parser-index loop.
-                 (setq some-pending t)
-                 (setf (wisi-elisp-parser-state-active result)
-                       (cl-case (wisi-elisp-parser-state-active result)
-                         (shift 'pending-shift)
-                         (reduce 'pending-reduce)
-                        )))
-                 )
-               (setq active-parser-count (1+ active-parser-count))
-               (when (> active-parser-count (car 
wisi-elisp-parse-max-parallel-current))
-                 (setq wisi-elisp-parse-max-parallel-current (cons 
active-parser-count (point))))
-               (setf (wisi-elisp-parser-state-label result) j)
-               (aset parser-states j result))
-             (when (> wisi-debug 1)
-                (message "spawn parser (%d active)" active-parser-count)))
-
-           (when (eq 'error (wisi-elisp-parser-state-active parser-state))
-             (setq active-parser-count (1- active-parser-count))
-             (when (> wisi-debug 1)
-                (message "terminate parser (%d active)" active-parser-count))
-             (cl-case active-parser-count
-               (0
-                (cond
-                 ((= active-parser-count-prev 1)
-                  ;; We were not in a parallel parse; abandon parsing, report 
the error.
-                  (let* ((state (aref (wisi-elisp-parser-state-stack 
parser-state)
-                                      (wisi-elisp-parser-state-sp 
parser-state)))
-                         (msg (wisi-error-msg "syntax error in grammar state 
%d; unexpected %s, expecting one of %s"
-                                              state
-                                              (wisi-token-text token)
-                                              (mapcar 'car (aref actions 
state)))))
-                    (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
-                    (signal 'wisi-parse-error msg)))
-                 (t
-                  ;; Report errors from all parsers that failed on this token.
-                  (let ((msg))
-                    (dotimes (_ (length parser-states))
-                      (let* ((parser-state (aref parser-states parser-index))
-                             (state (aref (wisi-elisp-parser-state-stack 
parser-state)
-                                          (wisi-elisp-parser-state-sp 
parser-state))))
-                        (when (eq 'error (wisi-elisp-parser-state-active 
parser-state))
-                          (setq msg
-                                (concat msg
-                                        (when msg "\n")
-                                        (wisi-error-msg
-                                         "syntax error in grammar state %d; 
unexpected %s, expecting one of %s"
-                                         state
-                                         (wisi-token-text token)
-                                         (mapcar 'car (aref actions state)))))
-                          )))
-                    (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
-                    (signal 'wisi-parse-error msg)))
-                 ))
-
-               (1
-                (setf (wisi-elisp-parser-state-active parser-state) nil); 
Don't save error for later.
-                (wisi-elisp-parse-execute-pending (aref parser-states 
(wisi-elisp-parse-active-parser parser-states))))
-
-               (t
-                ;; We were in a parallel parse, and this parser
-                ;; failed; mark it inactive, don't save error for
-                ;; later.
-                (setf (wisi-elisp-parser-state-active parser-state) nil)
-                )))
-           )));; end dotimes
-
-      (when some-pending
-       ;; Change pending-* parsers to *.
-       (dotimes (parser-index (length parser-states))
-         (cond
-          ((eq (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'pending-shift)
-           (setf (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'shift))
-          ((eq (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'pending-reduce)
-           (setf (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'reduce))
-          )))
-
-      (setq active (wisi-elisp-parsers-active parser-states 
active-parser-count))
-      (when (eq active 'shift)
-       (when (> active-parser-count 1)
-         (setq active-parser-count (wisi-elisp-parse-elim-identical parser 
parser-states active-parser-count)))
-
-       (setq token (funcall (wisi-elisp-parser-next-token parser))))
-    )
-    (when (> active-parser-count 1)
-      (error "ambiguous parse result"))
-
-    (cl-case wisi--parse-action
-      (indent
-       (wisi-elisp-parse--indent-leading-comments)
-       (wisi-elisp-parse--resolve-anchors))
-
-      (t nil))
-
-    ;; Return region parsed.
-    (cons (point-min) (point))
-    ))
-
-(defun wisi-elisp-parsers-active-index (parser-states)
-  ;; only called when active-parser-count = 1
-  (let ((result nil)
-       (i 0))
-    (while (and (not result)
-               (< i (length parser-states)))
-      (when (wisi-elisp-parser-state-active (aref parser-states i))
-       (setq result i))
-      (setq i (1+ i)))
-    result))
-
-(defun wisi-elisp-parsers-active (parser-states active-count)
-  "Return the type of parser cycle to execute.
-PARSER-STATES[*].active is the last action a parser took. If it
-was `shift', that parser used the input token, and should not be
-executed again until another input token is available, after all
-parsers have shifted the current token or terminated.
-
-Returns one of:
-
-`accept' : all PARSER-STATES have active set to nil or `accept' -
-done parsing
-
-`shift' : all PARSER-STATES have active set to nil, `accept', or
-`shift' - get a new token, execute `shift' parsers.
-
-`reduce' : some PARSER-STATES have active set to `reduce' - no new
-token, execute `reduce' parsers."
-  (let ((result nil)
-       (i 0)
-       (shift-count 0)
-       (accept-count 0)
-       active)
-    (while (and (not result)
-               (< i (length parser-states)))
-      (setq active (wisi-elisp-parser-state-active (aref parser-states i)))
-      (cond
-       ((eq active 'shift) (setq shift-count (1+ shift-count)))
-       ((eq active 'reduce) (setq result 'reduce))
-       ((eq active 'accept) (setq accept-count (1+ accept-count)))
-       )
-      (setq i (1+ i)))
-
-    (cond
-     (result )
-     ((= accept-count active-count)
-      'accept)
-     ((= (+ shift-count accept-count) active-count)
-      'shift)
-     (t
-      ;; all parsers in error state; should not get here
-      (error "all parsers in error state; programmer error"))
-     )))
-
-(defun wisi-elisp-parse-free-parser (parser-states)
-  "Return index to a non-active parser in PARSER-STATES, -1 if there is none."
-  (let ((result nil)
-       (i 0))
-    (while (and (not result)
-               (< i (length parser-states)))
-      (when (not (wisi-elisp-parser-state-active (aref parser-states i)))
-       (setq result i))
-      (setq i (1+ i)))
-    (if result result -1)))
-
-(defun wisi-elisp-parse-active-parser (parser-states)
-  "Return index to the first active parser in PARSER-STATES."
-  (let ((result nil)
-       (i 0))
-    (while (and (not result)
-               (< i (length parser-states)))
-      (when (wisi-elisp-parser-state-active (aref parser-states i))
-       (setq result i))
-      (setq i (1+ i)))
-    (unless result
-      (error "no active parsers"))
-    result))
-
-(defun wisi-elisp-parse-elim-identical (parser parser-states 
active-parser-count)
-  "Check for parsers in PARSER-STATES that have reached identical states 
eliminate one.
-Return new ACTIVE-PARSER-COUNT. Assumes all parsers have active
-nil, `shift', or `accept'."
-  ;; parser-states passed by reference; active-parser-count by copy
-  ;; see test/ada_mode-slices.adb for example
-  (dotimes (parser-i (1- (length parser-states)))
-    (when (wisi-elisp-parser-state-active (aref parser-states parser-i))
-      (dotimes (parser-j (- (length parser-states) parser-i 1))
-       (when (wisi-elisp-parser-state-active (aref parser-states (+ parser-i 
parser-j 1)))
-         (when (eq (wisi-elisp-parser-state-sp (aref parser-states parser-i))
-                    (wisi-elisp-parser-state-sp (aref parser-states (+ 
parser-i parser-j 1))))
-           (let ((compare t)
-                 exec)
-             (dotimes (stack-i (wisi-elisp-parser-state-sp (aref parser-states 
parser-i)))
-               (setq
-                compare
-                (and compare ;; bypass expensive 'arefs' after first stack 
item compare fail
-                     (equal (aref (wisi-elisp-parser-state-stack (aref 
parser-states parser-i)) stack-i)
-                            (aref (wisi-elisp-parser-state-stack (aref 
parser-states (+ parser-i parser-j 1)))
-                                  stack-i)))))
-             (when compare
-               ;; parser stacks are identical
-               (setq active-parser-count (1- active-parser-count))
-               (when (> wisi-debug 1)
-                 (message "terminate identical parser %d (%d active)"
-                          (+ parser-i parser-j 1) active-parser-count)
-                 (let ((state-i (aref parser-states parser-i))
-                       (state-j (aref parser-states (+ parser-i parser-j 1))))
-                   (message "%d actions:" (wisi-elisp-parser-state-label 
state-i))
-                   (mapc #'wisi-elisp-parse-debug-put-action 
(wisi-elisp-parser-state-pending state-i))
-
-                   (message "%d actions:" (wisi-elisp-parser-state-label 
state-j))
-                   (mapc #'wisi-elisp-parse-debug-put-action 
(wisi-elisp-parser-state-pending state-j))
-                   ))
-               (cl-ecase wisi-debug-identical
-                 (0
-                  (setq exec parser-i)
-                  (setf (wisi-elisp-parser-state-active (aref parser-states (+ 
parser-i parser-j 1))) nil))
-
-                 (1
-                  (setq exec (+ parser-i parser-j 1))
-                  (setf (wisi-elisp-parser-state-active (aref parser-states 
parser-i)) nil))
-
-                 (2
-                  (let ((msg "identical parser stacks"))
-                    (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
-                    (signal 'wisi-parse-error msg)))
-                 )
-               (when (= active-parser-count 1)
-                 ;; The actions for the two parsers are not
-                 ;; identical, but most of the time either is good
-                 ;; enough for indentation and navigation, so we just
-                 ;; do the actions for the one that is not
-                 ;; terminating. Some times, a significant action is
-                 ;; lost. In that case, turn on
-                 ;; ‘wisi-debug-identical’ to investigate fixing it.
-                 (wisi-elisp-parse-execute-pending (aref parser-states exec)))
-               ))))
-       )))
-  active-parser-count)
-
-(defun wisi-elisp-parse-exec-action (func nonterm tokens)
-  "Execute action if TOKENS not null."
-  ;; `tokens' is null when all tokens in a grammar statement are
-  ;; optional and not present.
-  (unless wisi-action-disable
-    (if (< 0 (length tokens))
-       (when wisi--parse-action
-         (funcall func nonterm tokens))
-
-      (when (> wisi-debug 1)
-       (message "... action skipped; no tokens"))
-      )))
-
-(defvar wisi-elisp-parser-state nil
-  "Let-bound in `wisi-elisp-parse-reduce', used in `wisi-parse-find-token'.")
-
-(defun wisi-elisp-parse-debug-put-action (action)
-  ;; Action is (semantic-function nonterm [tokens])
-  (message "%s [%s]"
-          (nth 0 action)
-          (mapcar #'wisi-tok-debug-image (nth 2 action))))
-
-(defun wisi-elisp-parse-execute-pending (parser-state)
-  (let ((wisi-elisp-parser-state parser-state);; reference, for 
wisi-parse-find-token
-       (pending (wisi-elisp-parser-state-pending parser-state)))
-
-    (when (> wisi-debug 1)
-      (message "%d: pending actions:" (wisi-elisp-parser-state-label 
parser-state)))
-
-    (while pending
-      (when (> wisi-debug 1) (wisi-elisp-parse-debug-put-action (car pending)))
-
-      (let ((func-args (pop pending)))
-       (wisi-elisp-parse-exec-action (nth 0 func-args) (nth 1 func-args) 
(cl-caddr func-args)))
-      )
-    (setf (wisi-elisp-parser-state-pending parser-state) nil)
-    ))
-
-(defmacro wisi-elisp-parse-action (i al)
-  "Return the parser action.
-I is a token item number and AL is the list of (item . action)
-available at current state.  The first element of AL contains the
-default action for this state."
-  `(cdr (or (assq ,i ,al) (car ,al))))
-
-(defun wisi-elisp-parse-1 (token parser-state pendingp actions gotos)
-  "Perform one shift or reduce on PARSER-STATE.
-If PENDINGP, push actions onto PARSER-STATE.pending; otherwise execute them.
-See `wisi-elisp-parse' for full details.
-Return nil or new parser (a wisi-elisp-parser-state struct)."
-  (let* ((state (aref (wisi-elisp-parser-state-stack parser-state)
-               (wisi-elisp-parser-state-sp parser-state)))
-        (parse-action (wisi-elisp-parse-action (wisi-tok-token token) (aref 
actions state)))
-        new-parser-state)
-
-    (when (> wisi-debug 1)
-      ;; output trace info
-      (if (> wisi-debug 2)
-         (progn
-           ;; put top 10 stack items
-           (let* ((count (min 20 (wisi-elisp-parser-state-sp parser-state)))
-                  (msg (make-vector (+ 1 count) nil)))
-             (dotimes (i count)
-               (aset msg (- count i)
-                     (aref (wisi-elisp-parser-state-stack parser-state)
-                           (- (wisi-elisp-parser-state-sp parser-state) i)))
-               )
-             (message "%d: %s: %d: %s"
-                      (wisi-elisp-parser-state-label parser-state)
-                      (wisi-elisp-parser-state-active parser-state)
-                      (wisi-elisp-parser-state-sp parser-state)
-                      msg))
-           (message "   %d: %s: %s" state (wisi-tok-debug-image token) 
parse-action))
-       (message "%d: %d: %s: %s" (wisi-elisp-parser-state-label parser-state) 
state token parse-action)))
-
-    (when (and (listp parse-action)
-              (not (symbolp (car parse-action))))
-      ;; Conflict; spawn a new parser.
-      (setq new-parser-state
-           (make-wisi-elisp-parser-state
-            :active  nil
-            :stack   (vconcat (wisi-elisp-parser-state-stack parser-state))
-            :sp      (wisi-elisp-parser-state-sp parser-state)
-            :pending (wisi-elisp-parser-state-pending parser-state)))
-
-      (wisi-elisp-parse-2 (cadr parse-action) token new-parser-state t gotos)
-      (setq pendingp t)
-      (setq parse-action (car parse-action))
-      );; when
-
-    ;; current parser
-    (wisi-elisp-parse-2 parse-action token parser-state pendingp gotos)
-
-    new-parser-state))
-
-(defun wisi-elisp-parse-2 (action token parser-state pendingp gotos)
-  "Execute parser ACTION (must not be a conflict).
-Return nil."
-  (cond
-   ((eq action 'accept)
-    (setf (wisi-elisp-parser-state-active parser-state) 'accept))
-
-   ((eq action 'error)
-    (setf (wisi-elisp-parser-state-active parser-state) 'error))
-
-   ((natnump action)
-    ;; Shift token and new state (= action) onto stack
-    (let ((stack (wisi-elisp-parser-state-stack parser-state)); reference
-         (sp (wisi-elisp-parser-state-sp parser-state))); copy
-      (setq sp (+ sp 2))
-      (aset stack (1- sp) token)
-      (aset stack sp action)
-      (setf (wisi-elisp-parser-state-sp parser-state) sp))
-    (setf (wisi-elisp-parser-state-active parser-state) 'shift))
-
-   (t
-    (wisi-elisp-parse-reduce action parser-state pendingp gotos)
-    (setf (wisi-elisp-parser-state-active parser-state) 'reduce))
-   ))
-
-(defun wisi-elisp-parse-first-last (stack i j)
-  "Return a pair (FIRST . LAST), indices for the first and last
-non-empty tokens for a nonterminal; or nil if all tokens are
-empty. STACK is the parser stack.  I and J are the indices in
-STACK of the first and last tokens of the nonterminal."
-  (let ((start (car (wisi-tok-region (aref stack i))))
-        (end   (cdr (wisi-tok-region (aref stack j)))))
-    (while (and (or (not start) (not end))
-               (/= i j))
-      (cond
-       ((not start)
-       ;; item i is an empty production
-       (setq start (car (wisi-tok-region (aref stack (setq i (+ i 2)))))))
-
-       ((not end)
-       ;; item j is an empty production
-       (setq end (cdr (wisi-tok-region (aref stack (setq j (- j 2)))))))
-
-       (t (setq i j))))
-
-    (when (and start end)
-      (cons i j))
-    ))
-
-(cl-defmethod wisi-parse-find-token ((_parser wisi-elisp-parser) token-symbol)
-  "Find token with TOKEN-SYMBOL on current parser stack, return token struct.
-For use in grammar actions."
-  ;; Called from wisi-parse-exec-action in wisi-parse-reduce
-  (let* ((stack (wisi-elisp-parser-state-stack wisi-elisp-parser-state))
-        (sp (1- (wisi-elisp-parser-state-sp wisi-elisp-parser-state)))
-        (tok (aref stack sp)))
-    (while (and (> sp 0)
-               (not (eq token-symbol (wisi-tok-token tok))))
-      (setq sp (- sp 2))
-      (setq tok (aref stack sp)))
-    (if (= sp 0)
-       (error "token %s not found on parse stack" token-symbol)
-      tok)
-    ))
-
-(cl-defmethod wisi-parse-stack-peek ((_parser wisi-elisp-parser) n)
-  ;; IMPROVEME: store stack in parser
-  (let* ((stack (wisi-elisp-parser-state-stack wisi-elisp-parser-state))
-        (sp (1- (wisi-elisp-parser-state-sp wisi-elisp-parser-state)))
-        (i (- sp (* 2 n))))
-    (when (> i 0)
-      (aref stack i))))
-
-(defun wisi-elisp-parse-reduce (action parser-state pendingp gotos)
-  "Reduce PARSER-STATE.stack, and execute or pend ACTION."
-  (let* ((wisi-elisp-parser-state parser-state);; reference, for 
wisi-parse-find-token
-        (stack (wisi-elisp-parser-state-stack parser-state)); reference
-        (sp (wisi-elisp-parser-state-sp parser-state)); copy
-        (token-count (nth 2 action))
-        (nonterm (nth 0 action))
-        (first-last (when (> token-count 0)
-                      (wisi-elisp-parse-first-last stack (- sp (* 2 (1- 
token-count)) 1) (1- sp))))
-        (nonterm-region (when first-last
-                          (cons
-                           (car (wisi-tok-region (aref stack (car 
first-last))))
-                           (cdr (wisi-tok-region (aref stack (cdr 
first-last)))))))
-        (post-reduce-state (aref stack (- sp (* 2 token-count))))
-        (new-state (cdr (assoc nonterm (aref gotos post-reduce-state))))
-        (tokens (make-vector token-count nil))
-        line first comment-line comment-end)
-
-    (when (not new-state)
-      (error "no goto for %s %d" nonterm post-reduce-state))
-
-    (dotimes (i token-count) ;;  i = 0 .. (1- token-count); last token = 0, 
first token = (1- token-count)
-      (let ((tok (aref stack (- sp (* 2 i) 1))))
-       (when (nth 1 action)
-         ;; don't need wisi-tokens for a null user action
-         (aset tokens (- token-count i 1) tok))
-
-       (when (eq wisi--parse-action 'indent)
-         (setq line (or (wisi-tok-line tok) line))
-         (cond
-          ((numberp (wisi-tok-first tok))
-           (setq first (wisi-tok-first tok)))
-
-          ((wisi-tok-first tok)
-           (setq first (wisi-tok-line tok)))
-
-          ((and (not (= i 0))
-                      (wisi-tok-comment-line tok))
-           ;; comment lines following last token are not included in nonterm
-           ;; test/ada_mode-nominal.ads Object_Access_Type_5a
-           ;; test/ada_mode-parens.adb
-           (setq first (wisi-tok-comment-line tok)))
-          )
-         (when (and (= i 0)
-                    (wisi-tok-comment-line tok))
-           (setq comment-line (wisi-tok-comment-line tok))
-           (setq comment-end (wisi-tok-comment-end tok)))
-       )))
-
-    (setq sp (+ 2 (- sp (* 2 token-count))))
-    (aset stack (1- sp)
-         (make-wisi-tok
-          :token nonterm
-          :region nonterm-region
-          :nonterminal t
-          :line line
-          :first first
-          :comment-line comment-line
-          :comment-end comment-end))
-    (aset stack sp new-state)
-    (setf (wisi-elisp-parser-state-sp parser-state) sp)
-
-    (when (nth 1 action)
-      ;; nothing to do for a null user action
-      (if pendingp
-         (if (wisi-elisp-parser-state-pending parser-state)
-             (setf (wisi-elisp-parser-state-pending parser-state)
-                   (append (wisi-elisp-parser-state-pending parser-state)
-                           (list (list (nth 1 action) nonterm tokens))))
-           (setf (wisi-elisp-parser-state-pending parser-state)
-                 (list (list (nth 1 action) nonterm tokens))))
-
-       ;; Not pending.
-       (wisi-elisp-parse-exec-action (nth 1 action) nonterm tokens)
-       ))
-    ))
-
-;;;; navigate grammar actions
-
-(defun wisi-elisp-parse--set-end (start-mark end-mark)
-  "Set END-MARK on all caches in `wisi-end-caches' in range START-MARK 
END-MARK,
-delete from `wisi-end-caches'."
-  (let ((i 0)
-       pos cache)
-    (while (< i (length wisi-end-caches))
-      (setq pos (nth i wisi-end-caches))
-      (setq cache (wisi-get-cache pos))
-
-      (if (and (>= pos start-mark)
-              (<  pos end-mark))
-         (progn
-           (setf (wisi-cache-end cache) end-mark)
-           (setq wisi-end-caches (delq pos wisi-end-caches)))
-
-       ;; else not in range
-       (setq i (1+ i)))
-      )))
-
-(defvar wisi-tokens nil
-  ;; Not wisi-elisp-parse--tokens for ease in debugging actions, and
-  ;; to match lots of doc strings.
-  "Array of ‘wisi-tok’ structures for the right hand side of the current 
production.
-Let-bound in parser semantic actions.")
-
-(defvar wisi-nterm nil
-  ;; Not wisi-elisp-parse--nterm for ease in debugging actions
-  "The token id for the left hand side of the current production.
-Let-bound in parser semantic actions.")
-
-(defun wisi-statement-action (pairs)
-  ;; Not wisi-elisp-parse--statement-action to match existing grammar files
-  "Cache navigation information in text properties of tokens.
-Intended as a grammar non-terminal action.
-
-PAIRS is a vector of the form [TOKEN-NUMBER CLASS TOKEN-NUMBER
-CLASS ...] where TOKEN-NUMBER is the (1 indexed) token number in
-the production, CLASS is the wisi class of that token. Use in a
-grammar action as:
-  (wisi-statement-action [1 statement-start 7 statement-end])"
-  (when (eq wisi--parse-action 'navigate)
-    (save-excursion
-      (let ((first-item t)
-           first-keyword-mark
-           (override-start nil)
-           (i 0))
-       (while (< i (length pairs))
-         (let* ((number (1- (aref pairs i)))
-                (region (wisi-tok-region (aref wisi-tokens number)))
-                (token (wisi-tok-token (aref wisi-tokens number)))
-                (class (aref pairs (setq i (1+ i))))
-                (mark (when region (copy-marker (car region) t)))
-                cache)
-
-           (setq i (1+ i))
-
-           (unless (seq-contains wisi-class-list class)
-             (error "%s not in wisi-class-list" class))
-
-           (if region
-               (progn
-                 (if (setq cache (wisi-get-cache (car region)))
-                     ;; We are processing a previously set non-terminal; ie 
simple_statement in
-                     ;;
-                     ;; statement : label_opt simple_statement
-                     ;;
-                     ;; override nonterm, class, containing
-                     (progn
-                       (setf (wisi-cache-class cache) (or override-start 
class))
-                       (setf (wisi-cache-nonterm cache) wisi-nterm)
-                       (setf (wisi-cache-containing cache) first-keyword-mark)
-                       (if wisi-end-caches
-                           (push (car region) wisi-end-caches)
-                         (setq wisi-end-caches (list (car region)))
-                         ))
-
-                   ;; else create new cache
-                   (with-silent-modifications
-                     (put-text-property
-                      (car region)
-                      (1+ (car region))
-                      'wisi-cache
-                      (wisi-cache-create
-                       :nonterm    wisi-nterm
-                       :token      token
-                       :last       (- (cdr region) (car region))
-                       :class      (or override-start class)
-                       :containing first-keyword-mark)
-                      ))
-                   (if wisi-end-caches
-                       (push (car region) wisi-end-caches)
-                     (setq wisi-end-caches (list (car region)))
-                     ))
-
-                 (when first-item
-                   (setq first-item nil)
-                   (when (or override-start
-                             (eq class 'statement-start))
-                     (setq override-start nil)
-                     (setq first-keyword-mark mark)))
-
-                 (when (eq class 'statement-end)
-                   (wisi-elisp-parse--set-end first-keyword-mark (copy-marker 
(car region) t)))
-                 )
-
-             ;; region is nil when a production is empty; if the first
-             ;; token is a start, override the class on the next token.
-             (when (and first-item
-                        (eq class 'statement-start))
-               (setq override-start class)))
-           ))
-       ))))
-
-(defun wisi-containing-action (containing-token contained-token)
-  ;; Not wisi-elisp-parse--containing-action to match existing grammar files
-  "Set containing marks in all tokens in CONTAINED-TOKEN
-with null containing mark to marker pointing to CONTAINING-TOKEN.
-If CONTAINING-TOKEN is empty, the next token number is used."
-  (when (eq wisi--parse-action 'navigate)
-    (let* ((containing-tok (aref wisi-tokens (1- containing-token)))
-          (containing-region (wisi-tok-region containing-tok))
-          (contained-tok (aref wisi-tokens (1- contained-token)))
-          (contained-region (wisi-tok-region contained-tok)))
-
-      (unless containing-region
-       (signal 'wisi-parse-error
-               (wisi-error-msg
-                "wisi-containing-action: containing-region '%s' is empty. 
grammar error; bad action"
-                (wisi-tok-token containing-tok))))
-
-      (unless (or (not contained-region) ;; contained-token is empty
-                 (wisi-get-cache (car containing-region)))
-       (signal 'wisi-parse-error
-               (wisi-error-msg
-                "wisi-containing-action: containing-token '%s' has no cache. 
grammar error; missing action"
-                (wisi-token-text (aref wisi-tokens (1- containing-token))))))
-
-      (when contained-region
-         ;; nil when empty production, may not contain any caches
-         (save-excursion
-           (goto-char (cdr contained-region))
-           (let ((cache (wisi-backward-cache))
-                 (mark (copy-marker (car containing-region) t)))
-             (while cache
-
-               ;; skip blocks that are already marked
-               (while (and (>= (point) (car contained-region))
-                           (markerp (wisi-cache-containing cache)))
-                 (goto-char (wisi-cache-containing cache))
-                 (setq cache (wisi-get-cache (point))))
-
-               (if (or (and (= (car containing-region) (car contained-region))
-                            (<= (point) (car contained-region)))
-                       (< (point) (car contained-region)))
-                   ;; done
-                   (setq cache nil)
-
-                 ;; else set mark, loop
-                 (setf (wisi-cache-containing cache) mark)
-                 (setq cache (wisi-backward-cache)))
-               ))))
-      )))
-
-(defun wisi-elisp-parse--match-token (cache tokens start)
-  "Return t if CACHE has id from TOKENS and is at START or has containing 
equal to START.
-point must be at cache token start.
-TOKENS is a vector [number token_id token_id ...].
-number is ignored."
-  (let ((i 1)
-       (done nil)
-       (result nil)
-       token)
-    (when (or (= start (point))
-             (and (wisi-cache-containing cache)
-                  (= start (wisi-cache-containing cache))))
-      (while (and (not done)
-                 (< i (length tokens)))
-       (setq token (aref tokens i))
-       (if (eq token (wisi-cache-token cache))
-           (setq result t
-                 done t)
-         (setq i (1+ i)))
-       ))
-    result))
-
-(defun wisi-motion-action (token-numbers)
-  ;; Not wisi-elisp-parse--motion-action to match existing grammar files
-  "Set prev/next marks in all tokens given by TOKEN-NUMBERS.
-TOKEN-NUMBERS is a vector with each element one of:
-
-number: the token number; mark that token
-
-vector [number token_id]:
-vector [number token_id token_id ...]:
-   mark all tokens in number nonterminal matching token_id with nil prev/next."
-  (when (eq wisi--parse-action 'navigate)
-    (save-excursion
-      (let (prev-keyword-mark
-           prev-cache
-           token
-           start
-           cache
-           mark
-           (i 0))
-       (while (< i (length token-numbers))
-         (let ((token-number (aref token-numbers i))
-               region)
-           (setq i (1+ i))
-           (cond
-            ((numberp token-number)
-             (setq token (aref wisi-tokens (1- token-number)))
-             (setq region (wisi-tok-region token))
-             (when region
-               (unless start (setq start (car region)))
-               (setq cache (wisi-get-cache (car region)))
-               (unless cache (error "no cache on token %d; add to 
statement-action" token-number))
-               (setq mark (copy-marker (car region) t))
-
-               (if prev-keyword-mark
-                   (progn
-                     (setf (wisi-cache-prev cache) prev-keyword-mark)
-                     (setf (wisi-cache-next prev-cache) mark)
-                     (setq prev-keyword-mark mark)
-                     (setq prev-cache cache))
-
-                 ;; else first token; save as prev
-                 (setq prev-keyword-mark mark)
-                 (setq prev-cache cache))
-               ))
-
-            ((vectorp token-number)
-             ;; token-number may contain 1 or more token_ids
-             ;; the corresponding region may be empty
-             ;; there may not have been a prev keyword
-             (setq region (wisi-tok-region (aref wisi-tokens (1- (aref 
token-number 0)))))
-             (when region ;; not an empty token
-               ;; We must search for all targets at the same time, to
-               ;; get the motion order right.
-               (unless start (setq start (car region)))
-               (goto-char (car region))
-               (setq cache (wisi-get-cache (point)))
-               (unless cache (error "no cache at %d; add to statement-action" 
(car region)))
-               (while (< (point) (cdr region))
-                 (when (wisi-elisp-parse--match-token cache token-number start)
-                   (setq mark (copy-marker (point) t))
-
-                   (if prev-keyword-mark
-                       ;; Don't include this token if prev/next
-                       ;; already set by a lower level statement,
-                       ;; such as a nested if/then/elsif/end if.
-                       (when (and (null (wisi-cache-prev cache))
-                                  (null (wisi-cache-next prev-cache)))
-                         (setf (wisi-cache-prev cache) prev-keyword-mark)
-                         (setf (wisi-cache-next prev-cache) mark)
-                         (setq prev-keyword-mark mark)
-                         (setq prev-cache cache))
-
-                     ;; else first token; save as prev
-                     (setq prev-keyword-mark mark)
-                     (setq prev-cache cache)))
-
-                 (setq cache (wisi-forward-cache))
-                 )))
-
-            (t
-             (error "unexpected token-number %s" token-number))
-            )
-
-           ))
-       ))))
-
-;;;; Face grammar actions
-
-(defun wisi-elisp-parse--face-put-cache (region class)
-  "Put a ’wisi-face’ cache with class CLASS on REGION."
-  (when (> wisi-debug 1)
-    (message "face: put cache %s:%s" region class))
-  (with-silent-modifications
-    (put-text-property
-     (car region)
-     (1+ (car region))
-     'wisi-face
-     (wisi-cache-create
-      :last (- (cdr region) (car region))
-      :class class)
-     )))
-
-(defun wisi-face-mark-action (pairs)
-  ;; Not wisi-elisp-parse--face-mark-action to match existing grammar files
-  "PAIRS is a vector of TOKEN CLASS pairs; mark TOKEN (token number)
-as having face CLASS (prefix or suffix).
-Intended as a grammar action."
-  (when (eq wisi--parse-action 'face)
-    (let ((i 0))
-      (while (< i (length pairs))
-       (let ((region (wisi-tok-region (aref wisi-tokens (1- (aref pairs i)))))
-             (class (aref pairs (setq i (1+ i)))))
-         (setq i (1+ i))
-         (when region
-           ;; region can be null on an optional or virtual token
-           (let ((cache (get-text-property (car region) 'wisi-face)))
-             (if cache
-                 ;; previously marked; extend this cache, delete any others
-                 (progn
-                   (with-silent-modifications
-                     (remove-text-properties (+ (car region) (wisi-cache-last 
cache)) (cdr region) '(wisi-face nil)))
-                   (setf (wisi-cache-class cache) class)
-                   (setf (wisi-cache-last cache) (- (cdr region) (car 
region))))
-
-               ;; else not previously marked
-               (wisi-elisp-parse--face-put-cache region class)))
-           ))
-       ))))
-
-(defun wisi-face-remove-action (tokens)
-  ;; Not wisi-elisp-parse--face-remove-action to match existing grammar files
-  "Remove face caches and faces in TOKENS.
-Intended as a grammar action.
-
-TOKENS is a vector of token numbers."
-  (when (eq wisi--parse-action 'face)
-    (let ((i 0))
-      (while (< i (length tokens))
-       (let* ((number (1- (aref tokens i)))
-              (region (wisi-tok-region (aref wisi-tokens number)))
-              face-cache)
-
-         (setq i (1+ i))
-
-         (when region
-           (let ((pos (car region)))
-             (while (< pos (cdr region))
-               (when (setq face-cache (get-text-property pos 'wisi-face))
-                 (when (> wisi-debug 1)
-                   (message "face: remove face %s" (cons pos (+ pos 
(wisi-cache-last face-cache)))))
-                 (with-silent-modifications
-                   (remove-text-properties
-                    pos (+ pos (wisi-cache-last face-cache))
-                    (list
-                     'wisi-face nil
-                     'font-lock-face nil
-                     'fontified t))))
-               (setq pos (next-single-property-change
-                          (+ pos (or (and face-cache
-                                          (wisi-cache-last face-cache))
-                                     0))
-                          'wisi-face nil (cdr region)))
-               )))
-         )))))
-
-(defun wisi-elisp-parse--face-action-1 (face region)
-  "Apply FACE to REGION."
-  (when region
-    (when (> wisi-debug 1)
-      (message "face: add face %s:%s" region face))
-    (with-silent-modifications
-      (add-text-properties
-       (car region) (cdr region)
-       (list
-       'font-lock-face face
-       'fontified t)))
-    ))
-
-(defun wisi-face-apply-action (triples)
-  ;; Not wisi-elisp-parse--face-apply-action to match existing grammar files
-  "Set face information in `wisi-face' text properties of tokens.
-Intended as a grammar non-terminal action.
-
-TRIPLES is a vector of the form [TOKEN-NUMBER PREFIX-FACE SUFFIX-FACE ...]
-
-In the first ’wisi-face’ cache in each token region, apply
-PREFIX-FACE to class PREFIX, SUFFIX-FACE to class SUFFIX, or
-SUFFIX-FACE to all of the token region if there is no ’wisi-face’
-cache."
-  (when (eq wisi--parse-action 'face)
-    (let (number prefix-face suffix-face (i 0))
-      (while (< i (length triples))
-       (setq number (aref triples i))
-       (setq prefix-face (aref triples (setq i (1+ i))))
-       (setq suffix-face (aref triples (setq i (1+ i))))
-       (cond
-        ((integerp number)
-         (let* ((token-region (wisi-tok-region (aref wisi-tokens (1- number))))
-                (pos (car token-region))
-                (j 0)
-                (some-cache nil)
-                cache)
-           (when token-region
-             ;; region can be null for an optional or virtual token
-             (while (< j 2)
-               (setq cache (get-text-property pos 'wisi-face))
-               (cond
-                ((and (not some-cache)
-                      (null cache))
-                 ;; cache is null when applying a face to a token
-                 ;; directly, without first calling
-                 ;; wisi-face-mark-action. Or when there is a
-                 ;; previously applied face in a lower level token,
-                 ;; such as a numeric literal.
-                 (wisi-elisp-parse--face-action-1 suffix-face token-region))
-
-                ((and cache
-                      (eq 'prefix (wisi-cache-class cache)))
-                 (setq some-cache t)
-                 (wisi-elisp-parse--face-action-1 prefix-face 
(wisi-cache-region cache pos)))
-
-                ((and cache
-                      (eq 'suffix (wisi-cache-class cache)))
-                 (setq some-cache t)
-                 (wisi-elisp-parse--face-action-1 suffix-face 
(wisi-cache-region cache pos)))
-
-                (t
-                 ;; don’t apply a face
-                 nil)
-                )
-
-               (setq j (1+ j))
-               (if suffix-face
-                   (setq pos (next-single-property-change (+ 2 pos) 'wisi-face 
nil (cdr token-region)))
-                 (setq j 2))
-               ))))
-
-        (t
-         ;; catch conversion errors from previous grammar syntax
-         (error "wisi-face-apply-action with non-integer token number"))
-        )
-       (setq i (1+ i))
-       ))))
-
-(defun wisi-face-apply-list-action (triples)
-  ;; Not wisi-elisp-parse--face-apply-list-action to match existing grammar 
files
-  "Similar to ’wisi-face-apply-action’, but applies faces to all
-tokens with a `wisi-face' cache in the wisi-tokens[token-number]
-region, and does not apply a face if there are no such caches."
-  (when (eq wisi--parse-action 'face)
-    (let (number token-region face-region prefix-face suffix-face cache (i 0) 
pos)
-      (while (< i (length triples))
-       (setq number (aref triples i))
-       (setq prefix-face (aref triples (setq i (1+ i))))
-       (setq suffix-face (aref triples (setq i (1+ i))))
-       (cond
-        ((integerp number)
-         (setq token-region (wisi-tok-region (aref wisi-tokens (1- number))))
-         (when token-region
-           ;; region can be null for an optional token
-           (setq pos (car token-region))
-           (while (and pos
-                       (< pos (cdr token-region)))
-             (setq cache (get-text-property pos 'wisi-face))
-             (setq face-region (wisi-cache-region cache pos))
-             (cond
-              ((or (null (wisi-cache-class cache))
-                   (eq 'prefix (wisi-cache-class cache)))
-               (wisi-elisp-parse--face-action-1 prefix-face face-region))
-              ((eq 'suffix (wisi-cache-class cache))
-               (wisi-elisp-parse--face-action-1 suffix-face face-region))
-
-              (t
-               (error "wisi-face-apply-list-action: face cache class is not 
prefix or suffix")))
-
-             (setq pos (next-single-property-change (1+ pos) 'wisi-face nil 
(cdr token-region)))
-             )))
-        (t
-         ;; catch conversion errors from previous grammar syntax
-         (error "wisi-face-apply-list-action with non-integer token number"))
-        )
-       (setq i (1+ i))
-       ))))
-
-;;;; indent grammar actions
-
-(defvar wisi-elisp-parse-indent-hanging-function nil
-  "Language-specific implementation of `wisi-hanging', `wisi-hanging%'.
-A function taking args TOK DELTA1 DELTA2 OPTION NO-ACCUMULATE,
-and returning an indent.
-TOK is a `wisi-tok' struct for the token being indented.
-DELTA1, DELTA2 are the indents of the first and following lines
-within the nonterminal.  OPTION is non-nil if action is `wisi-hanging%'.
-point is at start of TOK, and may be moved.")
-(make-variable-buffer-local 'wisi-elisp-parse-indent-hanging-function)
-
-(defvar wisi-token-index nil
-  ;; Not wisi-elisp-parse--token-index for backward compatibility
-  "Index of current token in `wisi-tokens'.
-Let-bound in `wisi-indent-action', for grammar actions.")
-
-(defvar wisi-indent-comment nil
-  ;; Not wisi-elisp-parse--indent-comment for backward compatibility
-  "Non-nil if computing indent for comment.
-Let-bound in `wisi-indent-action', for grammar actions.")
-
-(defun wisi-elisp-parse--indent-zero-p (indent)
-  (cond
-   ((integerp indent)
-    (= indent 0))
-
-   (t ;; 'anchor
-    (integerp (nth 2 indent)))
-   ))
-
-(defun wisi-elisp-parse--apply-int (i delta)
-  "Add DELTA (an integer) to the indent at index I."
-  (let ((indent (aref wisi-elisp-parse--indent i))) ;; reference if list
-
-    (cond
-     ((integerp indent)
-      (aset wisi-elisp-parse--indent i (+ delta indent)))
-
-     ((listp indent)
-      (cond
-       ((eq 'anchor (car indent))
-       (when (integerp (nth 2 indent))
-         (setf (nth 2 indent) (+ delta (nth 2 indent)))
-         ;; else anchored; not affected by this delta
-         ))
-
-       ((eq 'anchored (car indent))
-       ;; not affected by this delta
-       )))
-
-     (t
-      (error "wisi-elisp-parse--apply-int: invalid form : %s" indent))
-     )))
-
-(defun wisi-elisp-parse--apply-anchored (delta i)
-  "Apply DELTA (an anchored indent) to indent I."
-  ;; delta is from wisi-anchored; ('anchored 1 delta no-accumulate)
-  (let ((indent (aref wisi-elisp-parse--indent i))
-       (accumulate (not (nth 3 delta))))
-
-    (cond
-     ((integerp indent)
-      (when (or accumulate
-               (= indent 0))
-       (let ((temp (seq-take delta 3)))
-         (setf (nth 2 temp) (+ indent (nth 2 temp)))
-         (aset wisi-elisp-parse--indent i temp))))
-
-     ((and (listp indent)
-          (eq 'anchor (car indent))
-          (integerp (nth 2 indent)))
-      (when (or accumulate
-               (= (nth 2 indent) 0))
-       (let ((temp (seq-take delta 3)))
-         (setf (nth 2 temp) (+ (nth 2 indent) (nth 2 temp)))
-         (setf (nth 2 indent) temp))))
-     )))
-
-(defun wisi-elisp-parse--indent-token-1 (line end delta)
-  "Apply indent DELTA to all lines from LINE (a line number) thru END (a 
buffer position)."
-  (let ((i (1- line));; index to wisi-elisp-lexer-line-begin, 
wisi-elisp-parse--indent
-       (paren-first (when (and (listp delta)
-                               (eq 'hanging (car delta)))
-                      (nth 2 delta))))
-
-    (while (<= (aref (wisi-elisp-lexer-line-begin wisi--lexer) i) end)
-      (unless
-         (and ;; no check for called from wisi--indent-comment;
-              ;; comments within tokens are indented by
-              ;; wisi--indent-token
-              wisi-indent-comment-col-0
-              (= 11 (syntax-class (syntax-after (aref 
(wisi-elisp-lexer-line-begin wisi--lexer) i)))))
-       (cond
-        ((integerp delta)
-         (wisi-elisp-parse--apply-int i delta))
-
-        ((listp delta)
-         (cond
-          ((eq 'anchored (car delta))
-           (wisi-elisp-parse--apply-anchored delta i))
-
-          ((eq 'hanging (car delta))
-           ;; from wisi-hanging; delta is ('hanging first-line nest delta1 
delta2 no-accumulate)
-           ;; delta1, delta2 may be anchored
-           (when (or (not (nth 5 delta))
-                     (wisi-elisp-parse--indent-zero-p (aref 
wisi-elisp-parse--indent i)))
-             (if (= i (1- (nth 1 delta)))
-                 ;; apply delta1
-                 (let ((delta1 (nth 3 delta)))
-                   (cond
-                    ((integerp delta1)
-                     (wisi-elisp-parse--apply-int i delta1))
-
-                    (t ;; anchored
-                     (wisi-elisp-parse--apply-anchored delta1 i))
-                    ))
-
-               ;; don't apply hanging indent in nested parens.
-               ;; test/ada_mode-parens.adb
-               ;; No_Conditional_Set : constant Ada.Strings.Maps.Character_Set 
:=
-               ;;   Ada.Strings.Maps."or"
-               ;;     (Ada.Strings.Maps.To_Set (' '),
-               (when (= paren-first
-                        (nth 0 (save-excursion (syntax-ppss (aref 
(wisi-elisp-lexer-line-begin wisi--lexer) i)))))
-                 (let ((delta2 (nth 4 delta)))
-                   (cond
-                    ((integerp delta2)
-                     (wisi-elisp-parse--apply-int i delta2))
-
-                    (t ;; anchored
-                     (wisi-elisp-parse--apply-anchored delta2 i))
-                    )))
-               )))
-
-          (t
-           (error "wisi-elisp-parse--indent-token-1: invalid delta: %s" delta))
-          )) ;; listp delta
-
-        (t
-         (error "wisi-elisp-parse--indent-token-1: invalid delta: %s" delta))
-        ))
-      (setq i (1+ i))
-      )))
-
-(defun wisi-elisp-parse--indent-token (tok token-delta)
-  "Add TOKEN-DELTA to all indents in TOK region,"
-  (let ((line (if (wisi-tok-nonterminal tok)
-                 (wisi-tok-first tok)
-               (when (wisi-tok-first tok) (wisi-tok-line tok))))
-       (end (cdr (wisi-tok-region tok))))
-    (when (and line end token-delta)
-      (wisi-elisp-parse--indent-token-1 line end token-delta))))
-
-(defun wisi-elisp-parse--indent-comment (tok comment-delta)
-  "Add COMMENT-DELTA to all indents in comment region following TOK."
-  (let ((line (wisi-tok-comment-line tok))
-       (end (wisi-tok-comment-end tok)))
-    (when (and line end comment-delta)
-      (wisi-elisp-parse--indent-token-1 line end comment-delta))))
-
-(defun wisi-elisp-parse--anchored-1 (tok offset &optional no-accumulate)
-  "Return offset of TOK relative to current indentation + OFFSET.
-For use in grammar indent actions."
-  (when (wisi-tok-region tok)
-    ;; region can be nil when token is inserted by error recovery
-    (let ((pos (car (wisi-tok-region tok)))
-         delta)
-
-      (goto-char pos)
-      (setq delta (+ offset (- (current-column) (current-indentation))))
-      (wisi-elisp-parse--anchored-2
-       (wisi-tok-line tok) ;; anchor-line
-       (if wisi-indent-comment
-          (wisi-tok-comment-end (aref wisi-tokens wisi-token-index))
-        (cdr (wisi-tok-region (aref wisi-tokens wisi-token-index))));; end
-       delta
-       no-accumulate)
-      )))
-
-(defun wisi-elisp-parse--max-anchor (begin-line end)
-  (let ((i (1- begin-line))
-       (max-i (length (wisi-elisp-lexer-line-begin wisi--lexer)))
-       (result 0))
-    (while (and (< i max-i)
-               (<= (aref (wisi-elisp-lexer-line-begin wisi--lexer) i) end))
-      (let ((indent (aref wisi-elisp-parse--indent i)))
-       (when (listp indent)
-         (cond
-          ((eq 'anchor (car indent))
-           (setq result (max result (car (nth 1 indent))))
-           (when (listp (nth 2 indent))
-             (setq result (max result (nth 1 (nth 2 indent))))
-             ))
-          (t ;; anchored
-           (setq result (max result (nth 1 indent))))
-          )))
-      (setq i (1+ i)))
-    result
-    ))
-
-(defun wisi-elisp-parse--anchored-2 (anchor-line end delta no-accumulate)
-  "Set ANCHOR-LINE as anchor, increment anchors thru END, return anchored 
delta."
-  ;; Typically, we use anchored to indent relative to a token buried in a line:
-  ;;
-  ;; test/ada_mode-parens.adb
-  ;; Local_2 : Integer := (1 + 2 +
-  ;;                         3);
-  ;; line starting with '3' is anchored to '('
-  ;;
-  ;; If the anchor is a nonterminal, and the first token in the anchor
-  ;; is also first on a line, we don't need anchored to compute the
-  ;; delta:
-  ;;
-  ;; test/ada_mode-parens.adb
-  ;; Local_5 : Integer :=
-  ;;   (1 + 2 +
-  ;;      3);
-  ;; delta for line starting with '3' can just be '3'.
-  ;;
-  ;; However, in some places we need anchored to prevent later
-  ;; deltas from accumulating:
-  ;;
-  ;; test/ada_mode-parens.adb
-  ;; No_Conditional_Set : constant Ada.Strings.Maps.Character_Set :=
-  ;;   Ada.Strings.Maps."or"
-  ;;     (Ada.Strings.Maps.To_Set (' '),
-  ;;
-  ;; here the function call actual parameter part is indented first
-  ;; by 'name' and later by 'expression'; we use anchored to keep the
-  ;; 'name' delta and ignore the later delta.
-  ;;
-  ;; So we apply anchored whether the anchor token is first or not.
-
-  (let* ((i (1- anchor-line))
-        (indent (aref wisi-elisp-parse--indent i)) ;; reference if list
-        (anchor-id (1+ (wisi-elisp-parse--max-anchor anchor-line end))))
-
-    ;; Set anchor
-    (cond
-     ((integerp indent)
-      (aset wisi-elisp-parse--indent i (list 'anchor (list anchor-id) indent)))
-
-     ((and (listp indent)
-          (eq 'anchor (car indent)))
-      (push anchor-id (nth 1 indent)))
-
-     ((and (listp indent)
-          (eq 'anchored (car indent)))
-      (aset wisi-elisp-parse--indent i (list 'anchor (list anchor-id) 
(copy-sequence indent))))
-
-     (t
-      (error "wisi-anchored-delta: invalid form in indent: %s" indent)))
-
-    (list 'anchored anchor-id delta no-accumulate)
-    ))
-
-(defun wisi-anchored (token-number offset &optional no-accumulate)
-  ;; Not wisi-elisp-parse--anchored to match existing grammar files
-  "Return offset of token TOKEN-NUMBER in `wisi-tokens'.relative to current 
indentation + OFFSET.
-For use in grammar indent actions."
-  (wisi-elisp-parse--anchored-1 (aref wisi-tokens (1- token-number)) offset 
no-accumulate))
-
-(defun wisi-anchored* (token-number offset)
-  ;; Not wisi-elisp-parse--anchored* to match existing grammar files
-  "If TOKEN-NUMBER token in `wisi-tokens' is first on a line,
-call ’wisi-anchored OFFSET’. Otherwise return 0.
-For use in grammar indent actions."
-  (if (wisi-tok-first (aref wisi-tokens (1- token-number)))
-      (wisi-anchored token-number offset)
-    0))
-
-(defun wisi-anchored*- (token-number offset)
-  ;; Not wisi-elisp-parse--anchored*- to match existing grammar files
-  "If existing indent is zero, and TOKEN-NUMBER token in `wisi-tokens' is 
first on a line,
-call ’wisi-anchored OFFSET’. Otherwise return 0.
-For use in grammar indent actions."
-  (if (wisi-tok-first (aref wisi-tokens (1- token-number)))
-      (wisi-anchored token-number offset t)
-    0))
-
-(defun wisi-elisp-parse--paren-in-anchor-line (anchor-tok offset)
-  "If there is an opening paren containing ANCHOR-TOK in the same line as 
ANCHOR-TOK,
-return OFFSET plus the delta from the line indent to the paren
-position. Otherwise return OFFSET."
-  (let* ((tok-syntax (syntax-ppss (car (wisi-tok-region anchor-tok))))
-        (paren-pos (nth 1 tok-syntax))
-        (anchor-line (wisi-tok-line anchor-tok)))
-
-    (when (and paren-pos ;; in paren
-             (< paren-pos (aref (wisi-elisp-lexer-line-begin wisi--lexer) (1- 
anchor-line))))
-      ;; paren not in anchor line
-      (setq paren-pos nil))
-
-    (if paren-pos
-       (progn
-         (goto-char paren-pos)
-         (+ 1 (- (current-column) (current-indentation)) offset))
-      offset)
-    ))
-
-(defun wisi-anchored% (token-number offset &optional no-accumulate)
-  ;; Not wisi-elisp-parse--anchored% to match existing grammar files
-  "Return either an anchor for the current token at OFFSET from an enclosing 
paren on
-the line containing TOKEN-NUMBER, or OFFSET.
-For use in grammar indent actions."
-  (let* ((indent-tok (aref wisi-tokens wisi-token-index))
-        ;; indent-tok is a nonterminal; this function makes no sense for 
terminals
-        (anchor-tok (aref wisi-tokens (1- token-number))))
-
-    (wisi-elisp-parse--anchored-2
-     (wisi-tok-line anchor-tok)
-
-     (if wisi-indent-comment
-        (wisi-tok-comment-end indent-tok)
-       (cdr (wisi-tok-region indent-tok))) ;; end
-
-     (wisi-elisp-parse--paren-in-anchor-line anchor-tok offset)
-     no-accumulate)
-    ))
-
-(defun wisi-anchored%- (token-number offset)
-  ;; Not wisi-elisp-parse--anchored%- to match existing grammar files
-  "If existing indent is zero, anchor the current token at OFFSET
-from the first token on the line containing TOKEN-NUMBER in `wisi-tokens'.
-Return the delta.
-For use in grammar indent actions."
-  (wisi-anchored% token-number offset t))
-
-(defun wisi-elisp-parse--hanging-1 (delta1 delta2 option no-accumulate)
-  "If OPTION is nil, implement `wisi-hanging'; otherwise `wisi-hanging%'."
-  (let ((tok (aref wisi-tokens wisi-token-index)))
-    ;; tok is a nonterminal; this function makes no sense for terminals
-    ;; syntax-ppss moves point to start of tok
-
-    (cond
-     ((functionp wisi-elisp-parse-indent-hanging-function)
-      (funcall wisi-elisp-parse-indent-hanging-function tok delta1 delta2 
option no-accumulate))
-
-     (t
-      (let ((tok-syntax (syntax-ppss (car (wisi-tok-region tok))))
-           (first-tok-first-on-line
-            ;; first token in tok is first on line
-            (and (numberp (wisi-tok-first tok))
-                 (= (wisi-tok-line tok) (wisi-tok-first tok)))))
-       (list 'hanging
-             (wisi-tok-line tok) ;; first line of token
-             (nth 0 tok-syntax) ;; paren nest level at tok
-             delta1
-             (if (or (not option) first-tok-first-on-line)
-                 delta2
-               delta1)
-             no-accumulate))
-      ))
-    ))
-
-(defun wisi-hanging (delta1 delta2)
-  ;; Not wisi-elisp-parse--hanging to match existing grammar files
-  "Use DETLA1 for first line, DELTA2 for following lines.
-For use in grammar indent actions."
-  (wisi-elisp-parse--hanging-1 delta1 delta2 nil nil))
-
-(defun wisi-hanging% (delta1 delta2)
-  ;; Not wisi-elisp-parse--hanging% to match existing grammar files
-  "If first token is first in line, use DETLA1 for first line, DELTA2 for 
following lines.
-Otherwise use DELTA1 for all lines.
-For use in grammar indent actions."
-  (wisi-elisp-parse--hanging-1 delta1 delta2 t nil))
-
-(defun wisi-hanging%- (delta1 delta2)
-  ;; Not wisi-elisp-parse--hanging%- to match existing grammar files
-  "If existing indent is non-zero, do nothing.
-Else if first token is first in line, use DETLA1 for first line,
-DELTA2 for following lines.  Otherwise use DELTA1 for all lines.
-For use in grammar indent actions."
-  (wisi-elisp-parse--hanging-1 delta1 delta2 t t))
-
-(defun wisi-elisp-parse--indent-offset (token offset)
-  "Return offset from beginning of first token on line containing TOKEN,
-   to beginning of TOKEN, plus OFFSET."
-  (save-excursion
-    (goto-char (aref (wisi-elisp-lexer-line-begin wisi--lexer) (1- 
(wisi-tok-line token))))
-    (back-to-indentation)
-    (+ offset (- (car (wisi-tok-region token)) (point)))
-    ))
-
-(defun wisi-elisp-parse--indent-compute-delta (delta tok)
-  "Return evaluation of DELTA."
-  (cond
-   ((integerp delta)
-    delta)
-
-   ((symbolp delta)
-    (symbol-value delta))
-
-   ((vectorp delta)
-    ;; [token comment]
-    ;; if wisi-indent-comment, we are indenting the comments of the
-    ;; previous token; they should align with the 'token' delta.
-    (wisi-elisp-parse--indent-compute-delta (aref delta 0) tok))
-
-   (t ;; form
-    (cond
-     ((eq 'anchored (car delta))
-      delta)
-
-     (t
-      (save-excursion
-       (goto-char (car (wisi-tok-region tok)))
-       (eval delta)))))
-   ))
-
-(defun wisi-indent-action (deltas)
-  ;; Not wisi-elisp-parse--indent-action to match existing grammar files
-  "Accumulate `wisi--indents' from DELTAS.
-DELTAS is a vector; each element can be:
-- an integer
-- a symbol
-- a lisp form
-- a vector.
-
-The first three are evaluated to give an integer delta. A vector must
-have two elements, giving the code and following comment
-deltas. Otherwise the comment delta is the following delta in
-DELTAS."
-  (when (eq wisi--parse-action 'indent)
-    (dotimes (wisi-token-index (length wisi-tokens))
-      (let* ((tok (aref wisi-tokens wisi-token-index))
-            (token-delta (aref deltas wisi-token-index))
-            (comment-delta
-             (cond
-              ((vectorp token-delta)
-               (aref token-delta 1))
-
-              ((< wisi-token-index (1- (length wisi-tokens)))
-               (aref deltas (1+ wisi-token-index)))
-              )))
-       (when (wisi-tok-region tok)
-         ;; region is null when optional nonterminal is empty
-         (let ((wisi-indent-comment nil))
-           (setq token-delta
-                 (when (and token-delta
-                            (wisi-tok-first tok))
-                   (wisi-elisp-parse--indent-compute-delta token-delta tok)))
-
-           (when (and token-delta
-                      (or (consp token-delta)
-                          (not (= 0 token-delta))))
-             (wisi-elisp-parse--indent-token tok token-delta))
-
-           (setq wisi-indent-comment t)
-           (setq comment-delta
-                 (when (and comment-delta
-                            (wisi-tok-comment-line tok))
-                   (wisi-elisp-parse--indent-compute-delta comment-delta tok)))
-
-           (when (and comment-delta
-                      (or (consp comment-delta)
-                          (not (= 0 comment-delta))))
-             (wisi-elisp-parse--indent-comment tok comment-delta))
-           )
-         )))))
-
-(defun wisi-indent-action* (n deltas)
-  ;; Not wisi-elisp-parse--indent-action* to match existing grammar files
-  "If any of the first N tokens in `wisi-tokens' is first on a line,
-call `wisi-indent-action' with DELTAS.  Otherwise do nothing."
-  (when (eq wisi--parse-action 'indent)
-    (let ((done nil)
-         (i 0)
-         tok)
-      (while (and (not done)
-                 (< i n))
-       (setq tok (aref wisi-tokens i))
-       (setq i (1+ i))
-       (when (and (wisi-tok-region tok)
-                  (wisi-tok-first tok))
-         (setq done t)
-         (wisi-indent-action deltas))
-       ))))
-
-;;;; non-grammar indent functions
-
-(defconst wisi-elisp-parse--max-anchor-depth 20) ;; IMRPOVEME: can compute in 
actions
-
-(defun wisi-elisp-parse--indent-leading-comments ()
-  "Set `wisi-elisp-parse--indent to 0 for comment lines before first token in 
buffer.
-Leave point at first token (or eob)."
-  (save-excursion
-    (goto-char (point-min))
-    (forward-comment (point-max))
-    (let ((end (point))
-         (i 0)
-         (max-i (length wisi-elisp-parse--indent)))
-      (while (and (< i max-i)
-                 (< (aref (wisi-elisp-lexer-line-begin wisi--lexer) i) end))
-       (aset wisi-elisp-parse--indent i 0)
-       (setq i (1+ i)))
-      )))
-
-(defun wisi-elisp-parse--resolve-anchors ()
-  (let ((anchor-indent (make-vector wisi-elisp-parse--max-anchor-depth 0))
-       pos)
-
-    (dotimes (i (length wisi-elisp-parse--indent))
-      (let ((indent (aref wisi-elisp-parse--indent i)))
-
-       (cond
-        ((integerp indent))
-
-        ((listp indent)
-         (let ((anchor-ids (nth 1 indent))
-               (indent2 (nth 2 indent)))
-           (cond
-            ((eq 'anchor (car indent))
-             (cond
-              ((integerp indent2)
-               (dotimes (i (length anchor-ids))
-                 (aset anchor-indent (nth i anchor-ids) indent2))
-               (setq indent indent2))
-
-              ((listp indent2) ;; 'anchored
-               (setq indent (+ (aref anchor-indent (nth 1 indent2)) (nth 2 
indent2)))
-
-               (dotimes (i (length anchor-ids))
-                 (aset anchor-indent (nth i anchor-ids) indent)))
-
-              (t
-               (error "wisi-indent-region: invalid form in wisi-ind-indent %s" 
indent))
-              ));; 'anchor
-
-            ((eq 'anchored (car indent))
-             (setq indent (+ (aref anchor-indent (nth 1 indent)) indent2)))
-
-            (t
-             (error "wisi-indent-region: invalid form in wisi-ind-indent %s" 
indent))
-            )));; listp indent
-
-        (t
-         (error "wisi-indent-region: invalid form in wisi-ind-indent %s" 
indent))
-        );; cond indent
-
-       (when (> i 0)
-         (setq pos (aref (wisi-elisp-lexer-line-begin wisi--lexer) i))
-         (with-silent-modifications
-           (put-text-property (1- pos) pos 'wisi-indent indent)))
-       )) ;; dotimes lines
-
-    ))
-
-(provide 'wisi-elisp-parse)
-;;; wisi-elisp-parse.el ends here
+;; wisi-elisp-parse.el --- Wisi parser  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2013-2015, 2017 - 2019  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+
+;;; Commentary:
+
+;; An extended LALR parser, that handles shift/reduce and
+;; reduce/reduce conflicts by spawning parallel parsers to follow each
+;; path.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'wisi-elisp-lexer)
+(require 'wisi-parse-common)
+
+(defvar wisi-elisp-parse-max-parallel-current (cons 0 0)
+  "Cons (count . point); Maximum number of parallel parsers used in most 
recent parse,
+point at which that max was spawned.")
+
+(defvar wisi-debug-identical 0
+  "Debug terminating identical parsers.
+0 - keep lower-numbered parser.
+1 - keep higher-numbered parser.
+2 - error.")
+
+(cl-defstruct (wisi-elisp-parser-state
+           (:copier nil))
+  label ;; integer identifying parser for debug
+
+  active
+  ;; 'shift  - need new token
+  ;; 'reduce - need reduce
+  ;; 'accept - parsing completed
+  ;; 'error  - failed, error not reported yet
+  ;; nil     - terminated
+  ;;
+  ;; 'pending-shift, 'pending-reduce - newly created parser
+
+  stack
+  ;; Each stack item takes two slots: wisi-tok, state
+
+  sp ;; stack pointer
+
+  pending
+  ;; list of (action-symbol stack-fragment)
+  )
+
+(cl-defstruct (wisi-elisp-parser (:include wisi-parser))
+  actions
+  gotos
+  next-token
+  )
+
+;;;###autoload
+(defun wisi-make-elisp-parser (automaton next-token)
+  "Return ‘wisi-parser’ object.
+
+- AUTOMATON is the parse table generated by `wisi-compile-grammar'.
+
+- NEXT-TOKEN is a function with no argument called by the parser to
+  obtain the next token from the current buffer after point, as a
+  ’wisi-tok’ object (normally ‘wisi-forward-token’)."
+  (make-wisi-elisp-parser
+   :actions (aref automaton 0)
+   :gotos (aref automaton 1)
+   :next-token next-token))
+
+(cl-defmethod wisi-parse-kill ((_parser wisi-elisp-parser))
+  nil)
+
+(defvar wisi-elisp-parse--indent nil
+  ;; not buffer-local; only let-bound in wisi-parse-current (elisp)
+  "A vector of indentation for all lines in buffer.
+Each element can be one of:
+- nil : no indent set yet
+
+- integer : indent
+
+- list ('anchor (start-id ...) indent)  :
+  indent for current line, base indent for following 'anchored
+  lines. Start-id is list of ids anchored at this line. For parens
+  and other uses.
+
+- list ('anchored id delta) :
+  indent = delta + 'anchor id line indent; for lines indented
+  relative to anchor.
+
+- list ('anchor (start-id ...) ('anchored id delta))
+  for nested anchors.")
+
+(cl-defmethod wisi-parse-current ((parser wisi-elisp-parser) _begin _send-end 
_parse-end)
+  "Parse entire current buffer.
+BEGIN, END are ignored"
+
+  (let ((actions (wisi-elisp-parser-actions parser))
+       (gotos   (wisi-elisp-parser-gotos parser))
+       (parser-states ;; vector of parallel parser states
+        (vector
+         (make-wisi-elisp-parser-state
+          :label 0
+          :active  'shift
+          :stack   (make-vector wisi-parse-max-stack-size nil)
+          :sp      0
+          :pending nil)))
+       (active-parser-count 1)
+       active-parser-count-prev
+       (active 'shift)
+       (token nil)
+       some-pending
+       wisi-elisp-parse--indent)
+
+    (cl-case wisi--parse-action
+      (indent
+       (let ((line-count (1+ (count-lines (point-min) (point-max)))))
+        (setq wisi-elisp-parse--indent (make-vector line-count nil))
+        (wisi-elisp-lexer-reset line-count wisi--lexer)))
+
+      (navigate
+       (setq wisi-end-caches nil))
+
+      (t nil))
+
+    (setf (wisi-parser-lexer-errors parser) nil)
+    (setf (wisi-parser-parse-errors parser) nil)
+
+    ;; We assume the lexer relies on syntax properties
+    (when (< emacs-major-version 25) (syntax-propertize (point-max)))
+
+    (goto-char (point-min))
+    (forward-comment (point-max))
+    (aset (wisi-elisp-parser-state-stack (aref parser-states 0)) 0 0)
+
+    (setq token (funcall (wisi-elisp-parser-next-token parser)))
+    (setq wisi-elisp-parse-max-parallel-current (cons 0 0))
+
+    (while (not (eq active 'accept))
+      (setq active-parser-count-prev active-parser-count)
+      (setq some-pending nil)
+      (dotimes (parser-index (length parser-states))
+       (when (eq active (wisi-elisp-parser-state-active (aref parser-states 
parser-index)))
+         (let* ((parser-state (aref parser-states parser-index))
+                (result (wisi-elisp-parse-1 token parser-state (> 
active-parser-count 1) actions gotos)))
+           (when result
+             ;; spawn a new parser
+             (when (= active-parser-count wisi-parse-max-parallel)
+               (let* ((state (aref (wisi-elisp-parser-state-stack parser-state)
+                                   (wisi-elisp-parser-state-sp parser-state)))
+                      (msg (wisi-error-msg (concat "too many parallel parsers 
required in grammar state %d;"
+                                                   " simplify grammar, or 
increase `wisi-elisp-parse-max-parallel'")
+                                           state)))
+                 (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
+                 (signal 'wisi-parse-error msg)))
+
+             (let ((j (wisi-elisp-parse-free-parser parser-states)))
+               (cond
+                ((= j -1)
+                 ;; Add to parser-states; the new parser won't be executed
+                 ;; again in this parser-index loop.
+                 (setq parser-states (vconcat parser-states (vector nil)))
+                 (setq j (1- (length parser-states))))
+                ((< j parser-index)
+                 ;; The new parser won't be executed again in this
+                 ;; parser-index loop; nothing to do.
+                 )
+                (t
+                 ;; Don't let the new parser execute again in this
+                 ;; parser-index loop.
+                 (setq some-pending t)
+                 (setf (wisi-elisp-parser-state-active result)
+                       (cl-case (wisi-elisp-parser-state-active result)
+                         (shift 'pending-shift)
+                         (reduce 'pending-reduce)
+                        )))
+                 )
+               (setq active-parser-count (1+ active-parser-count))
+               (when (> active-parser-count (car 
wisi-elisp-parse-max-parallel-current))
+                 (setq wisi-elisp-parse-max-parallel-current (cons 
active-parser-count (point))))
+               (setf (wisi-elisp-parser-state-label result) j)
+               (aset parser-states j result))
+             (when (> wisi-debug 1)
+                (message "spawn parser (%d active)" active-parser-count)))
+
+           (when (eq 'error (wisi-elisp-parser-state-active parser-state))
+             (setq active-parser-count (1- active-parser-count))
+             (when (> wisi-debug 1)
+                (message "terminate parser (%d active)" active-parser-count))
+             (cl-case active-parser-count
+               (0
+                (cond
+                 ((= active-parser-count-prev 1)
+                  ;; We were not in a parallel parse; abandon parsing, report 
the error.
+                  (let* ((state (aref (wisi-elisp-parser-state-stack 
parser-state)
+                                      (wisi-elisp-parser-state-sp 
parser-state)))
+                         (msg (wisi-error-msg "syntax error in grammar state 
%d; unexpected %s, expecting one of %s"
+                                              state
+                                              (wisi-token-text token)
+                                              (mapcar 'car (aref actions 
state)))))
+                    (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
+                    (signal 'wisi-parse-error msg)))
+                 (t
+                  ;; Report errors from all parsers that failed on this token.
+                  (let ((msg))
+                    (dotimes (_ (length parser-states))
+                      (let* ((parser-state (aref parser-states parser-index))
+                             (state (aref (wisi-elisp-parser-state-stack 
parser-state)
+                                          (wisi-elisp-parser-state-sp 
parser-state))))
+                        (when (eq 'error (wisi-elisp-parser-state-active 
parser-state))
+                          (setq msg
+                                (concat msg
+                                        (when msg "\n")
+                                        (wisi-error-msg
+                                         "syntax error in grammar state %d; 
unexpected %s, expecting one of %s"
+                                         state
+                                         (wisi-token-text token)
+                                         (mapcar 'car (aref actions state)))))
+                          )))
+                    (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
+                    (signal 'wisi-parse-error msg)))
+                 ))
+
+               (1
+                (setf (wisi-elisp-parser-state-active parser-state) nil); 
Don't save error for later.
+                (wisi-elisp-parse-execute-pending (aref parser-states 
(wisi-elisp-parse-active-parser parser-states))))
+
+               (t
+                ;; We were in a parallel parse, and this parser
+                ;; failed; mark it inactive, don't save error for
+                ;; later.
+                (setf (wisi-elisp-parser-state-active parser-state) nil)
+                )))
+           )));; end dotimes
+
+      (when some-pending
+       ;; Change pending-* parsers to *.
+       (dotimes (parser-index (length parser-states))
+         (cond
+          ((eq (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'pending-shift)
+           (setf (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'shift))
+          ((eq (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'pending-reduce)
+           (setf (wisi-elisp-parser-state-active (aref parser-states 
parser-index)) 'reduce))
+          )))
+
+      (setq active (wisi-elisp-parsers-active parser-states 
active-parser-count))
+      (when (eq active 'shift)
+       (when (> active-parser-count 1)
+         (setq active-parser-count (wisi-elisp-parse-elim-identical parser 
parser-states active-parser-count)))
+
+       (setq token (funcall (wisi-elisp-parser-next-token parser))))
+    )
+    (when (> active-parser-count 1)
+      (error "ambiguous parse result"))
+
+    (cl-case wisi--parse-action
+      (indent
+       (wisi-elisp-parse--indent-leading-comments)
+       (wisi-elisp-parse--resolve-anchors))
+
+      (t nil))
+
+    ;; Return region parsed.
+    (cons (point-min) (point))
+    ))
+
+(defun wisi-elisp-parsers-active-index (parser-states)
+  ;; only called when active-parser-count = 1
+  (let ((result nil)
+       (i 0))
+    (while (and (not result)
+               (< i (length parser-states)))
+      (when (wisi-elisp-parser-state-active (aref parser-states i))
+       (setq result i))
+      (setq i (1+ i)))
+    result))
+
+(defun wisi-elisp-parsers-active (parser-states active-count)
+  "Return the type of parser cycle to execute.
+PARSER-STATES[*].active is the last action a parser took. If it
+was `shift', that parser used the input token, and should not be
+executed again until another input token is available, after all
+parsers have shifted the current token or terminated.
+
+Returns one of:
+
+`accept' : all PARSER-STATES have active set to nil or `accept' -
+done parsing
+
+`shift' : all PARSER-STATES have active set to nil, `accept', or
+`shift' - get a new token, execute `shift' parsers.
+
+`reduce' : some PARSER-STATES have active set to `reduce' - no new
+token, execute `reduce' parsers."
+  (let ((result nil)
+       (i 0)
+       (shift-count 0)
+       (accept-count 0)
+       active)
+    (while (and (not result)
+               (< i (length parser-states)))
+      (setq active (wisi-elisp-parser-state-active (aref parser-states i)))
+      (cond
+       ((eq active 'shift) (setq shift-count (1+ shift-count)))
+       ((eq active 'reduce) (setq result 'reduce))
+       ((eq active 'accept) (setq accept-count (1+ accept-count)))
+       )
+      (setq i (1+ i)))
+
+    (cond
+     (result )
+     ((= accept-count active-count)
+      'accept)
+     ((= (+ shift-count accept-count) active-count)
+      'shift)
+     (t
+      ;; all parsers in error state; should not get here
+      (error "all parsers in error state; programmer error"))
+     )))
+
+(defun wisi-elisp-parse-free-parser (parser-states)
+  "Return index to a non-active parser in PARSER-STATES, -1 if there is none."
+  (let ((result nil)
+       (i 0))
+    (while (and (not result)
+               (< i (length parser-states)))
+      (when (not (wisi-elisp-parser-state-active (aref parser-states i)))
+       (setq result i))
+      (setq i (1+ i)))
+    (if result result -1)))
+
+(defun wisi-elisp-parse-active-parser (parser-states)
+  "Return index to the first active parser in PARSER-STATES."
+  (let ((result nil)
+       (i 0))
+    (while (and (not result)
+               (< i (length parser-states)))
+      (when (wisi-elisp-parser-state-active (aref parser-states i))
+       (setq result i))
+      (setq i (1+ i)))
+    (unless result
+      (error "no active parsers"))
+    result))
+
+(defun wisi-elisp-parse-elim-identical (parser parser-states 
active-parser-count)
+  "Check for parsers in PARSER-STATES that have reached identical states 
eliminate one.
+Return new ACTIVE-PARSER-COUNT. Assumes all parsers have active
+nil, `shift', or `accept'."
+  ;; parser-states passed by reference; active-parser-count by copy
+  ;; see test/ada_mode-slices.adb for example
+  (dotimes (parser-i (1- (length parser-states)))
+    (when (wisi-elisp-parser-state-active (aref parser-states parser-i))
+      (dotimes (parser-j (- (length parser-states) parser-i 1))
+       (when (wisi-elisp-parser-state-active (aref parser-states (+ parser-i 
parser-j 1)))
+         (when (eq (wisi-elisp-parser-state-sp (aref parser-states parser-i))
+                    (wisi-elisp-parser-state-sp (aref parser-states (+ 
parser-i parser-j 1))))
+           (let ((compare t)
+                 exec)
+             (dotimes (stack-i (wisi-elisp-parser-state-sp (aref parser-states 
parser-i)))
+               (setq
+                compare
+                (and compare ;; bypass expensive 'arefs' after first stack 
item compare fail
+                     (equal (aref (wisi-elisp-parser-state-stack (aref 
parser-states parser-i)) stack-i)
+                            (aref (wisi-elisp-parser-state-stack (aref 
parser-states (+ parser-i parser-j 1)))
+                                  stack-i)))))
+             (when compare
+               ;; parser stacks are identical
+               (setq active-parser-count (1- active-parser-count))
+               (when (> wisi-debug 1)
+                 (message "terminate identical parser %d (%d active)"
+                          (+ parser-i parser-j 1) active-parser-count)
+                 (let ((state-i (aref parser-states parser-i))
+                       (state-j (aref parser-states (+ parser-i parser-j 1))))
+                   (message "%d actions:" (wisi-elisp-parser-state-label 
state-i))
+                   (mapc #'wisi-elisp-parse-debug-put-action 
(wisi-elisp-parser-state-pending state-i))
+
+                   (message "%d actions:" (wisi-elisp-parser-state-label 
state-j))
+                   (mapc #'wisi-elisp-parse-debug-put-action 
(wisi-elisp-parser-state-pending state-j))
+                   ))
+               (cl-ecase wisi-debug-identical
+                 (0
+                  (setq exec parser-i)
+                  (setf (wisi-elisp-parser-state-active (aref parser-states (+ 
parser-i parser-j 1))) nil))
+
+                 (1
+                  (setq exec (+ parser-i parser-j 1))
+                  (setf (wisi-elisp-parser-state-active (aref parser-states 
parser-i)) nil))
+
+                 (2
+                  (let ((msg "identical parser stacks"))
+                    (push (make-wisi--parse-error :pos (point) :message msg) 
(wisi-parser-parse-errors parser))
+                    (signal 'wisi-parse-error msg)))
+                 )
+               (when (= active-parser-count 1)
+                 ;; The actions for the two parsers are not
+                 ;; identical, but most of the time either is good
+                 ;; enough for indentation and navigation, so we just
+                 ;; do the actions for the one that is not
+                 ;; terminating. Some times, a significant action is
+                 ;; lost. In that case, turn on
+                 ;; ‘wisi-debug-identical’ to investigate fixing it.
+                 (wisi-elisp-parse-execute-pending (aref parser-states exec)))
+               ))))
+       )))
+  active-parser-count)
+
+(defun wisi-elisp-parse-exec-action (func nonterm tokens)
+  "Execute action if TOKENS not null."
+  ;; `tokens' is null when all tokens in a grammar statement are
+  ;; optional and not present.
+  (unless wisi-action-disable
+    (if (< 0 (length tokens))
+       (when wisi--parse-action
+         (funcall func nonterm tokens))
+
+      (when (> wisi-debug 1)
+       (message "... action skipped; no tokens"))
+      )))
+
+(defvar wisi-elisp-parser-state nil
+  "Let-bound in `wisi-elisp-parse-reduce', used in `wisi-parse-find-token'.")
+
+(defun wisi-elisp-parse-debug-put-action (action)
+  ;; Action is (semantic-function nonterm [tokens])
+  (message "%s [%s]"
+          (nth 0 action)
+          (mapcar #'wisi-tok-debug-image (nth 2 action))))
+
+(defun wisi-elisp-parse-execute-pending (parser-state)
+  (let ((wisi-elisp-parser-state parser-state);; reference, for 
wisi-parse-find-token
+       (pending (wisi-elisp-parser-state-pending parser-state)))
+
+    (when (> wisi-debug 1)
+      (message "%d: pending actions:" (wisi-elisp-parser-state-label 
parser-state)))
+
+    (while pending
+      (when (> wisi-debug 1) (wisi-elisp-parse-debug-put-action (car pending)))
+
+      (let ((func-args (pop pending)))
+       (wisi-elisp-parse-exec-action (nth 0 func-args) (nth 1 func-args) 
(cl-caddr func-args)))
+      )
+    (setf (wisi-elisp-parser-state-pending parser-state) nil)
+    ))
+
+(defmacro wisi-elisp-parse-action (i al)
+  "Return the parser action.
+I is a token item number and AL is the list of (item . action)
+available at current state.  The first element of AL contains the
+default action for this state."
+  `(cdr (or (assq ,i ,al) (car ,al))))
+
+(defun wisi-elisp-parse-1 (token parser-state pendingp actions gotos)
+  "Perform one shift or reduce on PARSER-STATE.
+If PENDINGP, push actions onto PARSER-STATE.pending; otherwise execute them.
+See `wisi-elisp-parse' for full details.
+Return nil or new parser (a wisi-elisp-parser-state struct)."
+  (let* ((state (aref (wisi-elisp-parser-state-stack parser-state)
+               (wisi-elisp-parser-state-sp parser-state)))
+        (parse-action (wisi-elisp-parse-action (wisi-tok-token token) (aref 
actions state)))
+        new-parser-state)
+
+    (when (> wisi-debug 1)
+      ;; output trace info
+      (if (> wisi-debug 2)
+         (progn
+           ;; put top 10 stack items
+           (let* ((count (min 20 (wisi-elisp-parser-state-sp parser-state)))
+                  (msg (make-vector (+ 1 count) nil)))
+             (dotimes (i count)
+               (aset msg (- count i)
+                     (aref (wisi-elisp-parser-state-stack parser-state)
+                           (- (wisi-elisp-parser-state-sp parser-state) i)))
+               )
+             (message "%d: %s: %d: %s"
+                      (wisi-elisp-parser-state-label parser-state)
+                      (wisi-elisp-parser-state-active parser-state)
+                      (wisi-elisp-parser-state-sp parser-state)
+                      msg))
+           (message "   %d: %s: %s" state (wisi-tok-debug-image token) 
parse-action))
+       (message "%d: %d: %s: %s" (wisi-elisp-parser-state-label parser-state) 
state token parse-action)))
+
+    (when (and (listp parse-action)
+              (not (symbolp (car parse-action))))
+      ;; Conflict; spawn a new parser.
+      (setq new-parser-state
+           (make-wisi-elisp-parser-state
+            :active  nil
+            :stack   (vconcat (wisi-elisp-parser-state-stack parser-state))
+            :sp      (wisi-elisp-parser-state-sp parser-state)
+            :pending (wisi-elisp-parser-state-pending parser-state)))
+
+      (wisi-elisp-parse-2 (cadr parse-action) token new-parser-state t gotos)
+      (setq pendingp t)
+      (setq parse-action (car parse-action))
+      );; when
+
+    ;; current parser
+    (wisi-elisp-parse-2 parse-action token parser-state pendingp gotos)
+
+    new-parser-state))
+
+(defun wisi-elisp-parse-2 (action token parser-state pendingp gotos)
+  "Execute parser ACTION (must not be a conflict).
+Return nil."
+  (cond
+   ((eq action 'accept)
+    (setf (wisi-elisp-parser-state-active parser-state) 'accept))
+
+   ((eq action 'error)
+    (setf (wisi-elisp-parser-state-active parser-state) 'error))
+
+   ((natnump action)
+    ;; Shift token and new state (= action) onto stack
+    (let ((stack (wisi-elisp-parser-state-stack parser-state)); reference
+         (sp (wisi-elisp-parser-state-sp parser-state))); copy
+      (setq sp (+ sp 2))
+      (aset stack (1- sp) token)
+      (aset stack sp action)
+      (setf (wisi-elisp-parser-state-sp parser-state) sp))
+    (setf (wisi-elisp-parser-state-active parser-state) 'shift))
+
+   (t
+    (wisi-elisp-parse-reduce action parser-state pendingp gotos)
+    (setf (wisi-elisp-parser-state-active parser-state) 'reduce))
+   ))
+
+(defun wisi-elisp-parse-first-last (stack i j)
+  "Return a pair (FIRST . LAST), indices for the first and last
+non-empty tokens for a nonterminal; or nil if all tokens are
+empty. STACK is the parser stack.  I and J are the indices in
+STACK of the first and last tokens of the nonterminal."
+  (let ((start (car (wisi-tok-region (aref stack i))))
+        (end   (cdr (wisi-tok-region (aref stack j)))))
+    (while (and (or (not start) (not end))
+               (/= i j))
+      (cond
+       ((not start)
+       ;; item i is an empty production
+       (setq start (car (wisi-tok-region (aref stack (setq i (+ i 2)))))))
+
+       ((not end)
+       ;; item j is an empty production
+       (setq end (cdr (wisi-tok-region (aref stack (setq j (- j 2)))))))
+
+       (t (setq i j))))
+
+    (when (and start end)
+      (cons i j))
+    ))
+
+(cl-defmethod wisi-parse-find-token ((_parser wisi-elisp-parser) token-symbol)
+  "Find token with TOKEN-SYMBOL on current parser stack, return token struct.
+For use in grammar actions."
+  ;; Called from wisi-parse-exec-action in wisi-parse-reduce
+  (let* ((stack (wisi-elisp-parser-state-stack wisi-elisp-parser-state))
+        (sp (1- (wisi-elisp-parser-state-sp wisi-elisp-parser-state)))
+        (tok (aref stack sp)))
+    (while (and (> sp 0)
+               (not (eq token-symbol (wisi-tok-token tok))))
+      (setq sp (- sp 2))
+      (setq tok (aref stack sp)))
+    (if (= sp 0)
+       (error "token %s not found on parse stack" token-symbol)
+      tok)
+    ))
+
+(cl-defmethod wisi-parse-stack-peek ((_parser wisi-elisp-parser) n)
+  ;; IMPROVEME: store stack in parser
+  (let* ((stack (wisi-elisp-parser-state-stack wisi-elisp-parser-state))
+        (sp (1- (wisi-elisp-parser-state-sp wisi-elisp-parser-state)))
+        (i (- sp (* 2 n))))
+    (when (> i 0)
+      (aref stack i))))
+
+(defun wisi-elisp-parse-reduce (action parser-state pendingp gotos)
+  "Reduce PARSER-STATE.stack, and execute or pend ACTION."
+  (let* ((wisi-elisp-parser-state parser-state);; reference, for 
wisi-parse-find-token
+        (stack (wisi-elisp-parser-state-stack parser-state)); reference
+        (sp (wisi-elisp-parser-state-sp parser-state)); copy
+        (token-count (nth 2 action))
+        (nonterm (nth 0 action))
+        (first-last (when (> token-count 0)
+                      (wisi-elisp-parse-first-last stack (- sp (* 2 (1- 
token-count)) 1) (1- sp))))
+        (nonterm-region (when first-last
+                          (cons
+                           (car (wisi-tok-region (aref stack (car 
first-last))))
+                           (cdr (wisi-tok-region (aref stack (cdr 
first-last)))))))
+        (post-reduce-state (aref stack (- sp (* 2 token-count))))
+        (new-state (cdr (assoc nonterm (aref gotos post-reduce-state))))
+        (tokens (make-vector token-count nil))
+        line first comment-line comment-end)
+
+    (when (not new-state)
+      (error "no goto for %s %d" nonterm post-reduce-state))
+
+    (dotimes (i token-count) ;;  i = 0 .. (1- token-count); last token = 0, 
first token = (1- token-count)
+      (let ((tok (aref stack (- sp (* 2 i) 1))))
+       (when (nth 1 action)
+         ;; don't need wisi-tokens for a null user action
+         (aset tokens (- token-count i 1) tok))
+
+       (when (eq wisi--parse-action 'indent)
+         (setq line (or (wisi-tok-line tok) line))
+         (cond
+          ((numberp (wisi-tok-first tok))
+           (setq first (wisi-tok-first tok)))
+
+          ((wisi-tok-first tok)
+           (setq first (wisi-tok-line tok)))
+
+          ((and (not (= i 0))
+                      (wisi-tok-comment-line tok))
+           ;; comment lines following last token are not included in nonterm
+           ;; test/ada_mode-nominal.ads Object_Access_Type_5a
+           ;; test/ada_mode-parens.adb
+           (setq first (wisi-tok-comment-line tok)))
+          )
+         (when (and (= i 0)
+                    (wisi-tok-comment-line tok))
+           (setq comment-line (wisi-tok-comment-line tok))
+           (setq comment-end (wisi-tok-comment-end tok)))
+       )))
+
+    (setq sp (+ 2 (- sp (* 2 token-count))))
+    (aset stack (1- sp)
+         (make-wisi-tok
+          :token nonterm
+          :region nonterm-region
+          :nonterminal t
+          :line line
+          :first first
+          :comment-line comment-line
+          :comment-end comment-end))
+    (aset stack sp new-state)
+    (setf (wisi-elisp-parser-state-sp parser-state) sp)
+
+    (when (nth 1 action)
+      ;; nothing to do for a null user action
+      (if pendingp
+         (if (wisi-elisp-parser-state-pending parser-state)
+             (setf (wisi-elisp-parser-state-pending parser-state)
+                   (append (wisi-elisp-parser-state-pending parser-state)
+                           (list (list (nth 1 action) nonterm tokens))))
+           (setf (wisi-elisp-parser-state-pending parser-state)
+                 (list (list (nth 1 action) nonterm tokens))))
+
+       ;; Not pending.
+       (wisi-elisp-parse-exec-action (nth 1 action) nonterm tokens)
+       ))
+    ))
+
+;;;; navigate grammar actions
+
+(defun wisi-elisp-parse--set-end (start-mark end-mark)
+  "Set END-MARK on all caches in `wisi-end-caches' in range START-MARK 
END-MARK,
+delete from `wisi-end-caches'."
+  (let ((i 0)
+       pos cache)
+    (while (< i (length wisi-end-caches))
+      (setq pos (nth i wisi-end-caches))
+      (setq cache (wisi-get-cache pos))
+
+      (if (and (>= pos start-mark)
+              (<  pos end-mark))
+         (progn
+           (setf (wisi-cache-end cache) end-mark)
+           (setq wisi-end-caches (delq pos wisi-end-caches)))
+
+       ;; else not in range
+       (setq i (1+ i)))
+      )))
+
+(defvar wisi-tokens nil
+  ;; Not wisi-elisp-parse--tokens for ease in debugging actions, and
+  ;; to match lots of doc strings.
+  "Array of ‘wisi-tok’ structures for the right hand side of the current 
production.
+Let-bound in parser semantic actions.")
+
+(defvar wisi-nterm nil
+  ;; Not wisi-elisp-parse--nterm for ease in debugging actions
+  "The token id for the left hand side of the current production.
+Let-bound in parser semantic actions.")
+
+(defun wisi-statement-action (pairs)
+  ;; Not wisi-elisp-parse--statement-action to match existing grammar files
+  "Cache navigation information in text properties of tokens.
+Intended as a grammar non-terminal action.
+
+PAIRS is a vector of the form [TOKEN-NUMBER CLASS TOKEN-NUMBER
+CLASS ...] where TOKEN-NUMBER is the (1 indexed) token number in
+the production, CLASS is the wisi class of that token. Use in a
+grammar action as:
+  (wisi-statement-action [1 statement-start 7 statement-end])"
+  (when (eq wisi--parse-action 'navigate)
+    (save-excursion
+      (let ((first-item t)
+           first-keyword-mark
+           (override-start nil)
+           (i 0))
+       (while (< i (length pairs))
+         (let* ((number (1- (aref pairs i)))
+                (region (wisi-tok-region (aref wisi-tokens number)))
+                (token (wisi-tok-token (aref wisi-tokens number)))
+                (class (aref pairs (setq i (1+ i))))
+                (mark (when region (copy-marker (car region) t)))
+                cache)
+
+           (setq i (1+ i))
+
+           (unless
+               (cond
+                ((fboundp 'seq-contains)  ;; emacs < 27
+                   (seq-contains wisi-class-list class))
+                ((fboundp 'seq-contains-p) ;; emacs >= 27
+                 (seq-contains-p wisi-class-list class)))
+             (error "%s not in wisi-class-list" class))
+
+           (if region
+               (progn
+                 (if (setq cache (wisi-get-cache (car region)))
+                     ;; We are processing a previously set non-terminal; ie 
simple_statement in
+                     ;;
+                     ;; statement : label_opt simple_statement
+                     ;;
+                     ;; override nonterm, class, containing
+                     (progn
+                       (setf (wisi-cache-class cache) (or override-start 
class))
+                       (setf (wisi-cache-nonterm cache) wisi-nterm)
+                       (setf (wisi-cache-containing cache) first-keyword-mark)
+                       (if wisi-end-caches
+                           (push (car region) wisi-end-caches)
+                         (setq wisi-end-caches (list (car region)))
+                         ))
+
+                   ;; else create new cache
+                   (with-silent-modifications
+                     (put-text-property
+                      (car region)
+                      (1+ (car region))
+                      'wisi-cache
+                      (wisi-cache-create
+                       :nonterm    wisi-nterm
+                       :token      token
+                       :last       (- (cdr region) (car region))
+                       :class      (or override-start class)
+                       :containing first-keyword-mark)
+                      ))
+                   (if wisi-end-caches
+                       (push (car region) wisi-end-caches)
+                     (setq wisi-end-caches (list (car region)))
+                     ))
+
+                 (when first-item
+                   (setq first-item nil)
+                   (when (or override-start
+                             (eq class 'statement-start))
+                     (setq override-start nil)
+                     (setq first-keyword-mark mark)))
+
+                 (when (eq class 'statement-end)
+                   (wisi-elisp-parse--set-end first-keyword-mark (copy-marker 
(car region) t)))
+                 )
+
+             ;; region is nil when a production is empty; if the first
+             ;; token is a start, override the class on the next token.
+             (when (and first-item
+                        (eq class 'statement-start))
+               (setq override-start class)))
+           ))
+       ))))
+
+(defun wisi-name-action (name)
+  ;; Not wisi-elisp-parse--name-action to simplify grammar files
+  "NAME is a token number; mark that token with the 'wisi-name text property.
+Intended as a grammar action."
+  (when (eq wisi--parse-action 'navigate)
+    (let ((region (wisi-tok-region (aref wisi-tokens (1- name)))))
+      (when region
+       ;; region can be null on an optional or virtual token
+       (with-silent-modifications
+         (put-text-property (car region) (cdr region) 'wisi-name t))
+       ))))
+
+(defun wisi-containing-action (containing-token contained-token)
+  ;; Not wisi-elisp-parse--containing-action to match existing grammar files
+  "Set containing marks in all tokens in CONTAINED-TOKEN
+with null containing mark to marker pointing to CONTAINING-TOKEN.
+If CONTAINING-TOKEN is empty, the next token number is used."
+  (when (eq wisi--parse-action 'navigate)
+    (let* ((containing-tok (aref wisi-tokens (1- containing-token)))
+          (containing-region (wisi-tok-region containing-tok))
+          (contained-tok (aref wisi-tokens (1- contained-token)))
+          (contained-region (wisi-tok-region contained-tok)))
+
+      (unless containing-region
+       (signal 'wisi-parse-error
+               (wisi-error-msg
+                "wisi-containing-action: containing-region '%s' is empty. 
grammar error; bad action"
+                (wisi-tok-token containing-tok))))
+
+      (unless (or (not contained-region) ;; contained-token is empty
+                 (wisi-get-cache (car containing-region)))
+       (signal 'wisi-parse-error
+               (wisi-error-msg
+                "wisi-containing-action: containing-token '%s' has no cache. 
grammar error; missing action"
+                (wisi-token-text (aref wisi-tokens (1- containing-token))))))
+
+      (when contained-region
+         ;; nil when empty production, may not contain any caches
+         (save-excursion
+           (goto-char (cdr contained-region))
+           (let ((cache (wisi-backward-cache))
+                 (mark (copy-marker (car containing-region) t)))
+             (while cache
+
+               ;; skip blocks that are already marked
+               (while (and (>= (point) (car contained-region))
+                           (markerp (wisi-cache-containing cache)))
+                 (goto-char (wisi-cache-containing cache))
+                 (setq cache (wisi-get-cache (point))))
+
+               (if (or (and (= (car containing-region) (car contained-region))
+                            (<= (point) (car contained-region)))
+                       (< (point) (car contained-region)))
+                   ;; done
+                   (setq cache nil)
+
+                 ;; else set mark, loop
+                 (setf (wisi-cache-containing cache) mark)
+                 (setq cache (wisi-backward-cache)))
+               ))))
+      )))
+
+(defun wisi-elisp-parse--match-token (cache tokens start)
+  "Return t if CACHE has id from TOKENS and is at START or has containing 
equal to START.
+point must be at cache token start.
+TOKENS is a vector [number token_id token_id ...].
+number is ignored."
+  (let ((i 1)
+       (done nil)
+       (result nil)
+       token)
+    (when (or (= start (point))
+             (and (wisi-cache-containing cache)
+                  (= start (wisi-cache-containing cache))))
+      (while (and (not done)
+                 (< i (length tokens)))
+       (setq token (aref tokens i))
+       (if (eq token (wisi-cache-token cache))
+           (setq result t
+                 done t)
+         (setq i (1+ i)))
+       ))
+    result))
+
+(defun wisi-motion-action (token-numbers)
+  ;; Not wisi-elisp-parse--motion-action to match existing grammar files
+  "Set prev/next marks in all tokens given by TOKEN-NUMBERS.
+TOKEN-NUMBERS is a vector with each element one of:
+
+number: the token number; mark that token
+
+vector [number token_id]:
+vector [number token_id token_id ...]:
+   mark all tokens in number nonterminal matching token_id with nil prev/next."
+  (when (eq wisi--parse-action 'navigate)
+    (save-excursion
+      (let (prev-keyword-mark
+           prev-cache
+           token
+           start
+           cache
+           mark
+           (i 0))
+       (while (< i (length token-numbers))
+         (let ((token-number (aref token-numbers i))
+               region)
+           (setq i (1+ i))
+           (cond
+            ((numberp token-number)
+             (setq token (aref wisi-tokens (1- token-number)))
+             (setq region (wisi-tok-region token))
+             (when region
+               (unless start (setq start (car region)))
+               (setq cache (wisi-get-cache (car region)))
+               (unless cache (error "no cache on token %d; add to 
statement-action" token-number))
+               (setq mark (copy-marker (car region) t))
+
+               (if prev-keyword-mark
+                   (progn
+                     (setf (wisi-cache-prev cache) prev-keyword-mark)
+                     (setf (wisi-cache-next prev-cache) mark)
+                     (setq prev-keyword-mark mark)
+                     (setq prev-cache cache))
+
+                 ;; else first token; save as prev
+                 (setq prev-keyword-mark mark)
+                 (setq prev-cache cache))
+               ))
+
+            ((vectorp token-number)
+             ;; token-number may contain 1 or more token_ids
+             ;; the corresponding region may be empty
+             ;; there may not have been a prev keyword
+             (setq region (wisi-tok-region (aref wisi-tokens (1- (aref 
token-number 0)))))
+             (when region ;; not an empty token
+               ;; We must search for all targets at the same time, to
+               ;; get the motion order right.
+               (unless start (setq start (car region)))
+               (goto-char (car region))
+               (setq cache (wisi-get-cache (point)))
+               (unless cache (error "no cache at %d; add to statement-action" 
(car region)))
+               (while (< (point) (cdr region))
+                 (when (wisi-elisp-parse--match-token cache token-number start)
+                   (setq mark (copy-marker (point) t))
+
+                   (if prev-keyword-mark
+                       ;; Don't include this token if prev/next
+                       ;; already set by a lower level statement,
+                       ;; such as a nested if/then/elsif/end if.
+                       (when (and (null (wisi-cache-prev cache))
+                                  (null (wisi-cache-next prev-cache)))
+                         (setf (wisi-cache-prev cache) prev-keyword-mark)
+                         (setf (wisi-cache-next prev-cache) mark)
+                         (setq prev-keyword-mark mark)
+                         (setq prev-cache cache))
+
+                     ;; else first token; save as prev
+                     (setq prev-keyword-mark mark)
+                     (setq prev-cache cache)))
+
+                 (setq cache (wisi-forward-cache))
+                 )))
+
+            (t
+             (error "unexpected token-number %s" token-number))
+            )
+
+           ))
+       ))))
+
+;;;; Face grammar actions
+
+(defun wisi-elisp-parse--face-put-cache (region class)
+  "Put a ’wisi-face’ cache with class CLASS on REGION."
+  (when (> wisi-debug 1)
+    (message "face: put cache %s:%s" region class))
+  (with-silent-modifications
+    (put-text-property
+     (car region)
+     (1+ (car region))
+     'wisi-face
+     (wisi-cache-create
+      :last (- (cdr region) (car region))
+      :class class)
+     )))
+
+(defun wisi-face-mark-action (pairs)
+  ;; Not wisi-elisp-parse--face-mark-action to match existing grammar files
+  "PAIRS is a vector of TOKEN CLASS pairs; mark TOKEN (token number)
+as having face CLASS (prefix or suffix).
+Intended as a grammar action."
+  (when (eq wisi--parse-action 'face)
+    (let ((i 0))
+      (while (< i (length pairs))
+       (let ((region (wisi-tok-region (aref wisi-tokens (1- (aref pairs i)))))
+             (class (aref pairs (setq i (1+ i)))))
+         (setq i (1+ i))
+         (when region
+           ;; region can be null on an optional or virtual token
+           (let ((cache (get-text-property (car region) 'wisi-face)))
+             (if cache
+                 ;; previously marked; extend this cache, delete any others
+                 (progn
+                   (with-silent-modifications
+                     (remove-text-properties (+ (car region) (wisi-cache-last 
cache)) (cdr region) '(wisi-face nil)))
+                   (setf (wisi-cache-class cache) class)
+                   (setf (wisi-cache-last cache) (- (cdr region) (car 
region))))
+
+               ;; else not previously marked
+               (wisi-elisp-parse--face-put-cache region class)))
+           ))
+       ))))
+
+(defun wisi-face-remove-action (tokens)
+  ;; Not wisi-elisp-parse--face-remove-action to match existing grammar files
+  "Remove face caches and faces in TOKENS.
+Intended as a grammar action.
+
+TOKENS is a vector of token numbers."
+  (when (eq wisi--parse-action 'face)
+    (let ((i 0))
+      (while (< i (length tokens))
+       (let* ((number (1- (aref tokens i)))
+              (region (wisi-tok-region (aref wisi-tokens number)))
+              face-cache)
+
+         (setq i (1+ i))
+
+         (when region
+           (let ((pos (car region)))
+             (while (< pos (cdr region))
+               (when (setq face-cache (get-text-property pos 'wisi-face))
+                 (when (> wisi-debug 1)
+                   (message "face: remove face %s" (cons pos (+ pos 
(wisi-cache-last face-cache)))))
+                 (with-silent-modifications
+                   (remove-text-properties
+                    pos (+ pos (wisi-cache-last face-cache))
+                    (list
+                     'wisi-face nil
+                     'font-lock-face nil
+                     'fontified t))))
+               (setq pos (next-single-property-change
+                          (+ pos (or (and face-cache
+                                          (wisi-cache-last face-cache))
+                                     0))
+                          'wisi-face nil (cdr region)))
+               )))
+         )))))
+
+(defun wisi-elisp-parse--face-action-1 (face region)
+  "Apply FACE to REGION."
+  (when region
+    (when (> wisi-debug 1)
+      (message "face: add face %s:%s" region face))
+    (with-silent-modifications
+      (add-text-properties
+       (car region) (cdr region)
+       (list
+       'font-lock-face face
+       'fontified t)))
+    ))
+
+(defun wisi-face-apply-action (triples)
+  ;; Not wisi-elisp-parse--face-apply-action to match existing grammar files
+  "Set face information in `wisi-face' text properties of tokens.
+Intended as a grammar non-terminal action.
+
+TRIPLES is a vector of the form [TOKEN-NUMBER PREFIX-FACE SUFFIX-FACE ...]
+
+In the first ’wisi-face’ cache in each token region, apply
+PREFIX-FACE to class PREFIX, SUFFIX-FACE to class SUFFIX, or
+SUFFIX-FACE to all of the token region if there is no ’wisi-face’
+cache."
+  (when (eq wisi--parse-action 'face)
+    (let (number prefix-face suffix-face (i 0))
+      (while (< i (length triples))
+       (setq number (aref triples i))
+       (setq prefix-face (aref triples (setq i (1+ i))))
+       (setq suffix-face (aref triples (setq i (1+ i))))
+       (cond
+        ((integerp number)
+         (let* ((token-region (wisi-tok-region (aref wisi-tokens (1- number))))
+                (pos (car token-region))
+                (j 0)
+                (some-cache nil)
+                cache)
+           (when token-region
+             ;; region can be null for an optional or virtual token
+             (while (< j 2)
+               (setq cache (get-text-property pos 'wisi-face))
+               (cond
+                ((and (not some-cache)
+                      (null cache))
+                 ;; cache is null when applying a face to a token
+                 ;; directly, without first calling
+                 ;; wisi-face-mark-action. Or when there is a
+                 ;; previously applied face in a lower level token,
+                 ;; such as a numeric literal.
+                 (wisi-elisp-parse--face-action-1 suffix-face token-region))
+
+                ((and cache
+                      (eq 'prefix (wisi-cache-class cache)))
+                 (setq some-cache t)
+                 (wisi-elisp-parse--face-action-1 prefix-face 
(wisi-cache-region cache pos)))
+
+                ((and cache
+                      (eq 'suffix (wisi-cache-class cache)))
+                 (setq some-cache t)
+                 (wisi-elisp-parse--face-action-1 suffix-face 
(wisi-cache-region cache pos)))
+
+                (t
+                 ;; don’t apply a face
+                 nil)
+                )
+
+               (setq j (1+ j))
+               (if suffix-face
+                   (setq pos (next-single-property-change (+ 2 pos) 'wisi-face 
nil (cdr token-region)))
+                 (setq j 2))
+               ))))
+
+        (t
+         ;; catch conversion errors from previous grammar syntax
+         (error "wisi-face-apply-action with non-integer token number"))
+        )
+       (setq i (1+ i))
+       ))))
+
+(defun wisi-face-apply-list-action (triples)
+  ;; Not wisi-elisp-parse--face-apply-list-action to match existing grammar 
files
+  "Similar to ’wisi-face-apply-action’, but applies faces to all
+tokens with a `wisi-face' cache in the wisi-tokens[token-number]
+region, and does not apply a face if there are no such caches."
+  (when (eq wisi--parse-action 'face)
+    (let (number token-region face-region prefix-face suffix-face cache (i 0) 
pos)
+      (while (< i (length triples))
+       (setq number (aref triples i))
+       (setq prefix-face (aref triples (setq i (1+ i))))
+       (setq suffix-face (aref triples (setq i (1+ i))))
+       (cond
+        ((integerp number)
+         (setq token-region (wisi-tok-region (aref wisi-tokens (1- number))))
+         (when token-region
+           ;; region can be null for an optional token
+           (setq pos (car token-region))
+           (while (and pos
+                       (< pos (cdr token-region)))
+             (setq cache (get-text-property pos 'wisi-face))
+             (setq face-region (wisi-cache-region cache pos))
+             (cond
+              ((or (null (wisi-cache-class cache))
+                   (eq 'prefix (wisi-cache-class cache)))
+               (wisi-elisp-parse--face-action-1 prefix-face face-region))
+              ((eq 'suffix (wisi-cache-class cache))
+               (wisi-elisp-parse--face-action-1 suffix-face face-region))
+
+              (t
+               (error "wisi-face-apply-list-action: face cache class is not 
prefix or suffix")))
+
+             (setq pos (next-single-property-change (1+ pos) 'wisi-face nil 
(cdr token-region)))
+             )))
+        (t
+         ;; catch conversion errors from previous grammar syntax
+         (error "wisi-face-apply-list-action with non-integer token number"))
+        )
+       (setq i (1+ i))
+       ))))
+
+;;;; indent grammar actions
+
+(defvar wisi-elisp-parse-indent-hanging-function nil
+  "Language-specific implementation of `wisi-hanging', `wisi-hanging%'.
+A function taking args TOK DELTA1 DELTA2 OPTION NO-ACCUMULATE,
+and returning an indent.
+TOK is a `wisi-tok' struct for the token being indented.
+DELTA1, DELTA2 are the indents of the first and following lines
+within the nonterminal.  OPTION is non-nil if action is `wisi-hanging%'.
+point is at start of TOK, and may be moved.")
+(make-variable-buffer-local 'wisi-elisp-parse-indent-hanging-function)
+
+(defvar wisi-token-index nil
+  ;; Not wisi-elisp-parse--token-index for backward compatibility
+  "Index of current token in `wisi-tokens'.
+Let-bound in `wisi-indent-action', for grammar actions.")
+
+(defvar wisi-indent-comment nil
+  ;; Not wisi-elisp-parse--indent-comment for backward compatibility
+  "Non-nil if computing indent for comment.
+Let-bound in `wisi-indent-action', for grammar actions.")
+
+(defun wisi-elisp-parse--apply-int (i delta)
+  "Add DELTA (an integer) to the indent at index I."
+  (let ((indent (aref wisi-elisp-parse--indent i))) ;; reference if list
+
+    (cond
+     ((null indent)
+      (aset wisi-elisp-parse--indent i delta))
+
+     ((integerp indent)
+      (aset wisi-elisp-parse--indent i (+ delta indent)))
+
+     ((listp indent)
+      (cond
+       ((eq 'anchor (car indent))
+       (cond
+        ((null (nth 2 indent))
+         (setf (nth 2 indent) delta))
+
+        ((integerp (nth 2 indent))
+         (setf (nth 2 indent) (+ delta (nth 2 indent))))
+
+        ;; else anchored; not affected by this delta
+        ))
+
+       ((eq 'anchored (car indent))
+       ;; not affected by this delta
+       )))
+
+     (t
+      (error "wisi-elisp-parse--apply-int: invalid form : %s" indent))
+     )))
+
+(defun wisi-elisp-parse--apply-anchored (delta i)
+  "Apply DELTA (an anchored indent) to indent I."
+  ;; delta is from wisi-anchored; ('anchored 1 delta no-accumulate)
+  (let ((indent (aref wisi-elisp-parse--indent i))
+       (accumulate (not (nth 3 delta))))
+
+    (when delta
+      (cond
+       ((null indent)
+       (aset wisi-elisp-parse--indent i (seq-take delta 3)))
+
+       ((integerp indent)
+       (when accumulate
+         (let ((temp (seq-take delta 3)))
+           (setf (nth 2 temp) (+ indent (nth 2 temp)))
+           (aset wisi-elisp-parse--indent i temp))))
+
+       ((and (listp indent)
+            (eq 'anchor (car indent))
+            (or (null (nth 2 indent))
+                (integerp (nth 2 indent))))
+       (when (or (null (nth 2 indent))
+                 accumulate)
+         (let ((temp (seq-take delta 3)))
+           (cond
+            ((null (nth 2 indent))
+             (setf (nth 2 indent) temp))
+
+            (t
+             (setf (nth 2 temp) (+ (nth 2 indent) (nth 2 temp)))
+             (setf (nth 2 indent) temp))))
+         ))
+       ))))
+
+(defun wisi-elisp-parse--indent-null-p (indent)
+  (or (null indent)
+      (and (eq 'anchor (nth 0 indent))
+          (null (nth 2 indent)))))
+
+(defun wisi-elisp-parse--indent-token-1 (line end delta)
+  "Apply indent DELTA to all lines from LINE (a line number) thru END (a 
buffer position)."
+  (let ((i (1- line));; index to wisi-elisp-lexer-line-begin, 
wisi-elisp-parse--indent
+       (paren-first (when (and (listp delta)
+                               (eq 'hanging (car delta)))
+                      (nth 2 delta))))
+
+    (while (<= (aref (wisi-elisp-lexer-line-begin wisi--lexer) i) end)
+      (if
+         (and ;; no check for called from wisi--indent-comment;
+              ;; comments within tokens are indented by
+              ;; wisi--indent-token
+              wisi-indent-comment-col-0
+              (= 11 (syntax-class (syntax-after (aref 
(wisi-elisp-lexer-line-begin wisi--lexer) i)))))
+         (wisi-elisp-parse--apply-int i 0)
+       (cond
+        ((integerp delta)
+         (wisi-elisp-parse--apply-int i delta))
+
+        ((listp delta)
+         (cond
+          ((eq 'anchored (car delta))
+           (wisi-elisp-parse--apply-anchored delta i))
+
+          ((eq 'hanging (car delta))
+           ;; from wisi-hanging; delta is ('hanging first-line nest delta1 
delta2 no-accumulate)
+           ;; delta1, delta2 may be anchored
+           (when (or (not (nth 5 delta))
+                     (wisi-elisp-parse--indent-null-p (aref 
wisi-elisp-parse--indent i)))
+             (if (= i (1- (nth 1 delta)))
+                 ;; apply delta1
+                 (let ((delta1 (nth 3 delta)))
+                   (cond
+                    ((integerp delta1)
+                     (wisi-elisp-parse--apply-int i delta1))
+
+                    (t ;; anchored
+                     (wisi-elisp-parse--apply-anchored delta1 i))
+                    ))
+
+               ;; don't apply hanging indent in nested parens.
+               ;; test/ada_mode-parens.adb
+               ;; No_Conditional_Set : constant Ada.Strings.Maps.Character_Set 
:=
+               ;;   Ada.Strings.Maps."or"
+               ;;     (Ada.Strings.Maps.To_Set (' '),
+               (when (= paren-first
+                        (nth 0 (save-excursion (syntax-ppss (aref 
(wisi-elisp-lexer-line-begin wisi--lexer) i)))))
+                 (let ((delta2 (nth 4 delta)))
+                   (cond
+                    ((integerp delta2)
+                     (wisi-elisp-parse--apply-int i delta2))
+
+                    (t ;; anchored
+                     (wisi-elisp-parse--apply-anchored delta2 i))
+                    )))
+               )))
+
+          (t
+           (error "wisi-elisp-parse--indent-token-1: invalid delta: %s" delta))
+          )) ;; listp delta
+
+        (t
+         (error "wisi-elisp-parse--indent-token-1: invalid delta: %s" delta))
+        ))
+      (setq i (1+ i))
+      )))
+
+(defun wisi-elisp-parse--indent-token (tok token-delta)
+  "Add TOKEN-DELTA to all indents in TOK region,"
+  (let ((line (if (wisi-tok-nonterminal tok)
+                 (wisi-tok-first tok)
+               (when (wisi-tok-first tok) (wisi-tok-line tok))))
+       (end (cdr (wisi-tok-region tok))))
+    (when (and line end token-delta)
+      (wisi-elisp-parse--indent-token-1 line end token-delta))))
+
+(defun wisi-elisp-parse--indent-comment (tok comment-delta)
+  "Add COMMENT-DELTA to all indents in comment region following TOK."
+  (let ((line (wisi-tok-comment-line tok))
+       (end (wisi-tok-comment-end tok)))
+    (when (and line end comment-delta)
+      (wisi-elisp-parse--indent-token-1 line end comment-delta))))
+
+(defun wisi-elisp-parse--anchored-1 (tok offset &optional no-accumulate)
+  "Return offset of TOK relative to current indentation + OFFSET.
+For use in grammar indent actions."
+  (when (wisi-tok-region tok)
+    ;; region can be nil when token is inserted by error recovery
+    (let ((pos (car (wisi-tok-region tok)))
+         delta)
+
+      (goto-char pos)
+      (setq delta (+ offset (- (current-column) (current-indentation))))
+      (wisi-elisp-parse--anchored-2
+       (wisi-tok-line tok) ;; anchor-line
+       (if wisi-indent-comment
+          (wisi-tok-comment-end (aref wisi-tokens wisi-token-index))
+        (cdr (wisi-tok-region (aref wisi-tokens wisi-token-index))));; end
+       delta
+       no-accumulate)
+      )))
+
+(defun wisi-elisp-parse--max-anchor (begin-line end)
+  (let ((i (1- begin-line))
+       (max-i (length (wisi-elisp-lexer-line-begin wisi--lexer)))
+       (result 0))
+    (while (and (< i max-i)
+               (<= (aref (wisi-elisp-lexer-line-begin wisi--lexer) i) end))
+      (let ((indent (aref wisi-elisp-parse--indent i)))
+       (when (and indent (listp indent))
+         (cond
+          ((eq 'anchor (car indent))
+           (setq result (max result (car (nth 1 indent))))
+           (when (and (nth 2 indent) (listp (nth 2 indent)))
+             (setq result (max result (nth 1 (nth 2 indent))))
+             ))
+          (t ;; anchored
+           (setq result (max result (nth 1 indent))))
+          )))
+      (setq i (1+ i)))
+    result
+    ))
+
+(defun wisi-elisp-parse--anchored-2 (anchor-line end delta no-accumulate)
+  "Set ANCHOR-LINE as anchor, increment anchors thru END, return anchored 
delta."
+  ;; Typically, we use anchored to indent relative to a token buried in a line:
+  ;;
+  ;; test/ada_mode-parens.adb
+  ;; Local_2 : Integer := (1 + 2 +
+  ;;                         3);
+  ;; line starting with '3' is anchored to '('
+  ;;
+  ;; If the anchor is a nonterminal, and the first token in the anchor
+  ;; is also first on a line, we don't need anchored to compute the
+  ;; delta:
+  ;;
+  ;; test/ada_mode-parens.adb
+  ;; Local_5 : Integer :=
+  ;;   (1 + 2 +
+  ;;      3);
+  ;; delta for line starting with '3' can just be '3'.
+  ;;
+  ;; However, in some places we need anchored to prevent later
+  ;; deltas from accumulating:
+  ;;
+  ;; test/ada_mode-parens.adb
+  ;; No_Conditional_Set : constant Ada.Strings.Maps.Character_Set :=
+  ;;   Ada.Strings.Maps."or"
+  ;;     (Ada.Strings.Maps.To_Set (' '),
+  ;;
+  ;; here the function call actual parameter part is indented first
+  ;; by 'name' and later by 'expression'; we use anchored to keep the
+  ;; 'name' delta and ignore the later delta.
+  ;;
+  ;; So we apply anchored whether the anchor token is first or not.
+
+  (let* ((i (1- anchor-line))
+        (indent (aref wisi-elisp-parse--indent i)) ;; reference if list
+        (anchor-id (1+ (wisi-elisp-parse--max-anchor anchor-line end))))
+
+    ;; Set anchor
+    (cond
+     ((or
+       (null indent)
+       (integerp indent))
+      (aset wisi-elisp-parse--indent i (list 'anchor (list anchor-id) indent)))
+
+     ((and (listp indent)
+          (eq 'anchor (car indent)))
+      (push anchor-id (nth 1 indent)))
+
+     ((and (listp indent)
+          (eq 'anchored (car indent)))
+      (aset wisi-elisp-parse--indent i (list 'anchor (list anchor-id) 
(copy-sequence indent))))
+
+     (t
+      (error "wisi-anchored-delta: invalid form in indent: %s" indent)))
+
+    (list 'anchored anchor-id delta no-accumulate)
+    ))
+
+(defun wisi-anchored (token-number offset &optional no-accumulate)
+  ;; Not wisi-elisp-parse--anchored to match existing grammar files
+  "Return offset of token TOKEN-NUMBER in `wisi-tokens'.relative to current 
indentation + OFFSET.
+For use in grammar indent actions."
+  (wisi-elisp-parse--anchored-1 (aref wisi-tokens (1- token-number)) offset 
no-accumulate))
+
+(defun wisi-anchored* (token-number offset)
+  ;; Not wisi-elisp-parse--anchored* to match existing grammar files
+  "If TOKEN-NUMBER token in `wisi-tokens' is first on a line,
+call ’wisi-anchored OFFSET’. Otherwise return 0.
+For use in grammar indent actions."
+  (if (wisi-tok-first (aref wisi-tokens (1- token-number)))
+      (wisi-anchored token-number offset)
+    0))
+
+(defun wisi-anchored*- (token-number offset)
+  ;; Not wisi-elisp-parse--anchored*- to match existing grammar files
+  "If existing indent is zero, and TOKEN-NUMBER token in `wisi-tokens' is 
first on a line,
+call ’wisi-anchored OFFSET’. Otherwise return 0.
+For use in grammar indent actions."
+  (if (wisi-tok-first (aref wisi-tokens (1- token-number)))
+      (wisi-anchored token-number offset t)
+    0))
+
+(defun wisi-elisp-parse--paren-in-anchor-line (anchor-tok offset)
+  "If there is an opening paren containing ANCHOR-TOK in the same line as 
ANCHOR-TOK,
+return OFFSET plus the delta from the line indent to the paren
+position. Otherwise return OFFSET."
+  (let* ((tok-syntax (syntax-ppss (car (wisi-tok-region anchor-tok))))
+        (paren-pos (nth 1 tok-syntax))
+        (anchor-line (wisi-tok-line anchor-tok)))
+
+    (when (and paren-pos ;; in paren
+             (< paren-pos (aref (wisi-elisp-lexer-line-begin wisi--lexer) (1- 
anchor-line))))
+      ;; paren not in anchor line
+      (setq paren-pos nil))
+
+    (if paren-pos
+       (progn
+         (goto-char paren-pos)
+         (+ 1 (- (current-column) (current-indentation)) offset))
+      offset)
+    ))
+
+(defun wisi-anchored% (token-number offset &optional no-accumulate)
+  ;; Not wisi-elisp-parse--anchored% to match existing grammar files
+  "Return either an anchor for the current token at OFFSET from an enclosing 
paren on
+the line containing TOKEN-NUMBER, or OFFSET.
+For use in grammar indent actions."
+  (let* ((indent-tok (aref wisi-tokens wisi-token-index))
+        ;; indent-tok is a nonterminal; this function makes no sense for 
terminals
+        (anchor-tok (aref wisi-tokens (1- token-number))))
+
+    (wisi-elisp-parse--anchored-2
+     (wisi-tok-line anchor-tok)
+
+     (if wisi-indent-comment
+        (wisi-tok-comment-end indent-tok)
+       (cdr (wisi-tok-region indent-tok))) ;; end
+
+     (wisi-elisp-parse--paren-in-anchor-line anchor-tok offset)
+     no-accumulate)
+    ))
+
+(defun wisi-anchored%- (token-number offset)
+  ;; Not wisi-elisp-parse--anchored%- to match existing grammar files
+  "If existing indent is zero, anchor the current token at OFFSET
+from the first token on the line containing TOKEN-NUMBER in `wisi-tokens'.
+Return the delta.
+For use in grammar indent actions."
+  (wisi-anchored% token-number offset t))
+
+(defun wisi-elisp-parse--hanging-1 (delta1 delta2 option no-accumulate)
+  "If OPTION is nil, implement `wisi-hanging'; otherwise `wisi-hanging%'."
+  (let ((tok (aref wisi-tokens wisi-token-index)))
+    ;; tok is a nonterminal; this function makes no sense for terminals
+    ;; syntax-ppss moves point to start of tok
+
+    (cond
+     ((functionp wisi-elisp-parse-indent-hanging-function)
+      (funcall wisi-elisp-parse-indent-hanging-function tok delta1 delta2 
option no-accumulate))
+
+     (t
+      (let ((tok-syntax (syntax-ppss (car (wisi-tok-region tok))))
+           (first-tok-first-on-line
+            ;; first token in tok is first on line
+            (and (numberp (wisi-tok-first tok))
+                 (= (wisi-tok-line tok) (wisi-tok-first tok)))))
+       (list 'hanging
+             (wisi-tok-line tok) ;; first line of token
+             (nth 0 tok-syntax) ;; paren nest level at tok
+             delta1
+             (if (or (not option) first-tok-first-on-line)
+                 delta2
+               delta1)
+             no-accumulate))
+      ))
+    ))
+
+(defun wisi-hanging (delta1 delta2)
+  ;; Not wisi-elisp-parse--hanging to match existing grammar files
+  "Use DETLA1 for first line, DELTA2 for following lines.
+For use in grammar indent actions."
+  (wisi-elisp-parse--hanging-1 delta1 delta2 nil nil))
+
+(defun wisi-hanging% (delta1 delta2)
+  ;; Not wisi-elisp-parse--hanging% to match existing grammar files
+  "If first token is first in line, use DETLA1 for first line, DELTA2 for 
following lines.
+Otherwise use DELTA1 for all lines.
+For use in grammar indent actions."
+  (wisi-elisp-parse--hanging-1 delta1 delta2 t nil))
+
+(defun wisi-hanging%- (delta1 delta2)
+  ;; Not wisi-elisp-parse--hanging%- to match existing grammar files
+  "If existing indent is non-zero, do nothing.
+Else if first token is first in line, use DETLA1 for first line,
+DELTA2 for following lines.  Otherwise use DELTA1 for all lines.
+For use in grammar indent actions."
+  (wisi-elisp-parse--hanging-1 delta1 delta2 t t))
+
+(defun wisi-elisp-parse--indent-offset (token offset)
+  "Return offset from beginning of first token on line containing TOKEN,
+   to beginning of TOKEN, plus OFFSET."
+  (save-excursion
+    (goto-char (aref (wisi-elisp-lexer-line-begin wisi--lexer) (1- 
(wisi-tok-line token))))
+    (back-to-indentation)
+    (+ offset (- (car (wisi-tok-region token)) (point)))
+    ))
+
+(defun wisi-elisp-parse--indent-compute-delta (delta tok)
+  "Return evaluation of DELTA."
+  (cond
+   ((integerp delta)
+    delta)
+
+   ((symbolp delta)
+    (symbol-value delta))
+
+   ((vectorp delta)
+    ;; [token comment]
+    ;; if wisi-indent-comment, we are indenting the comments of the
+    ;; previous token; they should align with the 'token' delta.
+    (wisi-elisp-parse--indent-compute-delta (aref delta 0) tok))
+
+   (t ;; form
+    (cond
+     ((eq 'anchored (car delta))
+      delta)
+
+     (t
+      (save-excursion
+       (goto-char (car (wisi-tok-region tok)))
+       (eval delta)))))
+   ))
+
+(defun wisi-indent-action (deltas)
+  ;; Not wisi-elisp-parse--indent-action to match existing grammar files
+  "Accumulate `wisi--indents' from DELTAS.
+DELTAS is a vector; each element can be:
+- an integer
+- a symbol
+- a lisp form
+- a vector.
+
+The first three are evaluated to give an integer delta. A vector must
+have two elements, giving the code and following comment
+deltas. Otherwise the comment delta is the following delta in
+DELTAS."
+  (when (eq wisi--parse-action 'indent)
+    (dotimes (wisi-token-index (length wisi-tokens))
+      (let* ((tok (aref wisi-tokens wisi-token-index))
+            (token-delta (aref deltas wisi-token-index))
+            (comment-delta
+             (cond
+              ((vectorp token-delta)
+               (aref token-delta 1))
+
+              ((< wisi-token-index (1- (length wisi-tokens)))
+               (aref deltas (1+ wisi-token-index)))
+              )))
+       (when (wisi-tok-region tok)
+         ;; region is null when optional nonterminal is empty
+         (let ((wisi-indent-comment nil))
+           (setq token-delta
+                 (when (and token-delta
+                            (wisi-tok-first tok))
+                   (wisi-elisp-parse--indent-compute-delta token-delta tok)))
+
+           (when token-delta
+             (wisi-elisp-parse--indent-token tok token-delta))
+
+           (setq wisi-indent-comment t)
+           (setq comment-delta
+                 (when (and comment-delta
+                            (wisi-tok-comment-line tok))
+                   (wisi-elisp-parse--indent-compute-delta comment-delta tok)))
+
+           (when comment-delta
+             (wisi-elisp-parse--indent-comment tok comment-delta))
+           )
+         )))))
+
+(defun wisi-indent-action* (n deltas)
+  ;; Not wisi-elisp-parse--indent-action* to match existing grammar files
+  "If any of the first N tokens in `wisi-tokens' is first on a line,
+call `wisi-indent-action' with DELTAS.  Otherwise do nothing."
+  (when (eq wisi--parse-action 'indent)
+    (let ((done nil)
+         (i 0)
+         tok)
+      (while (and (not done)
+                 (< i n))
+       (setq tok (aref wisi-tokens i))
+       (setq i (1+ i))
+       (when (and (wisi-tok-region tok)
+                  (wisi-tok-first tok))
+         (setq done t)
+         (wisi-indent-action deltas))
+       ))))
+
+;;;; non-grammar indent functions
+
+(defconst wisi-elisp-parse--max-anchor-depth 20) ;; IMRPOVEME: can compute in 
actions
+
+(defun wisi-elisp-parse--indent-leading-comments ()
+  "Set `wisi-elisp-parse--indent to 0 for comment lines before first token in 
buffer.
+Leave point at first token (or eob)."
+  (save-excursion
+    (goto-char (point-min))
+    (forward-comment (point-max))
+    (let ((end (point))
+         (i 0)
+         (max-i (length wisi-elisp-parse--indent)))
+      (while (and (< i max-i)
+                 (< (aref (wisi-elisp-lexer-line-begin wisi--lexer) i) end))
+       (aset wisi-elisp-parse--indent i 0)
+       (setq i (1+ i)))
+      )))
+
+(defun wisi-elisp-parse--resolve-anchors ()
+  (let ((anchor-indent (make-vector wisi-elisp-parse--max-anchor-depth 0))
+       pos)
+
+    (dotimes (i (length wisi-elisp-parse--indent))
+      (let ((indent (aref wisi-elisp-parse--indent i)))
+
+       (cond
+        ((or (null indent)
+             (integerp indent)))
+
+        ((listp indent)
+         (let ((anchor-ids (nth 1 indent))
+               (indent2 (nth 2 indent)))
+           (cond
+            ((eq 'anchor (car indent))
+             (cond
+              ((null indent2))
+
+              ((integerp indent2)
+               (dotimes (i (length anchor-ids))
+                 (aset anchor-indent (nth i anchor-ids) indent2))
+               (setq indent indent2))
+
+              ((listp indent2) ;; 'anchored
+               (setq indent (+ (aref anchor-indent (nth 1 indent2)) (nth 2 
indent2)))
+
+               (dotimes (i (length anchor-ids))
+                 (aset anchor-indent (nth i anchor-ids) indent)))
+
+              (t
+               (error "wisi-indent-region: invalid form in wisi-ind-indent %s" 
indent))
+              ));; 'anchor
+
+            ((eq 'anchored (car indent))
+             (setq indent (+ (aref anchor-indent (nth 1 indent)) indent2)))
+
+            (t
+             (error "wisi-indent-region: invalid form in wisi-ind-indent %s" 
indent))
+            )));; listp indent
+
+        (t
+         (error "wisi-indent-region: invalid form in wisi-ind-indent %s" 
indent))
+        );; cond indent
+
+       (when (> i 0)
+         (setq pos (aref (wisi-elisp-lexer-line-begin wisi--lexer) i))
+         (with-silent-modifications
+           (put-text-property (1- pos) pos 'wisi-indent indent)))
+       )) ;; dotimes lines
+
+    ))
+
+(provide 'wisi-elisp-parse)
+;;; wisi-elisp-parse.el ends here
diff --git a/wisi-parse-common.el b/wisi-parse-common.el
index bb559a6..895e899 100644
--- a/wisi-parse-common.el
+++ b/wisi-parse-common.el
@@ -61,11 +61,39 @@ for the language-specific parser options."
 
 (cl-defgeneric wisi-parse-expand-region ((parser wisi-parser) begin end)
   "Return a cons SEND-BEGIN . SEND-END that is an expansion of
-region BEGIN END that starts at a valid parse start point,
-contains END, and ends at a point the parser can handle
-gracefully."
+region BEGIN END that starts and ends at points the parser can
+handle gracefully."
   (cons begin end))
 
+(defun wisi-search-backward-skip (regexp skip-p)
+  "Search backward for REGEXP. If SKIP-P returns non-nil, search again.
+SKIP-P is a function taking no parameters.
+Return nil if no match found before bob."
+  (let ((maybe-found-p (search-backward-regexp regexp nil t)))
+    (while (and maybe-found-p
+               (funcall skip-p)
+               (setq maybe-found-p (search-backward-regexp regexp nil t))))
+    maybe-found-p))
+
+(defun wisi-search-forward-skip (regexp skip-p)
+  "Search forward for REGEXP. If SKIP-P returns non-nil, search again.
+SKIP-P is a function taking no parameters.
+Return nil if no match found before eob."
+  (let ((maybe-found-p (search-forward-regexp regexp nil t)))
+    (while (and maybe-found-p
+               (funcall skip-p)
+               (setq maybe-found-p (search-forward-regexp regexp nil t))))
+    maybe-found-p))
+
+(defun wisi-show-expanded-region ()
+  "For debugging. Expand currently selected region."
+  (interactive)
+  (let ((region (wisi-parse-expand-region wisi--parser (region-beginning) 
(region-end))))
+    (message "pre (%d . %d) post %s" (region-beginning) (region-end) region)
+    (set-mark (car region))
+    (goto-char (cdr region))
+    ))
+
 (cl-defgeneric wisi-parse-adjust-indent ((parser wisi-parser) indent _repair)
   "Adjust INDENT for REPAIR (a wisi--parse-error-repair struct). Return new 
indent."
   indent)
@@ -101,7 +129,7 @@ For use in grammar actions.")
 
   containing
   ;; Marker at the start of the containing statement for this token.
-  ;; nil only for first token in buffer
+  ;; nil for outermost containing.
 
   prev ;; marker at previous motion token in statement; nil if none
   next ;; marker at next motion token in statement; nil if none
@@ -201,15 +229,6 @@ value from grammar file."
   :safe 'integerp)
 (make-variable-buffer-local 'wisi-mckenzie-task-count)
 
-(defcustom wisi-mckenzie-cost-limit nil
-  "If integer, sets McKenzie error recovery algorithm cost limit.
-Higher value has more recover power, but takes longer.  If nil,
-uses value from grammar file."
-  :type 'integer
-  :group 'wisi
-  :safe 'integerp)
-(make-variable-buffer-local 'wisi-mckenzie-cost-limit)
-
 (defcustom wisi-mckenzie-check-limit nil
   "If integer, sets McKenzie error recovery algorithm token check limit.
 This sets the number of tokens past the error point that must be
@@ -232,11 +251,14 @@ If nil, uses value from grammar file."
   :safe 'integerp)
 (make-variable-buffer-local 'wisi-mckenzie-enqueue-limit)
 
-(defvar wisi-parse-max-parallel 15
+(defcustom wisi-parse-max-parallel 15
   "Maximum number of parallel parsers during regular parsing.
 Parallel parsers are used to resolve redundancy in the grammar.
 If a file needs more than this, it's probably an indication that
-the grammar is excessively redundant.")
+the grammar is excessively redundant."
+  :type 'integer
+  :group 'wisi
+  :safe 'integerp)
 
 (defvar wisi-parse-max-stack-size 500
   "Maximum parse stack size.
@@ -263,7 +285,6 @@ Normally set from a language-specific option.")
 
 (defconst wisi-class-list
   [motion ;; motion-action
-   name ;; for which-function
    statement-end
    statement-override
    statement-start
diff --git a/wisi-process-parse.el b/wisi-process-parse.el
index 154b75f..d941781 100644
--- a/wisi-process-parse.el
+++ b/wisi-process-parse.el
@@ -26,22 +26,24 @@
   "Options for Wisi package."
   :group 'programming)
 
-(defcustom wisi-process-time-out 1.0
+(defcustom wisi-process-time-out 5.0
   "Time out waiting for parser response. An error occurs if there
-  is no response from the parser after waiting this amount 5
-  times."
+  is no response from the parser after waiting this amount (in
+  seconds)."
   :type 'float
   :safe 'floatp)
 (make-variable-buffer-local 'wisi-process-time-out)
 
+(defconst wisi-process-parse-protocol-version "3"
+  "Defines data exchanged between this package and the background process.
+Must match emacs_wisi_common_parse.ads Protocol_Version.")
+
 (defconst wisi-process-parse-prompt "^;;> "
   "Regexp matching executable prompt; indicates previous command is complete.")
 
 (defconst wisi-process-parse-quit-cmd "004quit\n"
   "Command to external process telling it to quit.")
 
-(defvar wisi-process-parse-debug 0)
-
 ;;;;; sessions
 
 ;; The executable builds internal parser structures on startup,
@@ -54,6 +56,7 @@
 
 (cl-defstruct (wisi-process--parser (:include wisi-parser))
   (label nil)             ;; string uniquely identifying parser
+  language-protocol-version ;; string identifying language-specific params
   (exec-file nil)        ;; absolute file name of executable
   (exec-opts nil)         ;; list of process start options for executable
   (token-table nil)       ;; vector of token symbols, indexed by integer
@@ -65,6 +68,7 @@
   (total-wait-time 0.0)   ;; total time during last parse spent waiting for 
subprocess output.
   (response-count 0)      ;; responses received from subprocess during last 
parse; for profiling.
   end-pos                 ;; last character position parsed
+  language-action-table   ;; array of function pointers, each taking an sexp 
sent by the process
   )
 
 (defvar wisi-process--alist nil
@@ -93,6 +97,24 @@ Otherwise add PARSER to ‘wisi-process--alist’, return it."
       (wisi-parse-kill parser)
       (setf (wisi-process--parser-exec-file parser) exec-file))))
 
+(defun wisi-process-parse--check-version (parser)
+  "Verify protocol version reported by process."
+  ;; The process has just started; the first non-comment line in the
+  ;; process buffer contains the process and language protocol versions.
+  (with-current-buffer (wisi-process--parser-buffer parser)
+    (goto-char (point-min))
+    (search-forward-regexp "protocol: process version \\([0-9]+\\) language 
version \\([0-9]+\\)")
+    (unless (and (match-string 1)
+                (string-equal (match-string 1) 
wisi-process-parse-protocol-version)
+                (match-string 2)
+                (string-equal (match-string 2) 
(wisi-process--parser-language-protocol-version parser)))
+      (wisi-parse-kill parser)
+      (error "%s parser process protocol version mismatch: elisp %s %s, 
process %s %s"
+            (wisi-process--parser-label parser)
+            wisi-process-parse-protocol-version 
(wisi-process--parser-language-protocol-version parser)
+            (match-string 1) (match-string 2)))
+    ))
+
 (defun wisi-process-parse--require-process (parser)
   "Start the process for PARSER if not already started."
   (unless (process-live-p (wisi-process--parser-process parser))
@@ -108,26 +130,17 @@ Otherwise add PARSER to ‘wisi-process--alist’, return it."
        (erase-buffer)); delete any previous messages, prompt
 
       (setf (wisi-process--parser-process parser)
-           (if (fboundp 'make-process)
-               ;; emacs >= 25
-               (make-process
-                :name process-name
-                :buffer (wisi-process--parser-buffer parser)
-                :command (append (list (wisi-process--parser-exec-file parser))
-                                 (wisi-process--parser-exec-opts parser)))
-             ;; emacs < 25
-             (start-process
-              process-name
-              (wisi-process--parser-buffer parser)
-              (wisi-process--parser-exec-file parser)
-              (wisi-process--parser-exec-opts parser)
-              )))
+           (make-process
+            :name process-name
+            :buffer (wisi-process--parser-buffer parser)
+            :command (append (list (wisi-process--parser-exec-file parser))
+                             (wisi-process--parser-exec-opts parser))))
 
       (set-process-query-on-exit-flag (wisi-process--parser-process parser) 
nil)
       (setf (wisi-process--parser-busy parser) nil)
 
-      ;; IMPROVEME: check protocol and version numbers
       (wisi-process-parse--wait parser)
+      (wisi-process-parse--check-version parser)
       )))
 
 (defun wisi-process-parse--wait (parser)
@@ -145,16 +158,9 @@ Otherwise add PARSER to ‘wisi-process--alist’, return it."
                    (not (setq found (re-search-forward 
wisi-process-parse-prompt (point-max) t)))))
        (setq search-start (point));; don't search same text again
        (setq wait-count (1+ wait-count))
-       (when (> wisi-process-parse-debug 0)
-           (message "wisi-process-parse--wait: %d" wait-count))
        (accept-process-output process 0.1))
 
-      (if found
-         (when (> wisi-process-parse-debug 0)
-           (message "wisi-process-parse--wait: %d" wait-count)
-           (when (> wisi-process-parse-debug 2)
-             (message "'%s'" (buffer-substring-no-properties (point-min) 
(point-max)))))
-
+      (unless found
        (wisi-process-parse-show-buffer parser)
        (error "%s process died" (wisi-process--parser-exec-file parser)))
       )))
@@ -193,17 +199,15 @@ parse region."
                      wisi-trace-mckenzie
                      wisi-trace-action
                      (if wisi-mckenzie-disable 1 0)
-                     (if wisi-mckenzie-task-count wisi-mckenzie-task-count -1)
-                     (if wisi-mckenzie-cost-limit wisi-mckenzie-cost-limit -1)
-                     (if wisi-mckenzie-check-limit wisi-mckenzie-check-limit 
-1)
-                     (if wisi-mckenzie-enqueue-limit 
wisi-mckenzie-enqueue-limit -1)
+                     (or wisi-mckenzie-task-count -1)
+                     (or wisi-mckenzie-check-limit -1)
+                     (or wisi-mckenzie-enqueue-limit -1)
+                     (or wisi-parse-max-parallel -1)
                      (- (position-bytes send-end) (position-bytes begin)) ;; 
send-end is after last byte
                      (wisi-parse-format-language-options parser)
                      ))
         (msg (format "%03d%s" (length cmd) cmd))
         (process (wisi-process--parser-process parser)))
-    (when (> wisi-process-parse-debug 0)
-      (message msg))
     (with-current-buffer (wisi-process--parser-buffer parser)
       (erase-buffer))
 
@@ -221,8 +225,6 @@ complete."
   (let* ((cmd (format "noop %d" (1- (position-bytes (point-max)))))
         (msg (format "%03d%s" (length cmd) cmd))
         (process (wisi-process--parser-process parser)))
-    (when (> wisi-process-parse-debug 0)
-      (message msg))
     (with-current-buffer (wisi-process--parser-buffer parser)
       (erase-buffer))
 
@@ -254,6 +256,13 @@ complete."
        )))
     ))
 
+(defun wisi-process-parse--Name_Property (parser sexp)
+  ;; sexp is [Name_Property first-pos last-pos]
+  ;; see ‘wisi-process-parse--execute’
+  ;; implements wisi-name-action
+  (with-silent-modifications
+    (put-text-property (aref sexp 1) (1+ (aref sexp 2)) 'wisi-name t)))
+
 (defun wisi-process-parse--Face_Property (parser sexp)
   ;; sexp is [Face_Property first-pos last-pos face-index]
   ;; see ‘wisi-process-parse--execute’
@@ -366,6 +375,10 @@ complete."
   ;; see ‘wisi-process-parse--execute’
   (setf (wisi-process--parser-end-pos parser) (aref sexp 1)))
 
+(defun wisi-process-parse--Language (parser sexp)
+  ;; sexp is [Language language-action ...]
+  (funcall (aref (wisi-process--parser-language-action-table parser) (aref 
sexp 1)) sexp))
+
 (defun wisi-process-parse--execute (parser sexp)
   "Execute encoded SEXP sent from external process."
   ;; sexp is [action arg ...]; an encoded instruction that we need to execute
@@ -379,6 +392,8 @@ complete."
   ;;    length        : integer character count
   ;;    class         : integer index into wisi-class-list
   ;;
+  ;; [Name_Property first-pos last-pos]
+  ;;
   ;; [Face_Property first-pos last-pos face-index]
   ;;    Set a font-lock-face text-property
   ;;    face-index: integer index into parser-elisp-face-table
@@ -418,6 +433,10 @@ complete."
   ;;    Args are token ids; index into parser-token-table. Save the information
   ;;    for later use by ’wisi-repair-error’.
   ;;
+  ;; [Language ...]
+  ;;    Dispatch to a language-specific action, via
+  ;;    `wisi-process--parser-language-action-table'.
+  ;;
   ;;
   ;; Numeric action codes are given in the case expression below
 
@@ -430,6 +449,8 @@ complete."
     (6  (wisi-process-parse--Check_Error parser sexp))
     (7  (wisi-process-parse--Recover parser sexp))
     (8  (wisi-process-parse--End parser sexp))
+    (9  (wisi-process-parse--Name_Property parser sexp))
+    (10 (wisi-process-parse--Language parser sexp))
     ))
 
 ;;;;; main
@@ -462,14 +483,14 @@ Send BEGIN thru SEND-END to external parser."
   ;; wisi-indent-region, we signal an error here.
   (if (wisi-process--parser-busy parser)
       (progn
-       (setf (wisi-parser-parse-errors parser)
+       (setf (wisi-parser-parse-errors parser)
              (list
               (make-wisi--parse-error
                :pos 0
                :message (format "%s:%d:%d: parser busy (try 
’wisi-kill-parser’)"
                                 (if (buffer-file-name) (file-name-nondirectory 
(buffer-file-name)) "") 1 1))
               ))
-       (error "%s parse abandoned; parser busy" wisi--parse-action)
+       (error "%s parse abandoned; parser busy - use partial parse?" 
wisi--parse-action)
        )
 
     ;; It is not possible for a background elisp function (ie
@@ -487,7 +508,6 @@ Send BEGIN thru SEND-END to external parser."
               response-end
               (response-count 0)
               (sexp-start (point-min))
-              (wait-count 0)
               (need-more nil) ;; point-max if need more, to check for new input
               (done nil)
               start-wait-time)
@@ -568,6 +588,9 @@ Send BEGIN thru SEND-END to external parser."
 
                     ((equal 'parse_error (car response))
                      ;; Parser detected some other error non-fatal error, so 
signal it.
+                     (push
+                        (make-wisi--parse-error :pos 0 :message (cadr 
response))
+                        (wisi-parser-parse-errors parser))
                      (signal 'wisi-parse-error (cdr response)))
 
                     ((and (eq 'error (car response))
@@ -596,12 +619,13 @@ Send BEGIN thru SEND-END to external parser."
                       (push (make-wisi--parse-error :pos (point) :message 
(cadr err)) (wisi-parser-parse-errors parser))
                       (signal (car err) (cdr err)))
 
-                     (error ;; ie from [C:\Windows\system32\KERNEL32.DLL]
+                     (error ;; ie from [C:\Windows\system32\KERNEL32.DLL], or 
bug in action code above.
                       (set-buffer response-buffer)
                       (let ((content (buffer-substring-no-properties 
(point-min) (point-max)))
                             (buf-name (concat (buffer-name) "-save-error")))
                         (set-buffer (get-buffer-create buf-name))
                         (insert content)
+                        (insert (format "%s" err))
                         (error "parser failed; error messages in %s" 
buf-name)))
                      ))
                   )
@@ -626,7 +650,6 @@ Send BEGIN thru SEND-END to external parser."
                  (insert content)
                  (error "parser failed; error messages in %s" buf-name)))
 
-             (setq wait-count (1+ wait-count))
              (setq start-wait-time (float-time))
 
              ;; If we specify no time-out here, we get messages about
@@ -646,7 +669,7 @@ Send BEGIN thru SEND-END to external parser."
                       (- (float-time) start-wait-time)))
 
              (when (and (= (point-max) need-more)
-                        (> wait-count 5))
+                        (> (wisi-process--parser-total-wait-time parser) 
wisi-process-time-out))
                (error "wisi-process-parse not getting more text (or bad syntax 
in process output)"))
 
              (setq need-more nil))
diff --git a/wisi-run-indent-test.el b/wisi-run-indent-test.el
new file mode 100644
index 0000000..ce5b355
--- /dev/null
+++ b/wisi-run-indent-test.el
@@ -0,0 +1,300 @@
+;;; wisi-run-indent-test.el --- utils for automating indentation and casing 
tests
+;;
+;; Copyright (C) 2018 - 2019  Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs 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 of the License, or
+;; (at your option) any later version.
+;;
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+(require 'wisi-tests)
+
+;; user can set these to t in an EMACSCMD
+(defvar skip-cmds nil)
+(defvar skip-reindent-test nil)
+(defvar skip-recase-test nil)
+(defvar skip-write nil)
+
+(defun test-in-comment-p ()
+  (nth 4 (syntax-ppss)))
+
+(defun test-face (token face)
+  "Test if all of TOKEN in next code line has FACE.
+FACE may be a list."
+  (save-excursion
+    (when (test-in-comment-p)
+      (beginning-of-line); forward-comment doesn't move if inside a comment!
+      (forward-comment (point-max)))
+    (condition-case err
+       (search-forward token (line-end-position 5))
+      (error
+       (error "can't find '%s'" token)))
+
+    (save-match-data
+      (wisi-validate-cache (line-beginning-position 0) (line-end-position 5) 
nil 'face))
+
+    ;; We don't use face-at-point, because it doesn't respect
+    ;; font-lock-face set by the parser! And we want to check for
+    ;; conflicts between font-lock-keywords and the parser.
+
+    ;; font-lock-keywords sets 'face property, parser sets 'font-lock-face.
+
+    ;; In emacs < 27, if we use (get-text-property (point) 'face), we
+    ;; also get 'font-lock-face, but not vice-versa. So we have to use
+    ;; text-properties-at to check for both.
+    (let* ((token (match-string 0))
+          (props (text-properties-at (match-beginning 0)))
+          key
+          token-face)
+
+      (cond
+       ((plist-get props 'font-lock-face)
+       (setq key 'font-lock-face)
+       (setq token-face (plist-get props 'font-lock-face)))
+
+       ((plist-get props 'face)
+       (setq key 'face)
+       (setq token-face (plist-get props 'face)))
+       )
+
+      (when (and (memq 'font-lock-face props)
+                (memq 'face props))
+       (describe-text-properties (match-beginning 0))
+       (error "mixed font-lock-keyword and parser faces for '%s'" token))
+
+      (unless (not (text-property-not-all 0 (length token) key token-face 
token))
+       (error "mixed faces, expecting %s for '%s'" face token))
+
+      (unless (or (and (listp face)
+                      (memq token-face face))
+                 (eq token-face face))
+       (error "found face %s, expecting %s for '%s'" token-face face token))
+    )))
+
+(defun test-face-1 (search token face)
+  "Move to end of comment, search for SEARCH, call `test-face'."
+  (save-excursion
+    (when (test-in-comment-p)
+      (beginning-of-line); forward-comment doesn't move if inside a comment!
+      (forward-comment (point-max)))
+    (search-forward search)
+    (test-face token face)
+    ))
+
+(defun test-cache-class (token class)
+  "Test if TOKEN in next code line has wisi-cache with class CLASS."
+  (save-excursion
+    (wisi-validate-cache (line-beginning-position 0) (line-end-position 3) nil 
'navigate)
+    (beginning-of-line); forward-comment doesn't move if inside a comment!
+    (forward-comment (point-max))
+    (condition-case err
+       (search-forward token (line-end-position 5))
+      (error
+       (error "can't find '%s'" token)))
+
+    (let ((cache (get-text-property (match-beginning 0) 'wisi-cache)))
+
+      (unless cache (error "no cache"))
+      (unless (eq (wisi-cache-class cache) class)
+       (error "expecting class %s, found '%s'" class (wisi-cache-class cache)))
+    )))
+
+(defun test-cache-containing (containing contained)
+  "Test if CONTAINING in next code line has wisi-cache with that contains 
CONTAINED."
+  (save-excursion
+    (wisi-validate-cache (line-beginning-position 0) (line-end-position 3) nil 
'navigate)
+    (beginning-of-line)
+    (forward-comment (point-max))
+    (let (containing-pos contained-cache)
+      (condition-case err
+         (search-forward containing (line-end-position 5))
+       (error
+        (error "can't find '%s'" containing)))
+      (setq containing-pos (match-beginning 0))
+
+      (condition-case err
+         (search-forward contained (line-end-position 5))
+       (error
+        (error "can't find '%s'" contained)))
+      (setq contained-cache (get-text-property (match-beginning 0) 
'wisi-cache))
+
+      (unless contained-cache (error "no cache on %s" contained))
+      (unless (= containing-pos (wisi-cache-containing contained-cache))
+       (error "expecting %d, got %d" containing-pos (wisi-cache-containing 
contained-cache)))
+    )))
+
+(defun run-test-here ()
+  "Run an indentation and casing test on the current buffer."
+  (interactive)
+  (setq indent-tabs-mode nil)
+  (setq jit-lock-context-time 0.0);; for test-face
+
+  (let ((error-count 0)
+       (test-buffer (current-buffer))
+       cmd-line
+       last-result last-cmd expected-result)
+    ;; Look for EMACS* comments in the file:
+    ;;
+    ;; EMACSCMD: <form>
+    ;;    Executes the lisp form inside a save-excursion, saves the result as 
a lisp object.
+    ;;
+    ;; EMACSRESULT: <form>
+    ;;    point is moved to end of line, <form> is evaluated inside
+    ;;    save-excursion and compared (using `equal') with the result
+    ;;    of the previous EMACSCMD, and the test fails if they don't
+    ;;    match.
+    ;;
+    ;; EMACS_SKIP_UNLESS: <form>
+    ;;   skip entire test if form evals nil
+    ;;
+    ;; EMACSDEBUG: <form>
+    ;;    Eval form, display result. Also used for setting breakpoint.
+
+    (goto-char (point-min))
+    (while (and (not skip-cmds)
+               (re-search-forward (concat comment-start "EMACS\\([^:]+\\):") 
nil t))
+      (cond
+       ((string= (match-string 1) "CMD")
+       (looking-at ".*$")
+       (save-excursion
+         (setq cmd-line (line-number-at-pos)
+               last-cmd (match-string 0)
+               last-result
+               (condition-case-unless-debug err
+                   (eval (car (read-from-string last-cmd)))
+                 (error
+                    (setq error-count (1+ error-count))
+                    (message "%s:%d: command: %s"
+                             (buffer-file-name) cmd-line last-cmd)
+                    (message "%s:%d: %s: %s"
+                             (buffer-file-name)
+                             (line-number-at-pos)
+                             (car err)
+                             (cdr err))))
+               )
+         ;; save-excursion does not preserve mapping of buffer to
+         ;; window, but some tests depend on that. For example,
+         ;; execute-kbd-macro doesn’t work properly if current buffer
+         ;; is not visible..
+         (pop-to-buffer test-buffer)))
+
+       ((string= (match-string 1) "RESULT")
+       (looking-at ".*$")
+       (setq expected-result (save-excursion (end-of-line 1) (eval (car 
(read-from-string (match-string 0))))))
+       (unless (equal expected-result last-result)
+         (when debug-on-error (debug))
+         (setq error-count (1+ error-count))
+         (message
+          (concat
+           (format "error: %s:%d:\n" (buffer-file-name) (line-number-at-pos))
+           (format "Result of '%s' does not match.\nGot    '%s',\nexpect '%s'"
+                   last-cmd
+                   last-result
+                   expected-result)
+           ))))
+
+       ((string= (match-string 1) "_SKIP_UNLESS")
+       (looking-at ".*$")
+       (unless (eval (car (read-from-string (match-string 0))))
+         (setq skip-cmds t)
+         (setq skip-reindent-test t)
+         (setq skip-recase-test t)
+         ;; We don’t set ‘skip-write’ t here, so the *.diff Make target 
succeeds.
+         ))
+
+       ((string= (match-string 1) "DEBUG")
+       (looking-at ".*$")
+       (message "DEBUG: %s:%d %s"
+                (current-buffer)
+                (line-number-at-pos)
+                (save-excursion
+                 (eval (car (read-from-string (match-string 0)))))))
+
+       (t
+       (setq error-count (1+ error-count))
+       (error (concat "Unexpected EMACS test command " (match-string 1))))))
+
+    (when (> error-count 0)
+      (error
+       "%s:%d: aborting due to previous errors (%d)"
+       (buffer-file-name) (line-number-at-pos (point)) error-count))
+    )
+
+  (when (not skip-reindent-test)
+    ;; Reindent the buffer
+    (message "indenting")
+
+    ;; first unindent; if the indentation rules do nothing, the test
+    ;; would pass, otherwise!  Only unindent by 1 column, so comments
+    ;; not currently in column 0 are still not in column 0, in case
+    ;; the mode supports a special case for comments in column 0.
+    (indent-rigidly (point-min) (point-max) -1)
+
+    ;; indent-region uses save-excursion, so we can't goto an error location
+    (indent-region (point-min) (point-max))
+
+    ;; Cleanup the buffer; indenting often leaves trailing whitespace;
+    ;; files must be saved without any.
+    (delete-trailing-whitespace)
+    )
+  )
+
+(defun run-test (file-name)
+  "Run an indentation and casing test on FILE-NAME."
+  (interactive "f")
+  ;; we'd like to run emacs from a makefile as:
+  ;;
+  ;; emacs -Q --batch -l runtest.el -f run-test-here <filename>
+  ;;
+  ;; However, the function specified with -f is run _before_
+  ;; <filename> is visited. So we try this instead:
+  ;;
+  ;; emacs -Q --batch -l runtest.el --eval '(run-test "<filename>")'
+  ;;
+  ;; And then we discover that processes spawned with start-process
+  ;; don't run when emacs is in --batch mode. So we try this:
+  ;;
+  ;; emacs -Q -l runtest.el --eval '(progn (run-test 
"<filename>")(kill-emacs))'
+  ;;
+  ;; Then we have problems with font lock defaulting to jit-lock; that
+  ;; screws up font-lock tests because the test runs before jit-lock
+  ;; does. This forces default font-lock, which fontifies the whole
+  ;; buffer when (font-lock-fontify-buffer) is called, which tests
+  ;; that rely on font-lock do explicitly.
+  (setq font-lock-support-mode nil)
+
+  (let ((dir default-directory))
+    (find-file file-name) ;; sets default-directory
+
+    (when (eq major-mode 'fundamental-mode)
+      ;; Running a grammar in test/wisi
+      (add-to-list 'load-path (expand-file-name "."))
+      (wisi-tests-setup (file-name-sans-extension (file-name-nondirectory 
file-name))))
+
+    (run-test-here)
+
+    (unless skip-write
+      ;; Write the result file; makefile will diff.
+      (when skip-reindent-test
+       ;; user sets skip-reindent-test when testing interactive editing
+       ;; commands, so the diff would fail. Revert to the original file,
+       ;; save a copy of that.
+       (revert-buffer t t))
+
+      (delete-trailing-whitespace)
+      (write-file (concat dir (file-name-nondirectory file-name) ".tmp")) )
+    )
+  )
+
+(provide 'wisi-run-indent-test)
+;; end of file
diff --git a/wisi-tests.el b/wisi-tests.el
new file mode 100644
index 0000000..d1468ef
--- /dev/null
+++ b/wisi-tests.el
@@ -0,0 +1,146 @@
+;;; wisi-tests.el --- Common utils for wisi tests -*- lexical-binding:t -*-
+;;
+;; Copyright (C) 2012 - 2019  Free Software Foundation, Inc.
+;;
+;; Author: Stephen Leake <stephen_leake@stephe-leake.org>
+;; Maintainer: Stephen Leake <stephen_leake@stephe-leake.org>
+;;
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs 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 of the License, or
+;; (at your option) any later version.
+;;
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+;;
+
+(require 'cl-lib)
+(require 'wisi)
+
+(defvar wisi-test-parser 'elisp
+  "Set to ’process to test external process parser.")
+
+(defvar test-syntax-table
+  (let ((table (make-syntax-table)))
+    ;; make-syntax-table sets all alphanumeric to w, etc; so we only
+    ;; have to add test-specific things.
+
+    ;; operator symbols
+    (modify-syntax-entry ?&  "." table)
+    (modify-syntax-entry ?*  "." table)
+    (modify-syntax-entry ?+  "." table)
+    (modify-syntax-entry ?-  "." table)
+    (modify-syntax-entry ?/  "." table)
+    (modify-syntax-entry ?<  "." table)
+    (modify-syntax-entry ?=  "." table)
+    (modify-syntax-entry ?>  "." table)
+    (modify-syntax-entry ?|  "." table)
+
+    ;; \f and \n end a comment - see test-syntax-propertize for comment start
+    (modify-syntax-entry ?\f  ">   " table)
+    (modify-syntax-entry ?\n  ">   " table)
+    table
+    ))
+
+(defun test-syntax-propertize (start end)
+  "Assign `syntax-table' properties in accessible part of buffer."
+  ;; (info "(elisp)Syntax Properties")
+  (let ((modified (buffer-modified-p))
+       (buffer-undo-list t)
+       (inhibit-read-only t)
+       (inhibit-point-motion-hooks t)
+       (inhibit-modification-hooks t))
+    (goto-char start)
+    (while (re-search-forward
+            "\\(--\\)"; 1: comment start
+           end t)
+      ;; The help for syntax-propertize-extend-region-functions
+      ;; implies that 'start end' will always include whole lines, in
+      ;; which case we don't need
+      ;; syntax-propertize-extend-region-functions
+      (cond
+       ((match-beginning 1)
+       (put-text-property
+        (match-beginning 1) (match-end 1) 'syntax-table '(11 . nil)))
+       ))
+    (unless modified
+      (restore-buffer-modified-p nil))))
+
+(defun wisi-tests-setup (grammar-name)
+  ;; grammar-elisp file must be on load-path
+  ;; use Ada style comments in source
+  (set-syntax-table test-syntax-table)
+  (set (make-local-variable 'syntax-propertize-function) 
'test-syntax-propertize)
+  (syntax-ppss-flush-cache (point-min));; force re-evaluate with hook.
+
+  (cl-ecase wisi-test-parser
+    (elisp
+     (require 'wisi-elisp-parse)
+     (let* ((grammar-file-root (concat grammar-name "-lalr-elisp"))
+           (grammar-file-name (concat grammar-file-root ".el"))
+           (grammar-file-abs (locate-file grammar-file-name load-path)))
+       (unless grammar-file-abs
+        (error "can’t find ’%s’ on ’%s’" grammar-file-name load-path))
+       (require (intern grammar-file-root)))
+
+     (wisi-setup
+      :indent-calculate nil
+      :post-indent-fail nil
+      :parser (wisi-make-elisp-parser
+              (symbol-value (intern-soft (concat grammar-name 
"-lalr-elisp-parse-table")))
+              `wisi-forward-token)
+      :lexer (wisi-make-elisp-lexer
+             :token-table-raw (symbol-value (intern-soft (concat grammar-name 
"-lalr-elisp-token-table-raw")))
+             :keyword-table-raw (symbol-value (intern-soft (concat 
grammar-name "-lalr-elisp-keyword-table-raw")))
+             :string-quote-escape-doubled nil
+             :string-quote-escape nil)))
+
+    (process
+     (require 'wisi-process-parse)
+     (require (intern (concat grammar-name "-process"))) ;; generated by 
wisi-generate
+     (require (intern grammar-name)) ;; declares parser cl-defstruct
+     (add-to-list 'exec-path default-directory)
+     (wisi-setup
+      :indent-calculate nil
+      :post-indent-fail nil
+      :parser
+      (wisi-process-parse-get
+       (funcall
+       (intern-soft (concat "make-" grammar-name "-wisi-parser"))
+       :label grammar-name
+       :exec-file (concat grammar-name "_wisi_parse.exe")
+       :face-table (symbol-value (intern-soft (concat grammar-name 
"-process-face-table")))
+       :token-table (symbol-value (intern-soft (concat grammar-name 
"-process-token-table")))
+       ))
+      :lexer (wisi-make-elisp-lexer
+             :token-table-raw (symbol-value (intern-soft (concat grammar-name 
"-lalr-elisp-token-table-raw")))
+             :keyword-table-raw (symbol-value (intern-soft (concat 
grammar-name "-lalr-elisp-keyword-table-raw")))
+             :string-quote-escape-doubled nil
+             :string-quote-escape nil))
+     (setq wisi-mckenzie-disable nil)
+     )
+    )
+
+  ;; Not clear why this is not being done automatically
+  (syntax-propertize (point-max))
+  )
+
+;;; Initialization
+
+;; Default includes mtn, among others, which is broken in Emacs 22.2
+(setq vc-handled-backends '(CVS))
+
+(setq eval-expression-debug-on-error nil)
+
+;; ’package-initialize’ is not here; it must be run as part of one of the
+;; -l or --eval command line options
+
+(provide 'wisi-tests)
+;; end of file
diff --git a/wisi.adb b/wisi.adb
index 4031df0..b65bbee 100644
--- a/wisi.adb
+++ b/wisi.adb
@@ -17,27 +17,31 @@
 
 pragma License (Modified_GPL);
 
+with Ada.Exceptions;
 with Ada.Strings.Bounded;
 with Ada.Text_IO;
+with SAL;
 with WisiToken.Semantic_Checks;
 package body Wisi is
    use WisiToken;
 
-   Navigate_Cache_Code : constant String := "1 ";
-   Face_Property_Code  : constant String := "2 ";
-   Indent_Code         : constant String := "3 ";
-   Lexer_Error_Code    : constant String := "4";
-   Parser_Error_Code   : constant String := "5";
-   Check_Error_Code    : constant String := "6";
-   Recover_Code        : constant String := "7 ";
-   End_Code            : constant String := "8";
+   Navigate_Cache_Code  : constant String := "1";
+   Face_Property_Code   : constant String := "2";
+   Indent_Code          : constant String := "3";
+   Lexer_Error_Code     : constant String := "4";
+   Parser_Error_Code    : constant String := "5";
+   Check_Error_Code     : constant String := "6";
+   Recover_Code         : constant String := "7 ";
+   End_Code             : constant String := "8";
+   Name_Property_Code   : constant String := "9";
+   Language_Action_Code : constant String := "10 ";
 
    Chars_Per_Int : constant Integer := Integer'Width;
 
    ----------
    --  body subprogram specs (as needed), alphabetical
 
-   function Indent_Zero_P (Indent : in Indent_Type) return Boolean;
+   function Indent_Nil_P (Indent : in Indent_Type) return Boolean;
 
    function Max_Anchor_ID
      (Data       : in out Parse_Data_Type;
@@ -79,42 +83,53 @@ package body Wisi is
       when Int =>
          return "(" & Indent_Label'Image (Indent.Label) & Integer'Image 
(Indent.Int_Indent) & ")";
 
-      when Anchor =>
-         return "(" & Indent_Label'Image (Indent.Label) & Image 
(Indent.Anchor_IDs) & ", " & Integer'Image
-           (Indent.Anchor_Indent) & ")";
+      when Anchor_Nil =>
+         return "(" & Indent_Label'Image (Indent.Label) & ", " & Image 
(Indent.Anchor_Nil_IDs) & ", nil)";
+
+      when Anchor_Int =>
+         return "(" & Indent_Label'Image (Indent.Label) & ", " & Image 
(Indent.Anchor_Int_IDs) & ", " & Integer'Image
+           (Indent.Anchor_Int_Indent) & ")";
 
       when Anchored =>
-         return "(" & Indent_Label'Image (Indent.Label) & Integer'Image 
(Indent.Anchored_ID) & ", " & Integer'Image
-           (Indent.Anchored_Delta) & ")";
+         return "(" & Indent_Label'Image (Indent.Label) & ", " & Integer'Image 
(Indent.Anchored_ID) & ", " &
+           Integer'Image (Indent.Anchored_Delta) & ")";
 
       when Anchor_Anchored =>
-         return "(" & Indent_Label'Image (Indent.Label) & Image 
(Indent.Anchor_Anchored_IDs) & Integer'Image
+         return "(" & Indent_Label'Image (Indent.Label) & ", " & Image 
(Indent.Anchor_Anchored_IDs) & Integer'Image
            (Indent.Anchor_Anchored_ID) & ", " & Integer'Image 
(Indent.Anchor_Anchored_Delta) & ")";
       end case;
    end Image;
 
    procedure Indent_Apply_Anchored
-     (Delta_Indent : in     Anchored_Delta;
+     (Delta_Indent : in     Simple_Delta_Type;
       Indent       : in out Indent_Type)
+   with Pre => Delta_Indent.Label = Anchored
    is begin
-      --  [2] wisi-elisp-parse--apply-anchored
+      --  [2] wisi-elisp-parse--apply-anchored; add Delta_Indent to Indent
 
       case Indent.Label is
       when Not_Set =>
          Indent := (Anchored, Delta_Indent.Anchored_ID, 
Delta_Indent.Anchored_Delta);
 
       when Int =>
-         if Indent.Int_Indent = 0 or Delta_Indent.Anchored_Accumulate then
+         if Delta_Indent.Anchored_Accumulate then
             Indent := (Anchored, Delta_Indent.Anchored_ID, Indent.Int_Indent + 
Delta_Indent.Anchored_Delta);
          end if;
 
-      when Anchor =>
-         if Delta_Indent.Anchored_Accumulate or Indent.Anchor_Indent = 0 then
+      when Anchor_Nil =>
+         Indent :=
+           (Anchor_Anchored,
+            Indent.Anchor_Nil_IDs,
+            Delta_Indent.Anchored_ID,
+            Delta_Indent.Anchored_Delta);
+
+      when Anchor_Int =>
+         if Delta_Indent.Anchored_Accumulate then
             Indent :=
               (Anchor_Anchored,
-               Indent.Anchor_IDs,
+               Indent.Anchor_Int_IDs,
                Delta_Indent.Anchored_ID,
-               Delta_Indent.Anchored_Delta + Indent.Anchor_Indent);
+               Delta_Indent.Anchored_Delta + Indent.Anchor_Int_Indent);
          end if;
 
       when Anchored | Anchor_Anchored =>
@@ -125,7 +140,7 @@ package body Wisi is
 
    procedure Indent_Apply_Int (Indent : in out Indent_Type; Offset : in 
Integer)
    is begin
-      --  [2] wisi-elisp-parse--apply-int
+      --  [2] wisi-elisp-parse--apply-int; add an Int indent to Indent
       case Indent.Label is
       when Not_Set =>
          Indent := (Int, Offset);
@@ -133,8 +148,14 @@ package body Wisi is
       when Int =>
          Indent.Int_Indent := Indent.Int_Indent + Offset;
 
-      when Anchor =>
-         Indent.Anchor_Indent := Indent.Anchor_Indent + Offset;
+      when Anchor_Nil         =>
+         Indent :=
+           (Label             => Anchor_Int,
+            Anchor_Int_IDs    => Indent.Anchor_Nil_IDs,
+            Anchor_Int_Indent => Offset);
+
+      when Anchor_Int =>
+         Indent.Anchor_Int_Indent := Indent.Anchor_Int_Indent + Offset;
 
       when Anchored | Anchor_Anchored =>
          null;
@@ -152,6 +173,9 @@ package body Wisi is
       case Delta_Indent.Label is
       when Simple =>
          case Delta_Indent.Simple_Delta.Label is
+         when None =>
+            null;
+
          when Int =>
             Indent_Apply_Int (Indent, Delta_Indent.Simple_Delta.Int_Delta);
 
@@ -160,10 +184,12 @@ package body Wisi is
          end case;
 
       when Hanging =>
-         if Delta_Indent.Hanging_Accumulate or Indent_Zero_P (Data.Indents 
(Line)) then
+         if Delta_Indent.Hanging_Accumulate or Indent_Nil_P (Data.Indents 
(Line)) then
             if Line = Delta_Indent.Hanging_First_Line then
                --  Apply delta_1
                case Delta_Indent.Hanging_Delta_1.Label is
+               when None =>
+                  null;
                when Int =>
                   Indent_Apply_Int (Indent, 
Delta_Indent.Hanging_Delta_1.Int_Delta);
                when Anchored =>
@@ -172,6 +198,8 @@ package body Wisi is
             else
                if Delta_Indent.Hanging_Paren_State = Data.Line_Paren_State 
(Line) then
                   case Delta_Indent.Hanging_Delta_2.Label is
+                  when None =>
+                     null;
                   when Int =>
                      Indent_Apply_Int (Indent, 
Delta_Indent.Hanging_Delta_2.Int_Delta);
                   when Anchored =>
@@ -181,29 +209,18 @@ package body Wisi is
             end if;
          end if;
       end case;
+
+      if Trace_Action > Extra then
+         Ada.Text_IO.Put_Line (";; indent_line: " & Line_Number_Type'Image 
(Line) & " => " & Image (Indent));
+      end if;
+
       Data.Indents.Replace_Element (Line, Indent);
    end Indent_Line;
 
-   function Indent_Zero_P (Indent : in Indent_Type) return Boolean
+   function Indent_Nil_P (Indent : in Indent_Type) return Boolean
    is begin
-      --  wisi-elisp-parse--indent-zero-p
-      case Indent.Label is
-      when Not_Set =>
-         return True;
-
-      when Int =>
-         return Indent.Int_Indent = 0;
-
-      when Anchor =>
-         return Indent.Anchor_Indent = 0;
-
-      when Anchored =>
-         return Indent.Anchored_Delta = 0;
-
-      when Anchor_Anchored =>
-         return Indent.Anchor_Anchored_Delta = 0;
-      end case;
-   end Indent_Zero_P;
+      return Indent.Label in Not_Set | Anchor_Nil;
+   end Indent_Nil_P;
 
    function Max_Anchor_ID
      (Data       : in out Parse_Data_Type;
@@ -220,8 +237,10 @@ package body Wisi is
             case Indent.Label is
             when Not_Set | Int =>
                null;
-            when Anchor =>
-               Result := Integer'Max (Result, Indent.Anchor_IDs 
(Indent.Anchor_IDs.First_Index));
+            when Anchor_Nil =>
+               Result := Integer'Max (Result, Indent.Anchor_Nil_IDs 
(Indent.Anchor_Nil_IDs.First_Index));
+            when Anchor_Int =>
+               Result := Integer'Max (Result, Indent.Anchor_Int_IDs 
(Indent.Anchor_Int_IDs.First_Index));
             when Anchored =>
                Result := Integer'Max (Result, Indent.Anchored_ID);
             when Anchor_Anchored =>
@@ -238,8 +257,8 @@ package body Wisi is
       Offset       : in     Integer)
      return Integer
    is
-      Left_Paren_ID  : Token_ID renames Data.Descriptor.Left_Paren_ID;
-      Right_Paren_ID : Token_ID renames Data.Descriptor.Right_Paren_ID;
+      Left_Paren_ID  : Token_ID renames Data.Left_Paren_ID;
+      Right_Paren_ID : Token_ID renames Data.Right_Paren_ID;
 
       I              : Base_Token_Index := Anchor_Token.First_Terminals_Index;
       Paren_Count    : Integer          := 0;
@@ -308,6 +327,12 @@ package body Wisi is
       Ada.Text_IO.Put_Line (To_String (Line));
    end Put;
 
+   procedure Put (Cache : in WisiToken.Buffer_Region)
+   is begin
+      Ada.Text_IO.Put_Line
+        ("[" & Name_Property_Code & Buffer_Pos'Image (Cache.First) & 
Buffer_Pos'Image (Cache.Last) & "]");
+   end Put;
+
    procedure Put (Cache : in Face_Cache_Type)
    is
       package Bounded is new Ada.Strings.Bounded.Generic_Bounded_Length (Max 
=> 2 + 4 * Chars_Per_Int);
@@ -341,25 +366,26 @@ package body Wisi is
             Ind : constant Integer := Integer'Max (0, Item.Int_Indent);
          begin
             Ada.Text_IO.Put_Line
-              ('[' & Indent_Code & Trimmed_Image (Integer (Line_Number)) & 
Integer'Image (Ind) & ']');
+              ('[' & Indent_Code & Line_Number_Type'Image (Line_Number) & 
Integer'Image (Ind) & ']');
          end;
 
-      when Anchor | Anchored | Anchor_Anchored =>
+      when Anchor_Nil | Anchor_Int | Anchored | Anchor_Anchored =>
          raise SAL.Programmer_Error with "Indent item has non-int label: " & 
Indent_Label'Image (Item.Label);
       end case;
    end Put;
 
    procedure Put
-     (Item       : in Parse.LR.Configuration;
-      Terminals  : in Augmented_Token_Arrays.Vector;
-      Descriptor : in WisiToken.Descriptor)
+     (Item                          : in Parse.LR.Configuration;
+      Terminals                     : in Augmented_Token_Arrays.Vector;
+      Descriptor                    : in WisiToken.Descriptor;
+      Embedded_Quote_Escape_Doubled : in Boolean)
    is
       use Ada.Containers;
       use Ada.Strings.Unbounded;
       use Parse.LR;
 
       Line    : Unbounded_String := To_Unbounded_String ("[");
-      Last_Op : Config_Op        := (Fast_Forward, Token_Index'Last);
+      Last_Op : Config_Op        := (Fast_Forward, WisiToken.Token_Index'Last);
 
    begin
       if Trace_Action > Detail then
@@ -391,19 +417,19 @@ package body Wisi is
                when Insert =>
                   if Last_Op.Op = Fast_Forward then
                      Append (Line, "[");
-                     Append (Line, Buffer_Pos'Image (Terminals 
(Op.Token_Index).Char_Region.First));
+                     Append (Line, Buffer_Pos'Image (Terminals 
(Op.Ins_Token_Index).Char_Region.First));
                      Append (Line, "[");
 
                   elsif Last_Op.Op = Delete then
                      Append (Line, "]][");
-                     Append (Line, Buffer_Pos'Image (Terminals 
(Op.Token_Index).Char_Region.First));
+                     Append (Line, Buffer_Pos'Image (Terminals 
(Op.Ins_Token_Index).Char_Region.First));
                      Append (Line, "[");
 
                   else
                      --  Last_Op.Op = Insert
                      null;
                   end if;
-                  Append (Line, Token_ID'Image (Op.ID));
+                  Append (Line, Token_ID'Image (Op.Ins_ID));
 
                   Last_Op := Op;
 
@@ -413,20 +439,20 @@ package body Wisi is
                   begin
                      if Last_Op.Op = Fast_Forward then
                         Append (Line, "[");
-                        Append (Line, Buffer_Pos'Image (Terminals 
(Op.Token_Index).Char_Region.First));
+                        Append (Line, Buffer_Pos'Image (Terminals 
(Op.Del_Token_Index).Char_Region.First));
                         Append (Line, "[][");
 
                      elsif Last_Op.Op = Insert then
                         Append (Line, "][");
 
                      elsif Last_Op.Op = Delete then
-                        if Descriptor.Embedded_Quote_Escape_Doubled and then
-                          ((Last_Op.ID = Descriptor.String_1_ID and Op.ID = 
Descriptor.String_1_ID) or
-                             (Last_Op.ID = Descriptor.String_2_ID and Op.ID = 
Descriptor.String_2_ID))
+                        if Embedded_Quote_Escape_Doubled and then
+                          ((Last_Op.Del_ID = Descriptor.String_1_ID and 
Op.Del_ID = Descriptor.String_1_ID) or
+                             (Last_Op.Del_ID = Descriptor.String_2_ID and 
Op.Del_ID = Descriptor.String_2_ID))
                         then
                            declare
-                              Tok_1 : Augmented_Token renames Terminals 
(Last_Op.Token_Index);
-                              Tok_2 : Augmented_Token renames Terminals 
(Op.Token_Index);
+                              Tok_1 : Augmented_Token renames Terminals 
(Last_Op.Del_Token_Index);
+                              Tok_2 : Augmented_Token renames Terminals 
(Op.Del_Token_Index);
                            begin
                               if Tok_1.Char_Region.Last + 1 = 
Tok_2.Char_Region.First then
                                  --  Buffer text was '"""', lexer repair 
changed it to '""""'. The
@@ -441,7 +467,7 @@ package body Wisi is
                      end if;
 
                      if not Skip then
-                        Append (Line, Token_ID'Image (Op.ID));
+                        Append (Line, Token_ID'Image (Op.Del_ID));
                      end if;
                   end;
                   Last_Op := Op;
@@ -474,8 +500,9 @@ package body Wisi is
          Ada.Text_IO.New_Line;
          Ada.Text_IO.Put_Line (";; Begin_Indent: " & Integer'Image 
(Data.Begin_Indent));
          for I in Data.Indents.First_Index .. Data.Indents.Last_Index loop
-            Ada.Text_IO.Put_Line (Line_Number_Type'Image (I) & ", " & Image 
(Data.Indents (I)));
+            Ada.Text_IO.Put_Line (";; " & Line_Number_Type'Image (I) & ", " & 
Image (Data.Indents (I)));
          end loop;
+         Ada.Text_IO.Put_Line (";; resolve anchors");
       end if;
 
       for I in Data.Indents.First_Index .. Data.Indents.Last_Index loop
@@ -490,11 +517,17 @@ package body Wisi is
             when Int =>
                Data.Indents.Replace_Element (I, (Int, Indent.Int_Indent + 
Begin_Indent));
 
-            when Anchor =>
-               for I of Indent.Anchor_IDs loop
-                  Anchor_Indent (I) := Indent.Anchor_Indent + Begin_Indent;
+            when Anchor_Nil =>
+               for I of Indent.Anchor_Nil_IDs loop
+                  Anchor_Indent (I) := Begin_Indent;
                end loop;
-               Data.Indents.Replace_Element (I, (Int, Indent.Anchor_Indent + 
Begin_Indent));
+               Data.Indents.Replace_Element (I, (Int, Begin_Indent));
+
+            when Anchor_Int =>
+               for I of Indent.Anchor_Int_IDs loop
+                  Anchor_Indent (I) := Indent.Anchor_Int_Indent + Begin_Indent;
+               end loop;
+               Data.Indents.Replace_Element (I, (Int, Indent.Anchor_Int_Indent 
+ Begin_Indent));
 
             when Anchored =>
                Data.Indents.Replace_Element
@@ -589,6 +622,10 @@ package body Wisi is
       end case;
 
       Data.Reset;
+   exception
+   when E : others =>
+      raise SAL.Programmer_Error with "wisi.initialize: " & 
Ada.Exceptions.Exception_Name (E) & ": " &
+        Ada.Exceptions.Exception_Message (E);
    end Initialize;
 
    overriding procedure Reset (Data : in out Parse_Data_Type)
@@ -607,6 +644,9 @@ package body Wisi is
       Data.Navigate_Caches.Initialize;
       Data.End_Positions.Clear;
 
+      Data.Name_Caches.Finalize;
+      Data.Name_Caches.Initialize;
+
       Data.Face_Caches.Finalize;
       Data.Face_Caches.Initialize;
 
@@ -642,16 +682,16 @@ package body Wisi is
          then
             --  Previous token contains multiple lines; ie %code in 
wisitoken_grammar.wy
             declare
-               First_Unset_Line : Line_Number_Type;
+               First_Set_Line : Line_Number_Type;
             begin
                for Line in reverse Data.Line_Begin_Pos.First_Index .. 
Token.Line - 1 loop
                   if Data.Line_Begin_Pos (Line) /= Invalid_Buffer_Pos then
-                     First_Unset_Line := Line;
+                     First_Set_Line := Line;
                      exit;
                   end if;
                end loop;
-               for Line in First_Unset_Line .. Token.Line - 1 loop
-                  Data.Line_Begin_Pos (Line) := Data.Line_Begin_Pos 
(First_Unset_Line); -- good enough
+               for Line in First_Set_Line + 1 .. Token.Line - 1 loop
+                  Data.Line_Begin_Pos (Line) := Data.Line_Begin_Pos 
(First_Set_Line); -- good enough
                end loop;
             end;
          end if;
@@ -676,7 +716,10 @@ package body Wisi is
                     Containing_Token.Non_Grammar
                       (Containing_Token.Non_Grammar.Last_Index).ID = 
Data.Descriptor.New_Line_ID);
             begin
-               if Lexer.First and (Token.ID = Data.Descriptor.Comment_ID or 
Trailing_Blank) then
+               if Lexer.First and
+                 (Token.ID in Data.First_Comment_ID .. Data.Last_Comment_ID or
+                    Trailing_Blank)
+               then
                   if Containing_Token.First_Trailing_Comment_Line = 
Invalid_Line_Number then
                      Containing_Token.First_Trailing_Comment_Line := 
Token.Line;
                   end if;
@@ -707,10 +750,10 @@ package body Wisi is
                Last_Trailing_Comment_Line  => Invalid_Line_Number,
                Non_Grammar                 => <>);
          begin
-            if Token.ID = Data.Descriptor.Left_Paren_ID then
+            if Token.ID = Data.Left_Paren_ID then
                Data.Current_Paren_State := Data.Current_Paren_State + 1;
 
-            elsif Token.ID = Data.Descriptor.Right_Paren_ID then
+            elsif Token.ID = Data.Right_Paren_ID then
                Data.Current_Paren_State := Data.Current_Paren_State - 1;
             end if;
 
@@ -899,34 +942,67 @@ package body Wisi is
       Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
       Params  : in     Statement_Param_Array)
    is
-      First_Item         : Boolean     := True;
-      Override_Start_Set : Boolean     := False;
-      Override_Start     : Navigate_Class_Type;
+      First_Item         : Boolean        := True;
+      Start_Set          : Boolean        := False;
+      Override_Start_Set : Boolean        := False;
       Containing_Pos     : Nil_Buffer_Pos := Nil; --  wisi first-keyword-pos
    begin
       for Pair of Params loop
-         if Tree.Byte_Region (Tokens (Pair.Index)) /= Null_Buffer_Region then
+         if not (Pair.Index in Tokens'Range) then
             declare
-               Token  : constant Aug_Token_Ref      := Get_Aug_Token (Data, 
Tree, Tokens (Pair.Index));
-               Cursor : Navigate_Cache_Trees.Cursor := 
Navigate_Cache_Trees.Find
-                 (Data.Navigate_Caches.Iterate, Token.Char_Region.First,
+               Nonterm_Tok : constant Aug_Token_Ref := Get_Aug_Token (Data, 
Tree, Nonterm);
+            begin
+               raise Fatal_Error with Error_Message
+                 (File_Name => -Data.Source_File_Name,
+                  Line      => Nonterm_Tok.Line,
+                  Column    => Nonterm_Tok.Column,
+                  Message   => "wisi-statement-action: " & Trimmed_Image 
(Tree.Production_ID (Nonterm)) &
+                    " token index" & SAL.Peek_Type'Image (Pair.Index) &
+                    " not in tokens range (" & SAL.Peek_Type'Image 
(Tokens'First) & " .." &
+                    SAL.Peek_Type'Image (Tokens'Last) & "); bad grammar 
action.");
+            end;
+
+         elsif Tree.Byte_Region (Tokens (Pair.Index)) /= Null_Buffer_Region 
then
+            declare
+               use all type WisiToken.Syntax_Trees.Node_Label;
+               Token  : constant Aug_Token_Ref :=
+                  (if Pair.Class = Statement_End and then
+                    Tree.Label (Tokens (Pair.Index)) = 
WisiToken.Syntax_Trees.Nonterm
+                  then Data.Terminals.Variable_Ref (Tree.Max_Terminal_Index 
(Tokens (Pair.Index)))
+                  else Get_Aug_Token (Data, Tree, Tokens (Pair.Index)));
+
+               Cache_Pos : constant Buffer_Pos         := 
Token.Char_Region.First;
+               Cursor    : Navigate_Cache_Trees.Cursor := 
Navigate_Cache_Trees.Find
+                 (Data.Navigate_Caches.Iterate, Cache_Pos,
                   Direction => Navigate_Cache_Trees.Unknown);
             begin
                if Navigate_Cache_Trees.Has_Element (Cursor) then
                   declare
                      Cache : Navigate_Cache_Type renames Data.Navigate_Caches 
(Cursor);
                   begin
-                     Cache.Class          := (if Override_Start_Set then 
Override_Start else Pair.Class);
+                     if Pair.Class = Statement_Start then
+                        if Start_Set then
+                           Cache.Class := Motion;
+                        else
+                           Cache.Class := Statement_Start;
+                           Start_Set   := True;
+                        end if;
+                     elsif Override_Start_Set then
+                        Cache.Class := Statement_Start;
+                        Start_Set   := True;
+                     else
+                        Cache.Class := Pair.Class;
+                     end if;
                      Cache.Statement_ID   := Tree.ID (Nonterm);
                      Cache.Containing_Pos := Containing_Pos;
                   end;
                else
                   Cursor := Data.Navigate_Caches.Insert
-                    ((Pos            => Token.Char_Region.First,
+                    ((Pos            => Cache_Pos,
                       Statement_ID   => Tree.ID (Nonterm),
                       ID             => Token.ID,
                       Length         => Length (Token.Char_Region),
-                      Class          => (if Override_Start_Set then 
Override_Start else Pair.Class),
+                      Class          => (if Override_Start_Set then 
Statement_Start else Pair.Class),
                       Containing_Pos => Containing_Pos,
                       others         => Nil));
                end if;
@@ -942,22 +1018,67 @@ package body Wisi is
                end if;
 
                if Pair.Class = Statement_End and Containing_Pos.Set then
-                  Set_End (Data, Containing_Pos.Item, Token.Char_Region.First);
+                  Set_End (Data, Containing_Pos.Item, Cache_Pos);
                end if;
             end;
 
          else
             --  Token.Byte_Region is null
             if First_Item and Pair.Class = Statement_Start then
-               --  We don't reset First_Item here; next token may also be a 
start, if
-               --  this one is empty.
                Override_Start_Set := True;
-               Override_Start     := Pair.Class;
             end if;
          end if;
       end loop;
    end Statement_Action;
 
+   procedure Name_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     WisiToken.Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
+      Name    : in     WisiToken.Positive_Index_Type)
+   is
+      use all type WisiToken.Syntax_Trees.Node_Label;
+   begin
+      if not (Name in Tokens'Range) then
+         declare
+            Token : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tokens (Tokens'First));
+         begin
+            raise Fatal_Error with Error_Message
+              (File_Name => -Data.Source_File_Name,
+               Line      => Token.Line,
+               Column    => Token.Column,
+               Message   => "wisi-name-action: " & Trimmed_Image 
(Tree.Production_ID (Nonterm)) & " name (" &
+                 Trimmed_Image (Name) & ") not in Tokens range (" & 
SAL.Peek_Type'Image (Tokens'First) & " .." &
+                    SAL.Peek_Type'Image (Tokens'Last) & "); bad grammar 
action.");
+         end;
+      end if;
+
+      if Tree.Label (Tokens (Name)) = Syntax_Trees.Virtual_Terminal then
+         return;
+      end if;
+
+      declare
+         use Name_Cache_Trees;
+         Name_Token : constant Aug_Token_Ref           := Get_Aug_Token (Data, 
Tree, Tokens (Name));
+         Cursor     : constant Name_Cache_Trees.Cursor := Find
+           (Data.Name_Caches.Iterate, Name_Token.Char_Region.First,
+            Direction => Name_Cache_Trees.Unknown);
+      begin
+         if Name_Token.Char_Region = Null_Buffer_Region then
+            return;
+         elsif Has_Element (Cursor) then
+            raise Fatal_Error with Error_Message
+              (File_Name => -Data.Source_File_Name,
+               Line      => Name_Token.Line,
+               Column    => Name_Token.Column,
+               Message   => "wisi-name-action: name set twice.");
+         else
+            Data.Name_Caches.Insert (Name_Token.Char_Region);
+         end if;
+      end;
+   end Name_Action;
+
    procedure Containing_Action
      (Data       : in out Parse_Data_Type;
       Tree       : in     Syntax_Trees.Tree;
@@ -1071,8 +1192,6 @@ package body Wisi is
       Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
       Params  : in     Motion_Param_Array)
    is
-      pragma Unreferenced (Nonterm);
-
       --  [2] wisi-motion-action
       use Navigate_Cache_Trees;
       use all type Ada.Containers.Count_Type;
@@ -1122,8 +1241,9 @@ package body Wisi is
                         Line      => Token.Line,
                         Column    => Token.Column,
                         Message   => "wisi-motion-action: token " &
-                          Token.Image (Data.Descriptor.all) &
-                          " has no cache; add to statement-action.");
+                          WisiToken.Image (Token.ID, Data.Descriptor.all) &
+                          " has no cache; add to statement-action for " &
+                          Trimmed_Image (Tree.Production_ID (Nonterm)) & ".");
                   end if;
                end if;
 
@@ -1373,6 +1493,7 @@ package body Wisi is
    is begin
       return "(" & Simple_Indent_Param_Label'Image (Item.Label) &
         (case Item.Label is
+         when None => "",
          when Int => Integer'Image (Item.Int_Delta),
          when Anchored_Label => Positive_Index_Type'Image 
(Item.Anchored_Index) & "," &
               Integer'Image (Item.Anchored_Delta),
@@ -1409,7 +1530,9 @@ package body Wisi is
 
       --  [2] wisi-indent-action
       for I in Tokens'Range loop
-         if Tree.Byte_Region (Tokens (I)) /= Null_Buffer_Region then
+         if Tree.Byte_Region (Tokens (I)) /= Null_Buffer_Region and
+           I in Params'Range -- in some translated EBNF, not every token has 
an indent param
+         then
             declare
                use all type WisiToken.Syntax_Trees.Node_Index;
                use all type SAL.Base_Peek_Type;
@@ -1513,6 +1636,15 @@ package body Wisi is
       end if;
    end Indent_Hanging_1;
 
+   procedure Put_Language_Action
+     (Data    : in Parse_Data_Type;
+      Content : in String)
+   is
+      pragma Unreferenced (Data);
+   begin
+      Ada.Text_IO.Put_Line ("[" & Language_Action_Code & Content & "]");
+   end Put_Language_Action;
+
    procedure Put (Data : in out Parse_Data_Type; Parser : in 
Parse.Base_Parser'Class)
    is
       use all type Ada.Containers.Count_Type;
@@ -1552,8 +1684,15 @@ package body Wisi is
       function Get_Last_Line return Line_Number_Type
       is begin
          for I in Data.Line_Begin_Pos.First_Index .. 
Data.Line_Begin_Pos.Last_Index loop
+            if Data.Line_Begin_Pos (I) = Invalid_Buffer_Pos then
+               raise SAL.Programmer_Error with "line_begin_pos" & 
Line_Number_Type'Image (I) & " invalid";
+            end if;
             if Data.Line_Begin_Pos (I) > Last_Char_Pos then
-               return I - 1;
+               if I > Line_Number_Type'First then
+                  return I - 1;
+               else
+                  return I;
+               end if;
             end if;
          end loop;
          return Data.Line_Begin_Pos.Last_Index;
@@ -1574,6 +1713,9 @@ package body Wisi is
          for Cache of Data.Navigate_Caches loop
             Put (Cache);
          end loop;
+         for Cache of Data.Name_Caches loop
+            Put (Cache);
+         end loop;
 
       when Face =>
          for Cache of Data.Face_Caches loop
@@ -1706,7 +1848,7 @@ package body Wisi is
          end case;
 
          if Item.Recover.Stack.Depth > 0 then
-            Put (Item.Recover, Data.Terminals, Data.Descriptor.all);
+            Put (Item.Recover, Data.Terminals, Data.Descriptor.all, 
Data.Embedded_Quote_Escape_Doubled);
          end if;
       end loop;
    end Put;
@@ -1725,9 +1867,11 @@ package body Wisi is
    is begin
       return "(" & Simple_Delta_Labels'Image (Item.Label) &
         (case Item.Label is
+         when None => "",
          when Int => Integer'Image (Item.Int_Delta),
          when Anchored => Integer'Image (Item.Anchored_ID) & Integer'Image 
(Item.Anchored_Delta) & " " &
-              Boolean'Image (Item.Anchored_Accumulate) & ")");
+              Boolean'Image (Item.Anchored_Accumulate))
+        & ")";
    end Image;
 
    function Image (Item : in Delta_Type) return String
@@ -1792,6 +1936,7 @@ package body Wisi is
         (case Tree.Label (Tree_Index) is
          when Shared_Terminal => Data.Terminals.Variable_Ref (Tree.Terminal 
(Tree_Index)),
          when Virtual_Terminal => raise SAL.Programmer_Error with 
"wisi_runtime.get_aug_token virtual terminal",
+         when Virtual_Identifier => raise SAL.Programmer_Error with 
"wisi_runtime.get_aug_token virtual identifier",
          when Nonterm => (Element => Augmented_Token_Access (Tree.Augmented 
(Tree_Index))));
    end Get_Aug_Token;
 
@@ -1842,6 +1987,7 @@ package body Wisi is
       Accumulate  : in     Boolean)
      return Delta_Type
    is
+      --  [2] wisi-elisp-parse--anchored-2; return an anchored delta
       use Anchor_ID_Vectors;
       --  We can't use a Reference here, because the Element in reference
       --  types is constrained (as are all allocated objects of access
@@ -1849,18 +1995,30 @@ package body Wisi is
       Indent    : Indent_Type      := Data.Indents (Anchor_Line);
       Anchor_ID : constant Integer := 1 + Max_Anchor_ID (Data, Anchor_Line, 
Last_Line);
    begin
-      --  [2] wisi-elisp-parse--anchored-2
       Data.Max_Anchor_ID := Integer'Max (Data.Max_Anchor_ID, Anchor_ID);
 
       case Indent.Label is
       when Not_Set =>
-         Indent := (Anchor, To_Vector (Anchor_ID, 1), 0);
+         Indent := (Anchor_Nil, To_Vector (Anchor_ID, 1));
+
+         if Trace_Action > Extra then
+            Ada.Text_IO.Put_Line
+              (";; indent_anchored: " & Line_Number_Type'Image (Anchor_Line) & 
" => " & Image (Indent));
+         end if;
 
       when Int =>
-         Indent := (Anchor, To_Vector (Anchor_ID, 1), Indent.Int_Indent);
+         Indent := (Anchor_Int, To_Vector (Anchor_ID, 1), Indent.Int_Indent);
+
+         if Trace_Action > Extra then
+            Ada.Text_IO.Put_Line
+              (";; indent_anchored: " & Line_Number_Type'Image (Anchor_Line) & 
" => " & Image (Indent));
+         end if;
 
-      when Anchor =>
-         Indent.Anchor_IDs := Anchor_ID & Indent.Anchor_IDs;
+      when Anchor_Nil =>
+         Indent.Anchor_Nil_IDs := Anchor_ID & Indent.Anchor_Nil_IDs;
+
+      when Anchor_Int =>
+         Indent.Anchor_Int_IDs := Anchor_ID & Indent.Anchor_Int_IDs;
 
       when Anchored =>
          Indent := (Anchor_Anchored, To_Vector (Anchor_ID, 1), 
Indent.Anchored_ID, Indent.Anchored_Delta);
@@ -1889,6 +2047,9 @@ package body Wisi is
       case Param.Label is
       when Simple =>
          case Param.Param.Label is
+         when None =>
+            return (Simple, (Label => None));
+
          when Int =>
             return (Simple, (Int, Param.Param.Int_Delta));
 
@@ -1970,12 +2131,17 @@ package body Wisi is
               (Data, Tree, Tokens, Tree_Indenting, Indenting_Comment, 
Param.Hanging_Delta_1,
                Param.Hanging_Delta_2,
                Option => False, Accumulate => True);
-         when Hanging_1 => -- wisi-hanging%
+         when Hanging_1 => -- wisi-hanging-
+            return Indent_Hanging_1
+              (Data, Tree, Tokens, Tree_Indenting, Indenting_Comment, 
Param.Hanging_Delta_1,
+               Param.Hanging_Delta_2,
+               Option => False, Accumulate => False);
+         when Hanging_2 => -- wisi-hanging%
             return Indent_Hanging_1
               (Data, Tree, Tokens, Tree_Indenting, Indenting_Comment, 
Param.Hanging_Delta_1,
                Param.Hanging_Delta_2,
                Option => True, Accumulate => True);
-         when Hanging_2 => -- wisi-hanging%-
+         when Hanging_3 => -- wisi-hanging%-
             return Indent_Hanging_1
               (Data, Tree, Tokens, Tree_Indenting, Indenting_Comment, 
Param.Hanging_Delta_1,
                Param.Hanging_Delta_2,
@@ -1990,6 +2156,7 @@ package body Wisi is
       Delta_Indent      : in     Delta_Type;
       Indenting_Comment : in     Boolean)
    is
+      --  Aplly Delta_Indent to Indenting_Token
       First_Line : constant Line_Number_Type := Indenting_Token.First_Line 
(Indenting_Comment);
       Last_Line  : constant Line_Number_Type := Indenting_Token.Last_Line 
(Indenting_Comment);
    begin
@@ -2009,7 +2176,7 @@ package body Wisi is
                if Data.Line_Begin_Token.all (Line - 1) /= 
Augmented_Token_Arrays.No_Index then
                   for Tok of Data.Terminals (Data.Line_Begin_Token.all (Line - 
1)).Non_Grammar loop
                      if Tok.Line = Line and then
-                       Tok.ID = Data.Descriptor.Comment_ID and then
+                       Tok.ID in Data.First_Comment_ID .. Data.Last_Comment_ID 
and then
                        Tok.Column = 0
                      then
                         Indent := False;
@@ -2021,7 +2188,7 @@ package body Wisi is
                if Indent then
                   Indent_Line (Data, Line, Delta_Indent);
                else
-                  Indent_Line (Data, Line, Null_Delta);
+                  Indent_Line (Data, Line, (Simple, (Int, 0)));
                end if;
             end;
          else
diff --git a/wisi.ads b/wisi.ads
index d3f1214..818a7cc 100644
--- a/wisi.ads
+++ b/wisi.ads
@@ -79,7 +79,7 @@ package Wisi is
       Nonterm : in     WisiToken.Syntax_Trees.Valid_Node_Index;
       Tokens  : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
 
-   type Navigate_Class_Type is (Motion, Name, Statement_End, 
Statement_Override, Statement_Start, Misc);
+   type Navigate_Class_Type is (Motion, Statement_End, Statement_Override, 
Statement_Start, Misc);
    --  Matches [1] wisi-class-list.
 
    type Index_Navigate_Class is record
@@ -96,6 +96,13 @@ package Wisi is
       Tokens  : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
       Params  : in     Statement_Param_Array);
 
+   procedure Name_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     WisiToken.Syntax_Trees.Tree;
+      Nonterm : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
+      Name    : in     WisiToken.Positive_Index_Type);
+
    procedure Containing_Action
      (Data       : in out Parse_Data_Type;
       Tree       : in     WisiToken.Syntax_Trees.Tree;
@@ -191,7 +198,8 @@ package Wisi is
    --  evaluated by wisi-elisp-parse--indent-compute-delta.
 
    type Simple_Indent_Param_Label is -- not hanging
-     (Int,
+     (None,
+      Int,
       Anchored_0, -- wisi-anchored
       Anchored_1, -- wisi-anchored%
       Anchored_2, -- wisi-anchored%-
@@ -224,9 +232,12 @@ package Wisi is
 
    Null_Args : Indent_Arg_Arrays.Vector renames Indent_Arg_Arrays.Empty_Vector;
 
-   type Simple_Indent_Param (Label : Simple_Indent_Param_Label := Int) is
+   type Simple_Indent_Param (Label : Simple_Indent_Param_Label := None) is
    record
       case Label is
+      when None =>
+         null;
+
       when Int =>
          Int_Delta : Integer;
 
@@ -245,10 +256,11 @@ package Wisi is
    type Indent_Param_Label is
      (Simple,
       Hanging_0, -- wisi-hanging
-      Hanging_1, -- wisi-hanging%
-      Hanging_2  -- wisi-hanging%-
+      Hanging_1, -- wisi-hanging-
+      Hanging_2, -- wisi-hanging%
+      Hanging_3  -- wisi-hanging%-
      );
-   subtype Hanging_Label is Indent_Param_Label range Hanging_0 .. Hanging_2;
+   subtype Hanging_Label is Indent_Param_Label range Hanging_0 .. Hanging_3;
 
    type Indent_Param (Label : Indent_Param_Label := Simple) is
    record
@@ -313,6 +325,13 @@ package Wisi is
    --  Language specific child packages override this to implement
    --  wisi-elisp-parse-indent-hanging-function.
 
+   type Arg_Index_Array is array (Positive range <>) of 
WisiToken.Positive_Index_Type;
+
+   procedure Put_Language_Action
+     (Data    : in Parse_Data_Type;
+      Content : in String);
+   --  Send a Language_Action message to Emacs.
+
    procedure Put (Data : in out Parse_Data_Type; Parser : in 
WisiToken.Parse.Base_Parser'Class);
    --  Perform additional post-parse actions, then put result to
    --  Ada.Text_IO.Current_Output, as encoded responses as defined in [3]
@@ -442,15 +461,15 @@ private
    Nil : constant Nil_Buffer_Pos := (Set => False);
 
    type Navigate_Cache_Type is record
-      Pos            : WisiToken.Buffer_Pos;          -- implicit in wisi-cache
-      Statement_ID   : WisiToken.Token_ID;  -- wisi-cache-nonterm
-      ID             : WisiToken.Token_ID;  -- wisi-cache-token
-      Length         : Natural;             -- wisi-cache-last
-      Class          : Navigate_Class_Type; -- wisi-cache-class; one of 
wisi-class-list
-      Containing_Pos : Nil_Buffer_Pos;      -- wisi-cache-containing
-      Prev_Pos       : Nil_Buffer_Pos;      -- wisi-cache-prev
-      Next_Pos       : Nil_Buffer_Pos;      -- wisi-cache-next
-      End_Pos        : Nil_Buffer_Pos;      -- wisi-cache-end
+      Pos            : WisiToken.Buffer_Pos; -- implicit in wisi-cache
+      Statement_ID   : WisiToken.Token_ID;   -- wisi-cache-nonterm
+      ID             : WisiToken.Token_ID;   -- wisi-cache-token
+      Length         : Natural;              -- wisi-cache-last
+      Class          : Navigate_Class_Type;  -- wisi-cache-class
+      Containing_Pos : Nil_Buffer_Pos;       -- wisi-cache-containing
+      Prev_Pos       : Nil_Buffer_Pos;       -- wisi-cache-prev
+      Next_Pos       : Nil_Buffer_Pos;       -- wisi-cache-next
+      End_Pos        : Nil_Buffer_Pos;       -- wisi-cache-end
    end record;
 
    function Key (Cache : in Navigate_Cache_Type) return WisiToken.Buffer_Pos 
is (Cache.Pos);
@@ -463,6 +482,11 @@ private
    package Navigate_Cache_Trees is new 
SAL.Gen_Unbounded_Definite_Red_Black_Trees
      (Navigate_Cache_Type, WisiToken.Buffer_Pos);
 
+   function Key (Cache : in WisiToken.Buffer_Region) return 
WisiToken.Buffer_Pos is (Cache.First);
+
+   package Name_Cache_Trees is new SAL.Gen_Unbounded_Definite_Red_Black_Trees
+     (WisiToken.Buffer_Region, WisiToken.Buffer_Pos);
+
    type Nil_Integer (Set : Boolean := False) is record
       case Set is
       when True =>
@@ -482,7 +506,7 @@ private
 
    package Face_Cache_Trees is new SAL.Gen_Unbounded_Definite_Red_Black_Trees 
(Face_Cache_Type, WisiToken.Buffer_Pos);
 
-   type Indent_Label is (Not_Set, Int, Anchor, Anchored, Anchor_Anchored);
+   type Indent_Label is (Not_Set, Int, Anchor_Nil, Anchor_Int, Anchored, 
Anchor_Anchored);
 
    package Anchor_ID_Vectors is new Ada.Containers.Vectors (Natural, Positive);
 
@@ -496,9 +520,12 @@ private
       when Int =>
          Int_Indent : Integer;
 
-      when Anchor =>
-         Anchor_IDs    : Anchor_ID_Vectors.Vector; --  Largest ID first.
-         Anchor_Indent : Integer;
+      when Anchor_Nil =>
+         Anchor_Nil_IDs : Anchor_ID_Vectors.Vector; --  Largest ID first.
+
+      when Anchor_Int =>
+         Anchor_Int_IDs    : Anchor_ID_Vectors.Vector; --  Largest ID first.
+         Anchor_Int_Indent : Integer;
 
       when Anchored =>
          Anchored_ID    : Positive;
@@ -521,6 +548,14 @@ private
      (Line_Begin_Token : not null access constant 
WisiToken.Line_Begin_Token_Vectors.Vector)
      is new WisiToken.Syntax_Trees.User_Data_Type with
    record
+      --  Aux token info
+      First_Comment_ID : WisiToken.Token_ID := WisiToken.Invalid_Token_ID;
+      Last_Comment_ID  : WisiToken.Token_ID := WisiToken.Invalid_Token_ID;
+      Left_Paren_ID    : WisiToken.Token_ID := WisiToken.Invalid_Token_ID;
+      Right_Paren_ID   : WisiToken.Token_ID := WisiToken.Invalid_Token_ID;
+
+      Embedded_Quote_Escape_Doubled : Boolean := False;
+
       --  Data from parsing
 
       Terminals : Augmented_Token_Arrays.Vector;
@@ -548,6 +583,7 @@ private
       Source_File_Name  : Ada.Strings.Unbounded.Unbounded_String;
       Post_Parse_Action : Post_Parse_Action_Type;
       Navigate_Caches   : Navigate_Cache_Trees.Tree;  -- Set by Navigate.
+      Name_Caches       : Name_Cache_Trees.Tree;      -- Set by Navigate.
       End_Positions     : Navigate_Cursor_Lists.List; -- Dynamic data for 
Navigate.
       Face_Caches       : Face_Cache_Trees.Tree;      -- Set by Face.
       Indents           : Indent_Vectors.Vector;      -- Set by Indent.
@@ -560,11 +596,29 @@ private
       Max_Anchor_ID : Integer;
    end record;
 
-   type Simple_Delta_Labels is (Int, Anchored);
+   type Simple_Delta_Labels is (None, Int, Anchored);
+
+   --  subtype Non_Anchored_Delta_Labels is Simple_Delta_Labels range None .. 
Int;
 
-   type Simple_Delta_Type (Label : Simple_Delta_Labels := Int) is
+   --  type Non_Anchored_Delta (Label : Non_Anchored_Delta_Labels := None) is
+   --  record
+   --     case Label is
+   --     when None =>
+   --        null;
+   --     when Int =>
+   --        Int_Delta : Integer;
+   --     end case;
+   --  end record;
+
+   --  function Image (Item : in Non_Anchored_Delta) return String;
+   --  For debugging
+
+   type Simple_Delta_Type (Label : Simple_Delta_Labels := None) is
    record
       case Label is
+      when None =>
+         null;
+
       when Int =>
          Int_Delta : Integer;
 
@@ -575,7 +629,6 @@ private
 
       end case;
    end record;
-   subtype Anchored_Delta is Simple_Delta_Type (Anchored);
 
    function Image (Item : in Simple_Delta_Type) return String;
    --  For debugging
@@ -598,7 +651,7 @@ private
       end case;
    end record;
 
-   Null_Delta : constant Delta_Type := (Simple, (Int, 0));
+   Null_Delta : constant Delta_Type := (Simple, (Label => None));
 
    function Image (Item : in Delta_Type) return String;
    --  For debugging
diff --git a/wisi.el b/wisi.el
index 3aa522c..5ea8d5a 100644
--- a/wisi.el
+++ b/wisi.el
@@ -1,1517 +1,1640 @@
-;;; wisi.el --- Utilities for implementing an indentation/navigation engine 
using a generalized LALR parser -*- lexical-binding:t -*-
-;;
-;; Copyright (C) 2012 - 2019  Free Software Foundation, Inc.
-;;
-;; Author: Stephen Leake <stephen_leake@stephe-leake.org>
-;; Maintainer: Stephen Leake <stephen_leake@stephe-leake.org>
-;; Keywords: parser
-;;  indentation
-;;  navigation
-;; Version: 2.1.0
-;; package-requires: ((cl-lib "1.0") (emacs "25.0") (seq "2.20"))
-;; URL: http://stephe-leake.org/ada/wisitoken.html
-;;
-;; This file is part of GNU Emacs.
-;;
-;; GNU Emacs 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 of the License, or
-;; (at your option) any later version.
-;;
-;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
-;;
-
-;;; Commentary:
-
-;;;; History: see NEWS-wisi.text
-;;
-;;;; Design:
-;;
-;; 'wisi' was originally short for "wisent indentation engine", but
-;; now is just a name. wisi was developed to support Emacs ada-mode
-;; 5.0 indentation, font-lock, and navigation, which are parser based.
-;;
-;; The approach to indenting a given token is to parse the buffer,
-;; computing a delta indent at each parse action.
-;;
-;; The parser actions also cache face and navigation information
-;; as text properties on tokens in statements.
-;;
-;; The three reasons to run the parser (indent, face, navigate) occur
-;; at different times (user indent, font-lock, user navigate), so only
-;; the relevant parser actions are run.
-;;
-;; Parsing can be noticeably slow in large files, so sometimes we do a
-;; partial parse, and keep a list of parsed regions.
-;;
-;; Since we have a cache (the text properties), we need to consider
-;; when to invalidate it.  Ideally, we invalidate only when a change
-;; to the buffer would change the result of a parse that crosses that
-;; change, or starts after that change.  Changes in whitespace
-;; (indentation and newlines) do not affect an Ada parse.  Other
-;; languages are sensitive to newlines (Bash for example) or
-;; indentation (Python).  Adding comments does not change a parse,
-;; unless code is commented out.
-;;
-;; For navigate, we expect fully accurate results, and can tolerate
-;; one initial delay, so we always parse the entire file.
-;;
-;; For font-lock, we only parse the portion of the file requested by
-;; font-lock, so we keep a list of regions, and edit that list when
-;; the buffer is changed..
-;;
-;; For indenting, we expect fast results, and can tolerate some
-;; inaccuracy until the editing is done, so we allow partial parse. We
-;; cache the indent for each line in a text property on the newline
-;; char preceding the line. `wisi-indent-region' sets the cache on all
-;; the lines computed (part of the buffer in large files), but
-;; performs the indent only on the lines in the indent
-;; region. Subsequent calls to `wisi-indent-region' apply the cached
-;; indents. Non-whitespace edits to the buffer invalidate the indent
-;; caches in the edited region and after. Since we can do partial
-;; parse, we keep a list of parsed regions.
-;;
-;; See `wisi--post-change' for the details of what we check for
-;; invalidating.
-;;
-;;;; Choice of grammar compiler and parser
-;;
-;; There are two other parsing engines available in Emacs:
-;;
-;; - SMIE
-;;
-;;   We don't use this because it is designed to parse small snippets
-;;   of code. For Ada indentation, we always need to parse the entire
-;;   buffer.
-;;
-;; - semantic
-;;
-;;   The Ada grammar as given in the Ada language reference manual is
-;;   not LALR(1). So we use a generalized parser. In addition, the
-;;   semantic lexer is more complex, and gives different information
-;;   than we need. Finally, the semantic parser does not support error
-;;   correction, and thus fails in most editing situations.
-;;
-;; We use wisitoken wisi-generate to compile BNF to Elisp source, and
-;; wisi-compile-grammar to compile that to the parser table. See
-;; ada-mode info for more information on the developer tools used for
-;; ada-mode and wisi.
-;;
-;; Alternately, to gain speed and error handling, we use wisi-generate
-;; to generate Ada source, and run that in an external process. That
-;; supports error correction while parsing.
-;;
-;;;; syntax-propertize
-;;
-;; `wisi-forward-token' relies on syntax properties, so
-;; `syntax-propertize' must be called on the text to be lexed before
-;; wisi-forward-token is called.
-;;
-;; Emacs >= 25 calls syntax-propertize transparently in the low-level
-;; lexer functions.
-;;
-;; In Emacs < 25, we call syntax-propertize in wisi-setup, and in
-;; `wisi--post-change'.
-;;
-;;;;;
-
-;;; Code:
-
-(require 'cl-lib)
-(require 'compile)
-(require 'seq)
-(require 'semantic/lex)
-(require 'wisi-parse-common)
-(require 'wisi-elisp-lexer)
-(require 'wisi-fringe)
-
-(defcustom wisi-size-threshold most-positive-fixnum
-  "Max size (in characters) for using wisi parser results for anything."
-  :type 'integer
-  :group 'wisi
-  :safe 'integerp)
-(make-variable-buffer-local 'wisi-size-threshold)
-
-(defcustom wisi-partial-parse-threshold 100001
-  "Min size (in characters) for using partial wisi parser.
-The partial parser finds a nearby start point, and parses a small
-portion of the file containing the region to parse. For indent,
-it assumes the start point is properly indented."
-  :type 'integer
-  :group 'wisi
-  :safe 'integerp)
-(make-variable-buffer-local 'wisi-partial-parse-threshold)
-
-(defvar wisi-inhibit-parse nil
-  "When non-nil, don't run the parser.
-Language code can set this non-nil when syntax is known to be
-invalid temporarily, or when making lots of changes.")
-
-(defcustom wisi-disable-face nil
-  "When non-nil, `wisi-setup' does not enable use of parser for font-lock.
-Useful when debugging parser or parser actions."
-  :type 'boolean
-  :group 'wisi
-  :safe 'booleanp)
-
-(defconst wisi-error-buffer-name "*wisi syntax errors*"
-  "Name of buffer for displaying syntax errors.")
-
-(defvar wisi-error-buffer nil
-  "Buffer for displaying syntax errors.")
-
-(defun wisi-safe-marker-pos (pos)
-  "Return an integer buffer position from POS, an integer or marker"
-  (cond
-   ((markerp pos)
-    (marker-position pos))
-
-   (t pos)))
-
-;;;; token info cache
-
-(defvar-local wisi-parse-failed nil
-  "Non-nil when a recent parse has failed - cleared when parse succeeds.")
-
-(defvar-local wisi--parse-try
-  (list
-   (cons 'face t)
-   (cons 'navigate t)
-   (cons 'indent t))
-  "Non-nil when parse is needed - cleared when parse succeeds.")
-
-(defun wisi-parse-try (&optional parse-action)
-  (cdr (assoc (or parse-action wisi--parse-action) wisi--parse-try)))
-
-(defun wisi-set-parse-try (value &optional parse-action)
-  (setcdr (assoc (or parse-action wisi--parse-action) wisi--parse-try) value))
-
-(defvar-local wisi--cached-regions
-  (list
-   (cons 'face nil)
-   (cons 'navigate nil)
-   (cons 'indent nil))
-  "Alist of lists of regions in buffer where parser text properties are valid.
-Regions in a list are in random order.")
-
-(defun wisi--contained-region (begin end region)
-  "Non-nil if BEGIN END (buffer positions) is contained in REGION (a cons of 
positions)."
-  ;; We assume begin < end
-  (and (<= (car region) begin)
-       (<= end (cdr region))))
-
-(defun wisi--contained-pos (pos region)
-  "Non-nil if POS (a buffer position) is contained in REGION (a cons of 
positions)."
-  (and (<= (car region) pos)
-       (<= pos (cdr region))))
-
-(defun wisi-cache-covers-region (begin end &optional parse-action)
-  "Non-nil if BEGIN END is contained in a parsed region."
-  (let ((region-list (cdr (assoc (or parse-action wisi--parse-action) 
wisi--cached-regions)))
-       region)
-    (while (and region-list
-               (not (wisi--contained-region begin end (car region-list))))
-      (pop region-list))
-
-    (when region-list
-      ;; return a nice value for verbosity in wisi-validate-cache
-      (setq region (car region-list))
-      (cons (marker-position (car region)) (marker-position (cdr region))))))
-
-(defun wisi-cache-covers-pos (parse-action pos)
-  "Non-nil if POS is contained in a PARSE-ACTION parsed region."
-  (let ((region-list (cdr (assoc parse-action wisi--cached-regions))))
-    (while (and region-list
-               (not (wisi--contained-pos pos (car region-list))))
-      (pop region-list))
-
-    (when region-list
-      t)))
-
-(defun wisi-cache-contains-pos (parse-action pos)
-  "Non-nil if POS is at or before the end of any PARSE-ACTION parsed region."
-  (let ((region-list (cdr (assoc parse-action wisi--cached-regions)))
-       result)
-    (while (and (not result) region-list)
-      (when (<= pos (cdr (car region-list)))
-       (setq result t))
-      (pop region-list))
-
-    result))
-
-(defun wisi-cache-set-region (region)
-  "Set the cached region list for `wisi--parse-action' to REGION."
-  (setcdr (assoc wisi--parse-action wisi--cached-regions)
-         (list (cons (copy-marker (car region))
-                     (copy-marker (cdr region))))))
-
-(defun wisi-cache-add-region (region)
-  "Add REGION to the cached region list for `wisi--parse-action'."
-  (push (cons (copy-marker (car region))
-             (copy-marker (cdr region)))
-       (cdr (assoc wisi--parse-action wisi--cached-regions))))
-
-(defun wisi-cache-delete-regions-after (parse-action pos)
-  "Delete any PARSE-ACTION parsed region at or after POS.
-Truncate any region that overlaps POS."
-  (let ((region-list (cdr (assoc parse-action wisi--cached-regions)))
-       result)
-    (while (and (not result) region-list)
-      (cond
-       ((and (> pos (car (car region-list)))
-            (<= pos (cdr (car region-list))))
-       ;; region contains POS; keep truncated
-       (push (cons (car (car region-list)) (copy-marker pos)) result))
-
-       ((> pos (car (car region-list)))
-       ;; region is entirely before POS; keep
-       (push (car region-list) result))
-
-       ;; else region is entirely after POS; delete
-       )
-
-      (pop region-list))
-    (setcdr (assoc parse-action wisi--cached-regions) result)
-    ))
-
-(defun wisi--delete-face-cache (after)
-  (with-silent-modifications
-    (remove-text-properties after (point-max) '(font-lock-face nil))
-    )
-  (wisi-cache-delete-regions-after 'face after))
-
-(defun wisi--delete-navigate-cache (after)
-  (with-silent-modifications
-    ;; This text property is 'wisi-cache', not 'wisi-navigate', for
-    ;; historical reasons.
-    (remove-text-properties after (point-max) '(wisi-cache nil))
-    )
-  (wisi-cache-delete-regions-after 'navigate after))
-
-(defun wisi--delete-indent-cache (after)
-  (with-silent-modifications
-    (remove-text-properties after (point-max) '(wisi-indent nil))
-    )
-  (wisi-cache-delete-regions-after 'indent after))
-
-(defun wisi-invalidate-cache (action after)
-  "Invalidate ACTION caches for the current buffer from AFTER to end of 
buffer."
-  (when (wisi-cache-contains-pos action after)
-    (when (> wisi-debug 0) (message "wisi-invalidate-cache %s:%s:%d" action 
(current-buffer) after))
-    (cond
-     ((eq 'face action)
-      (wisi--delete-face-cache after))
-
-     ((eq 'navigate action)
-      (when (wisi-cache-covers-pos 'navigate after)
-       ;; We goto statement start to ensure that motion within nested
-       ;; structures is properly done (ie prev/next on ’elsif’ is not
-       ;; set by wisi-motion-action if already set by a lower level
-       ;; statement). We don’t do it for ’face or ’indent, because that
-       ;; might require a parse, and they don’t care about nested
-       ;; structures.
-       (save-excursion
-         (goto-char after)
-
-         ;; This is copied from ‘wisi-goto-statement-start’; we can’t
-         ;; call that because it would call ‘wisi-validate-cache’,
-         ;; which would call ‘wisi-invalidate-cache’; infinite loop.
-         ;; If this needed a navigate parse to succeed, we would not
-         ;; get here.
-         (let ((cache (or (wisi-get-cache (point))
-                          (wisi-backward-cache))))
-           (cond
-            ((null cache)
-             ;; at bob
-             nil)
-
-            ((eq 'statement-end (wisi-cache-class cache))
-             ;; If the change did affect part of a structure statement,
-             ;; this is a lower level statement. Otherwise, we are
-             ;; invalidating more than necessary; not a problem.
-             (wisi-goto-start cache)
-             (setq cache (wisi-backward-cache))
-             (when cache ;; else bob
-               (wisi-goto-start cache)))
-
-            (t
-             (wisi-goto-start cache))
-            ))
-
-         (setq after (point))))
-      (wisi--delete-navigate-cache after))
-
-     ((eq 'indent action)
-      ;; The indent cache is stored on newline before line being
-      ;; indented. We delete that, because changing text on a line can
-      ;; change the indent of that line.
-      (setq after
-           (save-excursion
-             (goto-char after)
-             (line-beginning-position)))
-      (wisi--delete-indent-cache (max 1 (1- after))))
-     )
-    ))
-
-(defun wisi-reset-parser ()
-  "Force a parse."
-  (interactive)
-  (wisi-invalidate-cache 'indent (point-min))
-  (wisi-invalidate-cache 'face (point-min))
-  (wisi-invalidate-cache 'navigate (point-min))
-  (wisi-set-parse-try t 'indent)
-  (wisi-set-parse-try t 'face)
-  (wisi-set-parse-try t 'navigate)
-  (wisi-fringe-clean))
-
-;; wisi--change-* keep track of buffer modifications.
-;; If wisi--change-end comes before wisi--change-beg, it means there were
-;; no modifications.
-(defvar-local wisi--change-beg most-positive-fixnum
-  "First position where a change may have taken place.")
-
-(defvar-local wisi--change-end nil
-  "Marker pointing to the last position where a change may have taken place.")
-
-(defvar-local wisi--deleted-syntax nil
-  "Worst syntax class of characters deleted in changes.
-One of:
-nil - no deletions since reset
-0   - only whitespace or comment deleted
-2   - some other syntax deleted
-
-Set by `wisi-before-change', used and reset by `wisi--post-change'.")
-
-(defvar-local wisi-indenting-p nil
-  "Non-nil when `wisi-indent-region' is actively indenting.
-Used to ignore whitespace changes in before/after change hooks.")
-
-(defvar-local wisi--parser nil
-  "Choice of wisi parser implementation; a ‘wisi-parser’ object.")
-
-(defvar-local wisi--last-parse-action nil
-  "Last value of `wisi--parse-action' when `wisi-validate-cache' was run.")
-
-(defun wisi-before-change (begin end)
-  "For `before-change-functions'."
-  ;; begin . (1- end) is range of text being deleted
-  (unless wisi-indenting-p
-    ;; We set wisi--change-beg, -end even if only inserting, so we
-    ;; don't have to do it again in wisi-after-change.
-    (setq wisi--change-beg (min wisi--change-beg begin))
-
-    (cond
-     ((null wisi--change-end)
-      (setq wisi--change-end (copy-marker end)))
-
-     ((> end wisi--change-end)
-      ;; `buffer-base-buffer' deals with edits in indirect buffers
-      ;; created by ediff-regions-*
-      (set-marker wisi--change-end end (buffer-base-buffer)))
-     )
-
-    (unless (= begin end)
-      (cond
-       ((or (null wisi--deleted-syntax)
-           (= 0 wisi--deleted-syntax))
-       (save-excursion
-         (if (or (nth 4 (syntax-ppss begin)) ; in comment, moves point to begin
-                 (= end (skip-syntax-forward " " end)));; whitespace
-             (setq wisi--deleted-syntax 0)
-           (setq wisi--deleted-syntax 2))))
-
-       (t
-       ;; wisi--deleted-syntax is 2; no change.
-       )
-       ))))
-
-(defun wisi-after-change (begin end _length)
-  "For `after-change-functions'"
-  ;; begin . end is range of text being inserted (empty if equal);
-  ;; length is the size of the deleted text.
-
-  ;; Remove caches on inserted text, which could have caches from
-  ;; anywhere, and are in any case invalid.
-
-  ;; If the insertion changes a word that has wisi fontification,
-  ;; remove fontification from the entire word, so it is all
-  ;; refontified consistently.
-
-  (let (word-begin word-end)
-    (save-excursion
-      (goto-char end)
-      (skip-syntax-forward "w_")
-      (setq word-end (point))
-      (goto-char begin)
-      (skip-syntax-backward "w_")
-      (setq word-begin (point)))
-    (if (get-text-property word-begin 'font-lock-face)
-       (with-silent-modifications
-         (remove-text-properties
-          word-begin word-end
-          '(font-lock-face nil wisi-cache nil wisi-indent nil fontified nil)))
-
-      ;; No point in removing
-      ;; 'fontified here; that's already handled by jit-lock.
-      (with-silent-modifications
-       (remove-text-properties
-        begin end
-        '(font-lock-face nil wisi-cache nil wisi-indent nil))))
-    ))
-
-(defun wisi--post-change (begin end)
-  "Update wisi text properties for changes in region BEG END."
-  ;; (syntax-ppss-flush-cache begin) is in before-change-functions
-
-  ;; see comments above on syntax-propertize
-  (when (< emacs-major-version 25) (syntax-propertize end))
-
-  (save-excursion
-    (let ((need-invalidate t)
-         (done nil)
-         ;; non-nil if require a parse because the syntax may have
-         ;; changed.
-
-         (begin-state (syntax-ppss begin))
-         (end-state (syntax-ppss end)))
-         ;; (info "(elisp)Parser State")
-         ;; syntax-ppss has moved point to "end"; might be eob.
-
-      ;; consider deletion
-      (cond
-       ((null wisi--deleted-syntax)
-       ;; no deletions
-       )
-
-       ((= 0 wisi--deleted-syntax)
-       ;; Only deleted whitespace; may have joined two words
-       (when
-           (and (= begin end) ;; no insertions
-                (or
-                 (= (point-min) begin)
-                 (= 0 (syntax-class (syntax-after (1- begin))))
-                 (= (point-max) end)
-                 (= 0 (syntax-class (syntax-after end)))))
-         ;; More whitespace on at least one side of deletion; did not
-         ;; join two words.
-         (setq need-invalidate nil)
-         (setq done t)
-         ))
-
-       (t
-       ;; wisi--deleted-syntax is 2; need invalidate and parse for all
-       ;; parse actions
-       (setq done t)
-       ))
-
-      (setq wisi--deleted-syntax nil)
-
-      (unless done
-       ;; consider insertion
-       (cond
-        ((= begin end)
-         ;; no insertions
-         nil)
-
-        ((and
-          (nth 3 begin-state);; in string
-          (nth 3 end-state)
-          (= (nth 8 begin-state) (nth 8 end-state)));; no intervening 
non-string
-         (setq need-invalidate nil))
-
-        ((and
-          (nth 4 begin-state) ; in comment
-          (nth 4 end-state)
-          (= (nth 8 begin-state) (nth 8 end-state))) ;; no intervening 
non-comment
-         (setq need-invalidate nil))
-
-        ((and
-          (or
-           (= (point-min) begin)
-           (= 0 (syntax-class (syntax-after (1- begin)))); whitespace
-           (= (point-max) end)
-           (= 0 (syntax-class (syntax-after end))))
-          (progn
-            (goto-char begin)
-            (= (- end begin) (skip-syntax-forward " " end))
-            ))
-         ;; Inserted only whitespace, there is more whitespace on at
-         ;; least one side, and we are not in a comment or string
-         ;; (checked above).  This may affect indentation, but not
-         ;; the indentation cache.
-         (setq need-invalidate nil))
-        ))
-
-      (when need-invalidate
-       (wisi-set-parse-try t 'face)
-       (wisi-set-parse-try t 'navigate)
-       (wisi-set-parse-try t 'indent)
-
-       (wisi-invalidate-cache 'face begin)
-       (wisi-invalidate-cache 'navigate begin)
-       (wisi-invalidate-cache 'indent begin))
-      )))
-
-(defun wisi-goto-error ()
-  "Move point to position in last error message (if any)."
-  (cond
-   ((wisi-parser-parse-errors wisi--parser)
-    (let ((data (car (wisi-parser-parse-errors wisi--parser))))
-      (cond
-       ((wisi--parse-error-pos data)
-       (push-mark)
-       (goto-char (wisi--parse-error-pos data)))
-
-       ((string-match ":\\([0-9]+\\):\\([0-9]+\\):" (wisi--parse-error-message 
data))
-       (let* ((msg (wisi--parse-error-message data))
-              (line (string-to-number (match-string 1 msg)))
-              (col (string-to-number (match-string 2 msg))))
-         (push-mark)
-         (goto-char (point-min))
-         (condition-case nil
-             (progn
-               ;; line can be wrong if parser screws up, or user edits buffer
-               (forward-line (1- line))
-               (forward-char col))
-           (error
-            ;; just stay at eob.
-            nil))))
-       )))
-   ((wisi-parser-lexer-errors wisi--parser)
-    (push-mark)
-    (goto-char (wisi--lexer-error-pos (car (wisi-parser-lexer-errors 
wisi--parser)))))
-   ))
-
-(defun wisi-show-parse-error ()
-  "Show current wisi-parse errors."
-  (interactive)
-  (cond
-   ((or (wisi-parser-lexer-errors wisi--parser)
-       (wisi-parser-parse-errors wisi--parser))
-    (if (and (= 1 (+ (length (wisi-parser-lexer-errors wisi--parser))
-                    (length (wisi-parser-parse-errors wisi--parser))))
-            (or (and (wisi-parser-parse-errors wisi--parser)
-                     (not (wisi--parse-error-repair (car 
(wisi-parser-parse-errors wisi--parser)))))
-                (and (wisi-parser-lexer-errors wisi--parser)
-                     (not (wisi--lexer-error-inserted (car 
(wisi-parser-lexer-errors wisi--parser)))))))
-       ;; There is exactly one error; if there is error correction
-       ;; information, use a ’compilation’ buffer, so
-       ;; *-fix-compiler-error will call
-       ;; wisi-repair-error. Otherwise, just position cursor at
-       ;; error.
-       (progn
-         (wisi-goto-error)
-         (message (or (and (wisi-parser-parse-errors wisi--parser)
-                           (wisi--parse-error-message (car 
(wisi-parser-parse-errors wisi--parser))))
-                      (and (wisi-parser-lexer-errors wisi--parser)
-                           (wisi--lexer-error-message (car 
(wisi-parser-lexer-errors wisi--parser)))))
-                  ))
-
-      ;; else show all errors in a ’compilation’ buffer
-      (setq wisi-error-buffer (get-buffer-create wisi-error-buffer-name))
-
-      (let ((lexer-errs (nreverse (cl-copy-seq (wisi-parser-lexer-errors 
wisi--parser))))
-           (parse-errs (nreverse (cl-copy-seq (wisi-parser-parse-errors 
wisi--parser)))))
-       (with-current-buffer wisi-error-buffer
-         (compilation-mode)
-         (setq next-error-last-buffer (current-buffer))
-         (setq buffer-read-only nil)
-         (erase-buffer)
-         ;; compilation-nex-error-function assumes there is not an
-         ;; error at point min, so we need a comment.
-         (insert "wisi syntax errors")
-         (newline)
-         (dolist (err lexer-errs)
-           (insert (wisi--lexer-error-message err))
-           (put-text-property (line-beginning-position) (1+ 
(line-beginning-position)) 'wisi-error-data err)
-           (newline 2))
-         (dolist (err parse-errs)
-           (insert (wisi--parse-error-message err))
-           (put-text-property (line-beginning-position) (1+ 
(line-beginning-position)) 'wisi-error-data err)
-           (newline 2))
-         (compilation--flush-parse (point-min) (point-max))
-         (compilation--ensure-parse (point-max))
-         (when compilation-filter-hook
-           (let ((compilation-filter-start (point-min)))
-             (run-hooks 'compilation-filter-hook)))
-
-         (setq buffer-read-only t)
-         (goto-char (point-min)))
-
-       (display-buffer wisi-error-buffer
-                       (cons #'display-buffer-at-bottom
-                             (list (cons 'window-height 
#'shrink-window-if-larger-than-buffer))))
-       (next-error))
-      ))
-
-   ((wisi-parse-try wisi--last-parse-action)
-    (message "need parse"))
-
-   (t
-    (message "parse succeeded"))
-   ))
-
-(defun wisi-kill-parser ()
-  "Kill the background process running the parser for the current buffer.
-Usefull if the parser appears to be hung."
-  (interactive)
-  (wisi-parse-kill wisi--parser)
-  ;; also force re-parse
-  (dolist (parse-action '(face navigate indent))
-    (wisi-set-parse-try t parse-action)
-    (wisi-invalidate-cache parse-action (point-min)))
-  )
-
-(defun wisi-partial-parse-p (begin end)
-  (and (wisi-process--parser-p wisi--parser)
-       (not (and (= begin (point-min))
-                (= end (point-max))))
-       (>= (point-max) wisi-partial-parse-threshold)))
-
-(defun wisi--run-parse (begin parse-end)
-  "Run the parser, on at least region BEGIN PARSE-END."
-  (unless (or (buffer-narrowed-p)
-             (= (point-min) (point-max))) ;; some parsers can’t handle an 
empty buffer.
-    (let* ((partial-parse-p (wisi-partial-parse-p begin parse-end))
-          (msg (when (> wisi-debug 0)
-                 (format "wisi: %sparsing %s %s:%d %d %d ..."
-                         (if partial-parse-p "partial " "")
-                         wisi--parse-action
-                         (buffer-name)
-                         begin
-                         (if (markerp parse-end) (marker-position parse-end) 
parse-end)
-                         (line-number-at-pos begin))))
-          (parsed-region nil))
-
-      (when msg
-       (message msg))
-
-      (setq wisi--last-parse-action wisi--parse-action)
-
-      (unless (eq wisi--parse-action 'face)
-       (when (buffer-live-p wisi-error-buffer)
-         (with-current-buffer wisi-error-buffer
-           (setq buffer-read-only nil)
-           (erase-buffer)
-           (setq buffer-read-only t))))
-
-      (condition-case-unless-debug err
-         (save-excursion
-           (if partial-parse-p
-             (let ((send-region (wisi-parse-expand-region wisi--parser begin 
parse-end)))
-               (setq parsed-region (wisi-parse-current wisi--parser (car 
send-region) (cdr send-region) parse-end))
-               (wisi-cache-add-region parsed-region))
-
-             ;; parse full buffer
-             (setq parsed-region (cons (point-min) (point-max)))
-             (wisi-cache-set-region (wisi-parse-current wisi--parser 
(point-min) (point-max) (point-max))))
-
-           (when (> wisi-debug 0) (message "... parsed %s" parsed-region))
-           (setq wisi-parse-failed nil))
-       (wisi-parse-error
-        (cl-ecase wisi--parse-action
-          (face
-           ;; caches set by failed elisp parse are ok
-           (wisi--delete-face-cache (cdr parsed-region)))
-
-          (navigate
-           ;; elisp parse partially resets caches
-           (wisi--delete-navigate-cache (point-min)))
-
-          (indent
-           ;; parse does not set caches; see `wisi-indent-region'
-           nil))
-        (setq wisi-parse-failed t)
-        ;; parser should have stored this error message in parser-error-msgs
-        )
-       (error
-        ;; parser failed for other reason
-        (setq wisi-parse-failed t)
-        (signal (car err) (cdr err)))
-       )
-
-      (unless partial-parse-p
-       (wisi-fringe-display-errors
-        (append
-         (seq-map (lambda (err) (wisi--lexer-error-pos err)) 
(wisi-parser-lexer-errors wisi--parser))
-         (seq-map (lambda (err) (wisi--parse-error-pos err)) 
(wisi-parser-parse-errors wisi--parser)))))
-
-      (when (> wisi-debug 1)
-       (if (or (wisi-parser-lexer-errors wisi--parser)
-               (wisi-parser-parse-errors wisi--parser))
-           (progn
-             (message "%s error" msg)
-             (wisi-goto-error)
-             (error (or (and (wisi-parser-lexer-errors wisi--parser)
-                             (wisi--lexer-error-message (car 
(wisi-parser-lexer-errors wisi--parser))))
-                        (and (wisi-parser-parse-errors wisi--parser)
-                             (wisi--parse-error-message (car 
(wisi-parser-parse-errors wisi--parser))))
-                        )))
-
-         ;; no error
-         (message "%s done" msg))
-       ))))
-
-(defun wisi--check-change ()
-  "Process `wisi--change-beg', `wisi--change-end'.
-`wisi--parse-action' must be bound."
-  (when (and wisi--change-beg
-            wisi--change-end
-            (<= wisi--change-beg wisi--change-end))
-    (wisi--post-change wisi--change-beg (marker-position wisi--change-end))
-    (setq wisi--change-beg most-positive-fixnum)
-    (move-marker wisi--change-end (point-min))
-    ))
-
-(defun wisi-validate-cache (begin end error-on-fail parse-action)
-  "Ensure cached data for PARSE-ACTION is valid in region BEGIN END in current 
buffer."
-  (if (and (not wisi-inhibit-parse)
-          (< (point-max) wisi-size-threshold))
-      (let ((wisi--parse-action parse-action))
-       (wisi--check-change)
-
-       ;; Now we can rely on wisi-cache-covers-region
-
-       (if (and (or (not wisi-parse-failed)
-                    (wisi-parse-try))
-               (not (wisi-cache-covers-region begin end)))
-           (progn
-             ;; Don't keep retrying failed parse until text changes again.
-             (wisi-set-parse-try nil)
-             (wisi--run-parse begin end))
-
-         (when (> wisi-debug 0)
-           (message "parse %s skipped: parse-failed %s parse-try %s 
cache-covers-region %s %s.%s"
-                    parse-action
-                    wisi-parse-failed
-                    (wisi-parse-try)
-                    (wisi-cache-covers-region begin end)
-                    begin end)))
-
-       ;; We want this error even if we did not try to parse; it means
-       ;; the parse results are not valid.
-       (when (and error-on-fail wisi-parse-failed)
-         (error "parse %s failed" parse-action))
-       )
-    (when (> wisi-debug 0)
-      (message "parse %s skipped inihibit-parse %s wisi-size-threshold %d"
-              parse-action
-              wisi-inhibit-parse
-              wisi-size-threshold))))
-
-(defun wisi-fontify-region (begin end)
-  "For `jit-lock-functions'."
-  (wisi-validate-cache begin end nil 'face))
-
-(defun wisi-get-containing-cache (cache)
-  "Return cache from (wisi-cache-containing CACHE)."
-  (when cache
-    (let ((containing (wisi-cache-containing cache)))
-      (and containing
-          (wisi-get-cache containing)))))
-
-(defun wisi-cache-text (cache)
-  "Return property-less buffer substring designated by cache.
-Point must be at cache."
-  (buffer-substring-no-properties (point) (+ (point) (wisi-cache-last cache))))
-
-;;;; navigation
-
-(defun wisi-forward-find-class (class limit)
-  "Search at point or forward for a token that has a cache with CLASS.
-Return cache, or nil if at end of buffer.
-If LIMIT (a buffer position) is reached, throw an error."
-  (let ((cache (or (wisi-get-cache (point))
-                  (wisi-forward-cache))))
-    (while (not (eq class (wisi-cache-class cache)))
-      (setq cache (wisi-forward-cache))
-      (when (>= (point) limit)
-       (error "cache with class %s not found" class)))
-    cache))
-
-(defun wisi-forward-find-token (token limit &optional noerror)
-  "Search forward for TOKEN.
-If point is at a matching token, return that token.  TOKEN may be
-a list; stop on any member of the list.  Return `wisi-tok'
-struct, or if LIMIT (a buffer position) is reached, then if
-NOERROR is nil, throw an error, if non-nil, return nil."
-  (let ((token-list (cond
-                    ((listp token) token)
-                    (t (list token))))
-       (tok (wisi-forward-token))
-       (done nil))
-    (while (not (or done
-                   (memq (wisi-tok-token tok) token-list)))
-      (setq tok (wisi-forward-token))
-      (when (or (>= (point) limit)
-               (eobp))
-       (goto-char limit)
-       (setq tok nil)
-       (if noerror
-           (setq done t)
-         (error "token %s not found" token))))
-    tok))
-
-(defun wisi-forward-find-cache-token (ids limit)
-  "Search forward for a cache with token in IDS (a list of token ids).
-Return cache, or nil if at LIMIT or end of buffer."
-  (let ((cache (wisi-forward-cache)))
-    (while (and (< (point) limit)
-               (not (eobp))
-               (not (memq (wisi-cache-token cache) ids)))
-      (setq cache (wisi-forward-cache)))
-    cache))
-
-(defun wisi-forward-find-nonterm (nonterm limit)
-  "Search forward for a token that has a cache with NONTERM.
-NONTERM may be a list; stop on any cache that has a member of the list.
-Return cache, or nil if at end of buffer.
-If LIMIT (a buffer position) is reached, throw an error."
-  (let ((nonterm-list (cond
-                      ((listp nonterm) nonterm)
-                      (t (list nonterm))))
-       (cache (wisi-forward-cache)))
-    (while (not (memq (wisi-cache-nonterm cache) nonterm-list))
-      (setq cache (wisi-forward-cache))
-      (when (>= (point) limit)
-       (error "cache with nonterm %s not found" nonterm)))
-    cache))
-
-(defun wisi-goto-cache-next (cache)
-  (goto-char (wisi-cache-next cache))
-  (wisi-get-cache (point))
-  )
-
-(defun wisi-forward-statement-keyword ()
-  "If not at a cached token, move forward to next
-cache. Otherwise move to cache-next, or cache-end, or next cache
-if both nil.  Return cache found."
-  (unless (eobp)
-    (wisi-validate-cache (point-min) (point-max) t 'navigate)
-    (let ((cache (wisi-get-cache (point))))
-      (if (and cache
-              (not (eq (wisi-cache-class cache) 'statement-end)))
-         (let ((next (or (wisi-cache-next cache)
-                         (wisi-cache-end cache))))
-           (if next
-               (goto-char next)
-             (wisi-forward-cache)))
-       (wisi-forward-cache))
-      )
-    (wisi-get-cache (point))
-    ))
-
-(defun wisi-backward-statement-keyword ()
-  "If not at a cached token, move backward to prev
-cache. Otherwise move to cache-prev, or prev cache if nil."
-  (wisi-validate-cache (point-min) (point-max) t 'navigate)
-  (let ((cache (wisi-get-cache (point)))
-       prev)
-    (when cache
-      (setq prev (wisi-cache-prev cache))
-      (unless prev
-       (unless (eq 'statement-start (wisi-cache-class cache))
-         (setq prev (wisi-cache-containing cache)))))
-    (if prev
-       (goto-char prev)
-      (wisi-backward-cache))
-  ))
-
-(defun wisi-forward-sexp (&optional arg)
-  "For `forward-sexp-function'."
-  (interactive "^p")
-  (or arg (setq arg 1))
-  (cond
-   ((and (> arg 0) (= 4 (syntax-class (syntax-after (point)))))  ;; on open 
paren
-    (let ((forward-sexp-function nil))
-      (forward-sexp arg)))
-
-   ((and (< arg 0) (= 5 (syntax-class (syntax-after (1- (point)))))) ;; after 
close paren
-    (let ((forward-sexp-function nil))
-      (forward-sexp arg)))
-
-   ((and (> arg 0) (= 7 (syntax-class (syntax-after (point)))))  ;; on (open) 
string quote
-    (let ((forward-sexp-function nil))
-      (forward-sexp arg)))
-
-   ((and (< arg 0) (= 7 (syntax-class (syntax-after (1- (point)))))) ;; after 
(close) string quote
-    (let ((forward-sexp-function nil))
-      (forward-sexp arg)))
-
-   (t
-    (dotimes (_i (abs arg))
-      (if (> arg 0)
-         (wisi-forward-statement-keyword)
-       (wisi-backward-statement-keyword))))
-   ))
-
-(defun wisi-goto-containing (cache &optional error)
-  "Move point to containing token for CACHE, return cache at that point.
-If ERROR, throw error when CACHE has no container; else return nil."
-  (cond
-   ((and (markerp (wisi-cache-containing cache))
-
-        (not (= (wisi-cache-containing cache) (point))))
-    ;; This check is only needed if some cache points to itself as a
-    ;; container. Apparently that happend once that I caught in the
-    ;; debugger; emacs hung because we got here in the font-lock
-    ;; timer.
-
-    (goto-char (wisi-cache-containing cache))
-    (wisi-get-cache (point)))
-   (t
-    (when error
-      (error "already at outermost containing token")))
-   ))
-
-(defun wisi-goto-containing-paren (cache)
-  "Move point to just after the open-paren containing CACHE.
-Return cache for paren, or nil if no containing paren."
-  (while (and cache
-             (not (eq (wisi-cache-class cache) 'open-paren)))
-    (setq cache (wisi-goto-containing cache)))
-  (when cache
-    (forward-char 1))
-  cache)
-
-(defun wisi-goto-start (cache)
-  "Move point to containing ancestor of CACHE that has class statement-start.
-Return start cache."
-  ;; cache nil at bob, or on cache in partially parsed statement
-  (while (and cache
-             (not (eq (wisi-cache-class cache) 'statement-start)))
-    (setq cache (wisi-goto-containing cache)))
-  cache)
-
-(defun wisi-goto-end-1 (cache)
-  (goto-char (wisi-cache-end cache)))
-
-(defun wisi-goto-statement-start ()
-  "Move point to token at start of statement point is in or after.
-Return start cache."
-  (interactive)
-  (wisi-validate-cache (point-min) (point-max) t 'navigate)
-  (wisi-goto-start (or (wisi-get-cache (point))
-                      (wisi-backward-cache))))
-
-(defun wisi-goto-statement-end ()
-  "Move point to token at end of statement point is in or before."
-  (interactive)
-  (wisi-validate-cache (point-min) (point-max) t 'navigate)
-  (let ((cache (or (wisi-get-cache (point))
-                  (wisi-forward-cache))))
-    (when (wisi-cache-end cache)
-      ;; nil when cache is statement-end
-      (wisi-goto-end-1 cache))
-    ))
-
-(defun wisi-next-statement-cache (cache)
-  "Move point to CACHE-next, return cache; error if nil."
-  (when (not (markerp (wisi-cache-next cache)))
-    (error "no next statement cache"))
-  (goto-char (wisi-cache-next cache))
-  (wisi-get-cache (point)))
-
-(defun wisi-prev-statement-cache (cache)
-  "Move point to CACHE-prev, return cache; error if nil."
-  (when (not (markerp (wisi-cache-prev cache)))
-    (error "no prev statement cache"))
-  (goto-char (wisi-cache-prev cache))
-  (wisi-get-cache (point)))
-
-;;;; indentation
-
-(defun wisi-comment-indent ()
-  "For `comment-indent-function'. Indent single line comment to
-the comment on the previous line."
-  ;; Called from `comment-indent', either to insert a new comment, or
-  ;; to indent the first line of an existing one.  In either case, the
-  ;; comment may be after code on the same line.  For an existing
-  ;; comment, point is at the start of the starting delimiter.
-  (or
-   (save-excursion
-     ;; Check for a preceding comment line; fail if comment follows code.
-     (when (forward-comment -1)
-       ;; For the case:
-       ;;
-       ;; code;-- comment
-       ;;
-       ;; point is on '--', and 'forward-comment' does not move point,
-       ;; returns nil.
-       (when (looking-at comment-start)
-         (current-column))))
-
-   (save-excursion
-     (back-to-indentation)
-     (if (looking-at comment-start)
-         ;; An existing comment, no code preceding comment, and
-         ;; no comment on preceding line. Return nil, so
-         ;; `comment-indent' will call `indent-according-to-mode'
-         nil
-
-       ;; A comment after code on the same line.
-       comment-column))
-   ))
-
-(defun wisi-indent-statement ()
-  "Indent region given by `wisi-goto-start', `wisi-cache-end'."
-  (wisi-validate-cache (point-min) (point-max) t 'navigate)
-
-  (save-excursion
-    (let ((cache (or (wisi-get-cache (point))
-                    (wisi-backward-cache))))
-      (when cache
-       ;; can be nil if in header comment
-       (let ((start (progn (wisi-goto-start cache) (point)))
-             (end (if (wisi-cache-end cache)
-                        ;; nil when cache is statement-end
-                        (wisi-cache-end cache)
-                      (point))))
-         (indent-region start end)
-         ))
-      )))
-
-(defvar-local wisi-indent-calculate-functions nil
-  "Functions to compute indentation special cases.
-Called with point at current indentation of a line; return
-indentation column, or nil if function does not know how to
-indent that line. Run after parser indentation, so other lines
-are indented correctly.")
-
-(defvar-local wisi-post-indent-fail-hook
-  "Function to reindent portion of buffer.
-Called from `wisi-indent-region' when a parse succeeds after
-failing; assumes user was editing code that is now syntactically
-correct. Must leave point at indentation of current line.")
-
-(defvar-local wisi-indent-failed nil
-  "Non-nil when wisi-indent-region fails due to parse failing; cleared when 
indent succeeds.")
-
-(defvar-local wisi-indent-region-fallback 'wisi-indent-region-fallback-default
-  "Function to compute indent for lines in region when wisi parse fails.
-Called with BEGIN END.")
-
-(defun wisi-indent-region-fallback-default (begin end)
-  ;; Assume there is no indent info at point; user is editing. Indent
-  ;; to previous lines.
-  (goto-char begin)
-  (forward-line -1);; safe at bob
-  (back-to-indentation)
-  (let ((col (current-column)))
-    (while (and (not (eobp))
-               (< (point) end))
-      (forward-line 1)
-      (indent-line-to col)
-      (when (bobp)
-       ;; single line in buffer; terminate loop
-       (goto-char (point-max))))))
-
-(defun wisi-list-memq (a b)
-  "Return non-nil if any member of A is a memq of B."
-  (let ((temp (copy-sequence a))
-       result)
-    (while (and (not result)
-               temp)
-      (when (memq (pop temp) b)
-       (setq result t)))
-    result))
-
-(defun wisi--get-cached-indent (begin end)
-  "Return cached indent for point (must be bol), after correcting
-for parse errors. BEGIN, END is the parsed region."
-  (let ((indent (get-text-property (1- (point)) 'wisi-indent)))
-    (unless indent
-      (error "nil indent for line %d" (line-number-at-pos (point))))
-    (when (and (wisi-partial-parse-p begin end)
-              (< 0 (length (wisi-parser-parse-errors wisi--parser))))
-      (dolist (err (wisi-parser-parse-errors wisi--parser))
-       (dolist (repair (wisi--parse-error-repair err))
-         ;; point is at bol; error pos may be at first token on same line.
-         (save-excursion
-           (back-to-indentation)
-           (when (>= (point) (wisi--parse-error-repair-pos repair))
-             (setq indent (max 0 (wisi-parse-adjust-indent wisi--parser indent 
repair))))
-           ))))
-    indent))
-
-(defun wisi-indent-region (begin end &optional indent-blank-lines)
-  "For `indent-region-function', using the wisi indentation engine.
-If INDENT-BLANK-LINES is non-nil, also indent blank lines (for use as
-`indent-line-function')."
-  (when (< 0 wisi-debug)
-    (message "wisi-indent-region %d %d"
-            (wisi-safe-marker-pos begin)
-            (wisi-safe-marker-pos end)))
-
-  (let ((wisi--parse-action 'indent)
-       (parse-required nil)
-       (end-mark (copy-marker end))
-       (prev-indent-failed wisi-indent-failed))
-
-    (wisi--check-change)
-
-    ;; BEGIN is inclusive; END is exclusive.
-    (save-excursion
-      (goto-char begin)
-      (setq begin (line-beginning-position))
-
-      (when (bobp) (forward-line))
-      (while (and (not parse-required)
-                 (or (and (= begin end) (= (point) end))
-                     (< (point) end))
-                 (not (eobp)))
-       (unless (get-text-property (1- (point)) 'wisi-indent)
-         (setq parse-required t))
-       (forward-line))
-      )
-
-    ;; A parse either succeeds and sets the indent cache on all
-    ;; lines in the parsed region, or fails and leaves valid caches
-    ;; untouched.
-    (when (and parse-required
-              (or (not wisi-parse-failed)
-                  (wisi-parse-try 'indent)))
-
-      (wisi-set-parse-try nil)
-
-      (wisi--run-parse begin end)
-
-      ;; If there were errors corrected, the indentation is
-      ;; potentially ambiguous; see
-      ;; test/ada_mode-interactive_2.adb. Or it was a partial parse,
-      ;; where errors producing bad indent are pretty much expected.
-      (unless (wisi-partial-parse-p begin end)
-       (setq wisi-indent-failed (< 0 (+ (length (wisi-parser-lexer-errors 
wisi--parser))
-                                        (length (wisi-parser-parse-errors 
wisi--parser))))))
-      )
-
-    (if wisi-parse-failed
-       (progn
-         ;; primary indent failed
-         (setq wisi-indent-failed t)
-         (when (functionp wisi-indent-region-fallback)
-           (when (< 0 wisi-debug)
-             (message "wisi-indent-region fallback"))
-           (funcall wisi-indent-region-fallback begin end)))
-
-      (save-excursion
-       ;; Apply cached indents.
-       (goto-char begin)
-       (let ((wisi-indenting-p t))
-         (while (and (not (eobp))
-                     (or (and (= begin end) (= (point) end))
-                         (< (point) end-mark))) ;; end-mark is exclusive
-           (when (or indent-blank-lines (not (eolp)))
-             ;; ’indent-region’ doesn’t indent an empty line; ’indent-line’ 
does
-             (let ((indent (if (bobp) 0 (wisi--get-cached-indent begin end))))
-                   (indent-line-to indent))
-             )
-           (forward-line 1))
-
-         ;; Run wisi-indent-calculate-functions
-         (when wisi-indent-calculate-functions
-           (goto-char begin)
-           (while (and (not (eobp))
-                       (< (point) end-mark))
-             (back-to-indentation)
-             (let ((indent
-                    (run-hook-with-args-until-success 
'wisi-indent-calculate-functions)))
-               (when indent
-                 (indent-line-to indent)))
-
-             (forward-line 1)))
-         )
-
-       (when
-           (and prev-indent-failed
-                (not wisi-indent-failed))
-         ;; Previous parse failed or indent was potentially
-         ;; ambiguous, this one is not.
-         (goto-char end-mark)
-         (when (< 0 wisi-debug)
-           (message "wisi-indent-region post-parse-fail-hook"))
-         (run-hooks 'wisi-post-indent-fail-hook))
-       ))
-    ))
-
-(defun wisi-indent-line ()
-  "For `indent-line-function'."
-  (let ((savep (copy-marker (point)))
-       (to-indent nil))
-    (back-to-indentation)
-    (when (>= (point) savep)
-      (setq to-indent t))
-
-    (wisi-indent-region (line-beginning-position) (line-end-position) t)
-
-    (goto-char savep)
-    (when to-indent (back-to-indentation))
-    ))
-
-(defun wisi-repair-error-1 (data)
-  "Repair error reported in DATA (a ’wisi--parse-error’ or 
’wisi--lexer-error’)"
-  (let ((wisi--parse-action 'navigate) ;; tell wisi-forward-token not to 
compute indent stuff.
-       tok-2)
-    (cond
-     ((wisi--lexer-error-p data)
-      (goto-char (1+ (wisi--lexer-error-pos data)))
-      (insert (wisi--lexer-error-inserted data)))
-     ((wisi--parse-error-p data)
-      (dolist (repair (wisi--parse-error-repair data))
-       (goto-char (wisi--parse-error-repair-pos repair))
-       (dolist (tok-1 (wisi--parse-error-repair-deleted repair))
-         (setq tok-2 (wisi-forward-token))
-         (if (eq tok-1 (wisi-tok-token tok-2))
-             (delete-region (car (wisi-tok-region tok-2)) (cdr 
(wisi-tok-region tok-2)))
-           (error "mismatched tokens: %d: parser %s, buffer %s %s"
-                  (point) tok-1 (wisi-tok-token tok-2) (wisi-tok-region 
tok-2))))
-
-       (dolist (id (wisi--parse-error-repair-inserted repair))
-         (insert (cdr (assoc id (wisi-elisp-lexer-id-alist wisi--lexer))))
-         (insert " "))
-       ))
-     )))
-
-(defun wisi-repair-error ()
-  "Repair the current error."
-  (interactive)
-  (let ((wisi-inhibit-parse t)) ;; don’t let the error list change while we 
are processing it.
-    (if (= 1 (+ (length (wisi-parser-lexer-errors wisi--parser))
-               (length (wisi-parser-parse-errors wisi--parser))))
-       (progn
-         (wisi-goto-error)
-         (wisi-repair-error-1 (or (car (wisi-parser-lexer-errors wisi--parser))
-                                  (car (wisi-parser-parse-errors 
wisi--parser)))))
-      (if (buffer-live-p wisi-error-buffer)
-         (let ((err
-                (with-current-buffer wisi-error-buffer
-                  (get-text-property (point) 'wisi-error-data))))
-           (wisi-repair-error-1 err))
-       (error "no current error found")
-       ))))
-
-(defun wisi-repair-errors (&optional beg end)
-  "Repair errors reported by last parse.
-If non-nil, only repair errors in BEG END region."
-  (interactive)
-  (let ((wisi-inhibit-parse t)) ;; don’t let the error list change while we 
are processing it.
-    (dolist (data (wisi-parser-lexer-errors wisi--parser))
-      (when (or (null beg)
-               (and (not (= 0 (wisi--lexer-error-inserted data)))
-                    (wisi--lexer-error-pos data)
-                    (<= beg (wisi--lexer-error-pos data))
-                    (<= (wisi--lexer-error-pos data) end)))
-       (wisi-repair-error-1 data)))
-
-    (dolist (data (wisi-parser-parse-errors wisi--parser))
-      (when (or (null beg)
-               (and (wisi--parse-error-pos data)
-                    (<= beg (wisi--parse-error-pos data))
-                    (<= (wisi--parse-error-pos data) end)))
-       (wisi-repair-error-1 data)))
-    ))
-
-;;;; debugging
-
-(defun wisi-show-region (string)
-  (interactive "Mregion: ")
-  (when (not (= ?\( (aref string 0)))
-    (setq string (concat "(" string ")")))
-
-  (let ((region (read string)))
-    (cond
-     ((consp (cdr region))
-      ;; region is a list; (begin end)
-      (set-mark  (nth 0 region))
-      (goto-char (nth 1 region)))
-
-     ((consp region)
-      ;; region is a cons; (begin . end)
-      (set-mark  (car region))
-      (goto-char (cdr region)))
-     )))
-
-(defun wisi-debug-keys ()
-  "Add debug key definitions to `global-map'."
-  (interactive)
-  (define-key global-map "\M-h" 'wisi-show-containing-or-previous-cache)
-  (define-key global-map "\M-i" 'wisi-show-indent)
-  (define-key global-map "\M-j" 'wisi-show-cache)
-  )
-
-(defun wisi-read-parse-action ()
-  "Read a parse action symbol from the minibuffer."
-  (intern-soft (completing-read "parse action (indent): " '(face navigate 
indent) nil t nil nil 'indent)))
-
-(defun wisi-parse-buffer (&optional parse-action begin end)
-  (interactive)
-  (unless parse-action
-    (setq parse-action (wisi-read-parse-action)))
-  (if (use-region-p)
-      (progn
-       (setq begin (region-beginning))
-       (setq end   (region-end)))
-
-    (unless begin (setq begin (point-min)))
-    (unless end (setq end (point-max))))
-
-  (wisi-set-parse-try t parse-action)
-  (wisi-invalidate-cache parse-action begin)
-
-  (cl-ecase parse-action
-    (face
-     (with-silent-modifications
-       (remove-text-properties
-       begin end
-       (list
-        'font-lock-face nil
-        'fontified nil)))
-     (wisi-validate-cache begin end t parse-action)
-     (when (fboundp 'font-lock-ensure) (font-lock-ensure))) ;; emacs < 25
-
-    (navigate
-     (wisi-validate-cache begin end t parse-action))
-
-    (indent
-     (wisi-indent-region begin end))
-    ))
-
-(defun wisi-time (func count &optional report-wait-time)
-  "call FUNC COUNT times, show total time"
-  (interactive "afunction \nncount ")
-
-  (let ((start-time (float-time))
-       (start-gcs gcs-done)
-       (cum-wait-time 0.0)
-        (i 0)
-        diff-time
-       diff-gcs)
-    (while (not (eq (1+ count) (setq i (1+ i))))
-      (save-excursion
-        (funcall func))
-      (when report-wait-time
-       (setq cum-wait-time (+ cum-wait-time 
(wisi-process--parser-total-wait-time wisi--parser)))))
-    (setq diff-time (- (float-time) start-time))
-    (setq diff-gcs (- gcs-done start-gcs))
-    (if report-wait-time
-       (progn
-         (message "Total %f seconds, %d gcs; per iteration %f seconds %d gcs 
%d responses %f wait"
-                  diff-time
-                  diff-gcs
-                  (/ diff-time count)
-                  (/ (float diff-gcs) count)
-                  (wisi-process--parser-response-count wisi--parser)
-                  (/ cum-wait-time count)))
-
-      (message "Total %f seconds, %d gcs; per iteration %f seconds %d gcs"
-              diff-time
-              diff-gcs
-              (/ diff-time count)
-              (/ (float diff-gcs) count))
-      ))
-  nil)
-
-(defun wisi-time-indent-middle-line-cold-cache (count &optional 
report-wait-time)
-  (goto-char (point-min))
-  (forward-line (1- (/ (count-lines (point-min) (point-max)) 2)))
-  (let ((cum-wait-time 0.0))
-    (wisi-time
-     (lambda ()
-       (wisi-set-parse-try t 'indent)
-       (wisi-invalidate-cache 'indent (point-min))
-       (wisi-indent-line)
-       (when (wisi-process--parser-p wisi--parser)
-        (setq cum-wait-time (+ cum-wait-time 
(wisi-process--parser-total-wait-time wisi--parser)))))
-     count
-     report-wait-time)
-    ))
-
-(defun wisi-time-indent-middle-line-warm-cache (count)
-  (wisi-set-parse-try t 'indent)
-  (wisi-invalidate-cache 'indent (point-min))
-  (goto-char (point-min))
-  (forward-line (/ (count-lines (point-min) (point-max)) 2))
-  (wisi-indent-line)
-  (wisi-time #'wisi-indent-line count))
-
-(defun wisi-show-indent ()
-  "Show indent cache for current line."
-  (interactive)
-  (message "%s" (get-text-property (1- (line-beginning-position)) 
'wisi-indent)))
-
-(defun wisi-show-cache ()
-  "Show navigation cache, and applied faces, at point."
-  (interactive)
-  (message "%s:%s:%s"
-          (wisi-get-cache (point))
-          (get-text-property (point) 'face)
-          (get-text-property (point) 'font-lock-face)
-          ))
-
-(defun wisi-show-containing-or-previous-cache ()
-  (interactive)
-  (let ((cache (wisi-get-cache (point))))
-    (if cache
-       (message "containing %s" (wisi-goto-containing cache t))
-      (message "previous %s" (wisi-backward-cache)))
-    ))
-
-;;;;; setup
-
-(cl-defun wisi-setup (&key indent-calculate post-indent-fail parser lexer)
-  "Set up a buffer for parsing files with wisi."
-  (when wisi--parser
-    (wisi-kill-parser))
-
-  (setq wisi--parser parser)
-  (setq wisi--lexer lexer)
-  (setq wisi--cached-regions
-       (list
-        (cons 'face nil)
-        (cons 'navigate nil)
-        (cons 'indent nil)))
-
-  (setq wisi--parse-try
-       (list
-        (cons 'face t)
-        (cons 'navigate t)
-        (cons 'indent t)))
-
-  ;; file local variables may have added opentoken, gnatprep
-  (setq wisi-indent-calculate-functions (append 
wisi-indent-calculate-functions indent-calculate))
-  (set (make-local-variable 'indent-line-function) #'wisi-indent-line)
-  (set (make-local-variable 'indent-region-function) #'wisi-indent-region)
-  (set (make-local-variable 'forward-sexp-function) #'wisi-forward-sexp)
-
-  (setq wisi-post-indent-fail-hook post-indent-fail)
-  (setq wisi-indent-failed nil)
-
-  (add-hook 'before-change-functions #'wisi-before-change 'append t)
-  (add-hook 'after-change-functions #'wisi-after-change nil t)
-  (setq wisi--change-end (copy-marker (point-min) t))
-
-  ;; See comments above on syntax-propertize.
-  (when (< emacs-major-version 25) (syntax-propertize (point-max)))
-
-  ;; In Emacs >= 26, ‘run-mode-hooks’ (in the major mode function)
-  ;; runs ‘hack-local-variables’ after ’*-mode-hooks’; we need
-  ;; ‘wisi-post-local-vars’ to run after ‘hack-local-variables’.
-  (add-hook 'hack-local-variables-hook 'wisi-post-local-vars nil t)
-  )
-
-(defun wisi-post-local-vars ()
-  "See wisi-setup."
-  (setq hack-local-variables-hook (delq 'wisi-post-local-vars 
hack-local-variables-hook))
-
-  (unless wisi-disable-face
-    (jit-lock-register #'wisi-fontify-region)))
-
-
-(provide 'wisi)
-;;; wisi.el ends here
+;;; wisi.el --- Utilities for implementing an indentation/navigation engine 
using a generalized LALR parser -*- lexical-binding:t -*-
+;;
+;; Copyright (C) 2012 - 2019  Free Software Foundation, Inc.
+;;
+;; Author: Stephen Leake <stephen_leake@stephe-leake.org>
+;; Maintainer: Stephen Leake <stephen_leake@stephe-leake.org>
+;; Keywords: parser
+;;  indentation
+;;  navigation
+;; Version: 2.1.1
+;; package-requires: ((cl-lib "1.0") (emacs "25.0") (seq "2.20"))
+;; URL: http://stephe-leake.org/ada/wisitoken.html
+;;
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs 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 of the License, or
+;; (at your option) any later version.
+;;
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+;;
+
+;;; Commentary:
+
+;;;; History: see NEWS-wisi.text
+;;
+;;;; Design:
+;;
+;; 'wisi' was originally short for "wisent indentation engine", but
+;; now is just a name. wisi was developed to support Emacs ada-mode
+;; 5.0 indentation, font-lock, and navigation, which are parser based.
+;;
+;; The approach to indenting a given token is to parse the buffer,
+;; computing a delta indent at each parse action.
+;;
+;; The parser actions also cache face and navigation information
+;; as text properties on tokens in statements.
+;;
+;; The three reasons to run the parser (indent, face, navigate) occur
+;; at different times (user indent, font-lock, user navigate), so only
+;; the relevant parser actions are run.
+;;
+;; Parsing can be noticeably slow in large files, so sometimes we do a
+;; partial parse, and keep a list of parsed regions.
+;;
+;; Since we have a cache (the text properties), we need to consider
+;; when to invalidate it.  Ideally, we invalidate only when a change
+;; to the buffer would change the result of a parse that crosses that
+;; change, or starts after that change.  Changes in whitespace
+;; (indentation and newlines) do not affect an Ada parse.  Other
+;; languages are sensitive to newlines (Bash for example) or
+;; indentation (Python).  Adding comments does not change a parse,
+;; unless code is commented out.
+;;
+;; For navigate, we expect fully accurate results, and can tolerate
+;; one initial delay, so we always parse the entire file.
+;;
+;; For font-lock, we only parse the portion of the file requested by
+;; font-lock, so we keep a list of regions, and edit that list when
+;; the buffer is changed..
+;;
+;; For indenting, we expect fast results, and can tolerate some
+;; inaccuracy until the editing is done, so we allow partial parse. We
+;; cache the indent for each line in a text property on the newline
+;; char preceding the line. `wisi-indent-region' sets the cache on all
+;; the lines computed (part of the buffer in large files), but
+;; performs the indent only on the lines in the indent
+;; region. Subsequent calls to `wisi-indent-region' apply the cached
+;; indents. Non-whitespace edits to the buffer invalidate the indent
+;; caches in the edited region and after. Since we can do partial
+;; parse, we keep a list of parsed regions.
+;;
+;; See `wisi--post-change' for the details of what we check for
+;; invalidating.
+;;
+;;;; Choice of grammar compiler and parser
+;;
+;; There are two other parsing engines available in Emacs:
+;;
+;; - SMIE
+;;
+;;   We don't use this because it is designed to parse small snippets
+;;   of code. For Ada indentation, we always need to parse the entire
+;;   buffer.
+;;
+;; - semantic
+;;
+;;   The Ada grammar as given in the Ada language reference manual is
+;;   not LALR(1). So we use a generalized parser. In addition, the
+;;   semantic lexer is more complex, and gives different information
+;;   than we need. Finally, the semantic parser does not support error
+;;   correction, and thus fails in most editing situations.
+;;
+;; We use wisitoken wisi-generate to compile BNF to Elisp source, and
+;; wisi-compile-grammar to compile that to the parser table. See
+;; ada-mode info for more information on the developer tools used for
+;; ada-mode and wisi.
+;;
+;; Alternately, to gain speed and error handling, we use wisi-generate
+;; to generate Ada source, and run that in an external process. That
+;; supports error correction while parsing.
+;;
+;;;; syntax-propertize
+;;
+;; `wisi-forward-token' relies on syntax properties, so
+;; `syntax-propertize' must be called on the text to be lexed before
+;; wisi-forward-token is called. Emacs >= 25 calls syntax-propertize
+;; transparently in the low-level lexer functions.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'compile)
+(require 'seq)
+(require 'semantic/lex)
+(require 'wisi-parse-common)
+(require 'wisi-elisp-lexer)
+(require 'wisi-fringe)
+(require 'xref)
+
+(defcustom wisi-size-threshold most-positive-fixnum
+  "Max size (in characters) for using wisi parser results for anything."
+  :type 'integer
+  :group 'wisi
+  :safe 'integerp)
+(make-variable-buffer-local 'wisi-size-threshold)
+
+(defcustom wisi-partial-parse-threshold 100001
+  "Minimum size that will be parsed by each call to the parser.
+A parse is always requested at a point (or on a region); the
+point is first expanded to a start point before the region and an
+end point after the region, that the parser can gracefully
+handle. If the final region covers the entire buffer, a complete
+parse is done. Indent assumes the start point of the parse region
+is properly indented. Most navigate parses ignore this setting
+and parse the whole buffer."
+  :type 'integer
+  :group 'wisi
+  :safe 'integerp)
+(make-variable-buffer-local 'wisi-partial-parse-threshold)
+
+(defvar wisi-inhibit-parse nil
+  "When non-nil, don't run the parser.
+Language code can set this non-nil when syntax is known to be
+invalid temporarily, or when making lots of changes.")
+
+(defcustom wisi-disable-face nil
+  "When non-nil, `wisi-setup' does not enable use of parser for font-lock.
+Useful when debugging parser or parser actions."
+  :type 'boolean
+  :group 'wisi
+  :safe 'booleanp)
+
+(defconst wisi-error-buffer-name "*wisi syntax errors*"
+  "Name of buffer for displaying syntax errors.")
+
+(defvar wisi-error-buffer nil
+  "Buffer for displaying syntax errors.")
+
+(defun wisi-safe-marker-pos (pos)
+  "Return an integer buffer position from POS, an integer or marker"
+  (cond
+   ((markerp pos)
+    (marker-position pos))
+
+   (t pos)))
+
+;;;; token info cache
+
+(defvar-local wisi-parse-failed nil
+  "Non-nil when a recent parse has failed - cleared when parse succeeds.")
+
+(defvar-local wisi--parse-try
+  (list
+   (cons 'face t)
+   (cons 'navigate t)
+   (cons 'indent t))
+  "Non-nil when parse is needed - cleared when parse succeeds.")
+
+(defun wisi-parse-try (&optional parse-action)
+  (cdr (assoc (or parse-action wisi--parse-action) wisi--parse-try)))
+
+(defun wisi-set-parse-try (value &optional parse-action)
+  (setcdr (assoc (or parse-action wisi--parse-action) wisi--parse-try) value))
+
+(defvar-local wisi--cached-regions
+  (list
+   (cons 'face nil)
+   (cons 'navigate nil)
+   (cons 'indent nil))
+  "Alist of lists of regions in buffer where parser text properties are valid.
+Regions in a list are in random order.")
+
+(defun wisi--contained-region (begin end region)
+  "Non-nil if BEGIN and END (buffer positions) are both contained in REGION (a 
cons of positions)."
+  ;; We assume begin < end
+  (and (<= (car region) begin)
+       (<= end (cdr region))))
+
+(defun wisi--contained-pos (pos region)
+  "Non-nil if POS (a buffer position) is contained in REGION (a cons of 
positions)."
+  (and (<= (car region) pos)
+       (<= pos (cdr region))))
+
+(defun wisi-cache-covers-region (begin end &optional parse-action)
+  "Non-nil if BEGIN END is contained in a parsed region."
+  (let ((region-list (cdr (assoc (or parse-action wisi--parse-action) 
wisi--cached-regions)))
+       region)
+    (while (and region-list
+               (not (wisi--contained-region begin end (car region-list))))
+      (pop region-list))
+
+    (when region-list
+      ;; return a nice value for verbosity in wisi-validate-cache
+      (setq region (car region-list))
+      (cons (marker-position (car region)) (marker-position (cdr region))))))
+
+(defun wisi-cache-covers-pos (parse-action pos)
+  "Non-nil if POS is contained in a PARSE-ACTION parsed region."
+  (let ((region-list (cdr (assoc parse-action wisi--cached-regions))))
+    (while (and region-list
+               (not (wisi--contained-pos pos (car region-list))))
+      (pop region-list))
+
+    (when region-list
+      t)))
+
+(defun wisi-cache-contains-pos (parse-action pos)
+  "Non-nil if POS is at or before the end of any PARSE-ACTION parsed region."
+  (let ((region-list (cdr (assoc parse-action wisi--cached-regions)))
+       result)
+    (while (and (not result) region-list)
+      (when (<= pos (cdr (car region-list)))
+       (setq result t))
+      (pop region-list))
+
+    result))
+
+(defun wisi-cache-set-region (region)
+  "Set the cached region list for `wisi--parse-action' to REGION."
+  (setcdr (assoc wisi--parse-action wisi--cached-regions)
+         (list (cons (copy-marker (car region))
+                     (copy-marker (cdr region))))))
+
+(defun wisi-cache-add-region (region)
+  "Add REGION to the cached region list for `wisi--parse-action'."
+  (push (cons (copy-marker (car region))
+             (copy-marker (cdr region)))
+       (cdr (assoc wisi--parse-action wisi--cached-regions))))
+
+(defun wisi-cache-delete-regions-after (parse-action pos)
+  "Delete any PARSE-ACTION parsed region at or after POS.
+Truncate any region that overlaps POS."
+  (let ((region-list (cdr (assoc parse-action wisi--cached-regions)))
+       result)
+    (while (and (not result) region-list)
+      (cond
+       ((and (> pos (car (car region-list)))
+            (<= pos (cdr (car region-list))))
+       ;; region contains POS; keep truncated
+       (push (cons (car (car region-list)) (copy-marker pos)) result))
+
+       ((> pos (car (car region-list)))
+       ;; region is entirely before POS; keep
+       (push (car region-list) result))
+
+       ;; else region is entirely after POS; delete
+       )
+
+      (pop region-list))
+    (setcdr (assoc parse-action wisi--cached-regions) result)
+    ))
+
+(defun wisi--delete-face-cache (after)
+  (with-silent-modifications
+    (remove-text-properties after (point-max) '(font-lock-face nil))
+    )
+  (wisi-cache-delete-regions-after 'face after))
+
+(defun wisi--delete-navigate-cache (after)
+  (with-silent-modifications
+    ;; This text property is 'wisi-cache', not 'wisi-navigate', for
+    ;; historical reasons.
+    (remove-text-properties after (point-max) '(wisi-cache nil wisi-name nil))
+    )
+  (wisi-cache-delete-regions-after 'navigate after))
+
+(defun wisi--delete-indent-cache (after)
+  (with-silent-modifications
+    (remove-text-properties after (point-max) '(wisi-indent nil))
+    )
+  (wisi-cache-delete-regions-after 'indent after))
+
+(defun wisi-invalidate-cache (action after)
+  "Invalidate ACTION caches for the current buffer from AFTER to end of 
buffer."
+  (when (wisi-cache-contains-pos action after)
+    (when (> wisi-debug 0) (message "wisi-invalidate-cache %s:%s:%d" action 
(current-buffer) after))
+    (cond
+     ((eq 'face action)
+      (wisi--delete-face-cache after))
+
+     ((eq 'navigate action)
+      (when (wisi-cache-covers-pos 'navigate after)
+       ;; We goto statement start to ensure that motion within nested
+       ;; structures is properly done (ie prev/next on ’elsif’ is not
+       ;; set by wisi-motion-action if already set by a lower level
+       ;; statement). We don’t do it for ’face or ’indent, because that
+       ;; might require a parse, and they don’t care about nested
+       ;; structures.
+       (save-excursion
+         (goto-char after)
+
+         ;; This is copied from ‘wisi-goto-statement-start’; we can’t
+         ;; call that because it would call ‘wisi-validate-cache’,
+         ;; which would call ‘wisi-invalidate-cache’; infinite loop.
+         ;; If this needed a navigate parse to succeed, we would not
+         ;; get here.
+         (let ((cache (or (wisi-get-cache (point))
+                          (wisi-backward-cache))))
+           (cond
+            ((null cache)
+             ;; at bob
+             nil)
+
+            ((eq 'statement-end (wisi-cache-class cache))
+             ;; If the change did affect part of a structure statement,
+             ;; this is a lower level statement. Otherwise, we are
+             ;; invalidating more than necessary; not a problem.
+             (wisi-goto-start cache)
+             (setq cache (wisi-backward-cache))
+             (when cache ;; else bob
+               (wisi-goto-start cache)))
+
+            (t
+             (wisi-goto-start cache))
+            ))
+
+         (setq after (point))))
+      (wisi--delete-navigate-cache after))
+
+     ((eq 'indent action)
+      ;; The indent cache is stored on newline before line being
+      ;; indented. We delete that, because changing text on a line can
+      ;; change the indent of that line.
+      (setq after
+           (save-excursion
+             (goto-char after)
+             (line-beginning-position)))
+      (wisi--delete-indent-cache (max 1 (1- after))))
+     )
+    ))
+
+(defun wisi-reset-parser ()
+  "Force a parse."
+  (interactive)
+  (wisi-invalidate-cache 'indent (point-min))
+  (wisi-invalidate-cache 'face (point-min))
+  (wisi-invalidate-cache 'navigate (point-min))
+  (wisi-set-parse-try t 'indent)
+  (wisi-set-parse-try t 'face)
+  (wisi-set-parse-try t 'navigate)
+  (wisi-fringe-clean))
+
+;; wisi--change-* keep track of buffer modifications.
+;; If wisi--change-end comes before wisi--change-beg, it means there were
+;; no modifications.
+(defvar-local wisi--change-beg most-positive-fixnum
+  "First position where a change may have taken place.")
+
+(defvar-local wisi--change-end nil
+  "Marker pointing to the last position where a change may have taken place.")
+
+(defvar-local wisi--deleted-syntax nil
+  "Worst syntax class of characters deleted in changes.
+One of:
+nil - no deletions since reset
+0   - only whitespace or comment deleted
+2   - some other syntax deleted
+
+Set by `wisi-before-change', used and reset by `wisi--post-change'.")
+
+(defvar-local wisi-indenting-p nil
+  "Non-nil when `wisi-indent-region' is actively indenting.
+Used to ignore whitespace changes in before/after change hooks.")
+
+(defvar-local wisi--parser nil
+  "Choice of wisi parser implementation; a ‘wisi-parser’ object.")
+
+(defvar-local wisi--last-parse-action nil
+  "Last value of `wisi--parse-action' when `wisi-validate-cache' was run.")
+
+(defun wisi-before-change (begin end)
+  "For `before-change-functions'."
+  ;; begin . (1- end) is range of text being deleted
+  (unless wisi-indenting-p
+    ;; We set wisi--change-beg, -end even if only inserting, so we
+    ;; don't have to do it again in wisi-after-change.
+    (setq wisi--change-beg (min wisi--change-beg begin))
+
+    (cond
+     ((null wisi--change-end)
+      (setq wisi--change-end (copy-marker end)))
+
+     ((> end wisi--change-end)
+      ;; `buffer-base-buffer' deals with edits in indirect buffers
+      ;; created by ediff-regions-*
+      (set-marker wisi--change-end end (buffer-base-buffer)))
+     )
+
+    (unless (= begin end)
+      (cond
+       ((or (null wisi--deleted-syntax)
+           (= 0 wisi--deleted-syntax))
+       (save-excursion
+         (if (or (nth 4 (syntax-ppss begin)) ; in comment, moves point to begin
+                 (= end (skip-syntax-forward " " end)));; whitespace
+             (setq wisi--deleted-syntax 0)
+           (setq wisi--deleted-syntax 2))))
+
+       (t
+       ;; wisi--deleted-syntax is 2; no change.
+       )
+       ))))
+
+(defun wisi-after-change (begin end _length)
+  "For `after-change-functions'"
+  ;; begin . end is range of text being inserted (empty if equal);
+  ;; length is the size of the deleted text.
+
+  ;; Remove caches on inserted text, which could have caches from
+  ;; anywhere, and are in any case invalid.
+
+  ;; If the insertion changes a word that has wisi fontification,
+  ;; remove fontification from the entire word, so it is all
+  ;; refontified consistently.
+
+  (let (word-begin word-end)
+    (save-excursion
+      (goto-char end)
+      (skip-syntax-forward "w_")
+      (setq word-end (point))
+      (goto-char begin)
+      (skip-syntax-backward "w_")
+      (setq word-begin (point)))
+    (if (get-text-property word-begin 'font-lock-face)
+       (with-silent-modifications
+         (remove-text-properties
+          word-begin word-end
+          '(font-lock-face nil wisi-cache nil wisi-indent nil fontified nil)))
+
+      ;; No point in removing
+      ;; 'fontified here; that's already handled by jit-lock.
+      (with-silent-modifications
+       (remove-text-properties
+        begin end
+        '(font-lock-face nil wisi-cache nil wisi-indent nil))))
+    ))
+
+(defun wisi--post-change (begin end)
+  "Update wisi text properties for changes in region BEG END."
+  ;; (syntax-ppss-flush-cache begin) is in before-change-functions
+
+  (save-excursion
+    (let ((need-invalidate t)
+         (done nil)
+         ;; non-nil if require a parse because the syntax may have
+         ;; changed.
+
+         (begin-state (syntax-ppss begin))
+         (end-state (syntax-ppss end)))
+         ;; (info "(elisp)Parser State")
+         ;; syntax-ppss has moved point to "end"; might be eob.
+
+      ;; consider deletion
+      (cond
+       ((null wisi--deleted-syntax)
+       ;; no deletions
+       )
+
+       ((= 0 wisi--deleted-syntax)
+       ;; Only deleted whitespace; may have joined two words
+       (when
+           (and (= begin end) ;; no insertions
+                (or
+                 (= (point-min) begin)
+                 (= 0 (syntax-class (syntax-after (1- begin))))
+                 (= (point-max) end)
+                 (= 0 (syntax-class (syntax-after end)))))
+         ;; More whitespace on at least one side of deletion; did not
+         ;; join two words.
+         (setq need-invalidate nil)
+         (setq done t)
+         ))
+
+       (t
+       ;; wisi--deleted-syntax is 2; need invalidate and parse for all
+       ;; parse actions
+       (setq done t)
+       ))
+
+      (setq wisi--deleted-syntax nil)
+
+      (unless done
+       ;; consider insertion
+       (cond
+        ((= begin end)
+         ;; no insertions
+         nil)
+
+        ((and
+          (nth 3 begin-state);; in string
+          (nth 3 end-state)
+          (= (nth 8 begin-state) (nth 8 end-state)));; no intervening 
non-string
+         (setq need-invalidate nil))
+
+        ((and
+          (nth 4 begin-state) ;; in comment
+          (nth 4 end-state)
+          (= (nth 8 begin-state) (nth 8 end-state))) ;; no intervening 
non-comment
+
+         (if (and
+              (= 11 (car (syntax-after begin)))
+              (progn (goto-char begin)
+                     (skip-syntax-backward "<")
+                     (not (= (point) begin))))
+
+             ;; Either inserted last char of a multi-char comment
+             ;; start, or inserted extra comment-start chars.
+             (setq need-invalidate begin)
+           (setq need-invalidate nil)))
+
+        ((and
+          (or
+           (= (point-min) begin)
+           (= 0 (syntax-class (syntax-after (1- begin)))); whitespace
+           (= (point-max) end)
+           (= 0 (syntax-class (syntax-after end))))
+          (progn
+            (goto-char begin)
+            (= (- end begin) (skip-syntax-forward " " end))
+            ))
+         ;; Inserted only whitespace, there is more whitespace on at
+         ;; least one side, and we are not in a comment or string
+         ;; (checked above).  This may affect indentation, but not
+         ;; the indentation cache.
+         (setq need-invalidate nil))
+        ))
+
+      (when need-invalidate
+       (wisi-set-parse-try t 'face)
+       (wisi-set-parse-try t 'navigate)
+       (wisi-set-parse-try t 'indent)
+
+       (wisi-invalidate-cache 'face begin)
+       (wisi-invalidate-cache 'navigate begin)
+       (wisi-invalidate-cache 'indent begin))
+      )))
+
+(defun wisi-goto-error ()
+  "Move point to position in last error message (if any)."
+  (cond
+   ((wisi-parser-parse-errors wisi--parser)
+    (let ((data (car (wisi-parser-parse-errors wisi--parser))))
+      (cond
+       ((wisi--parse-error-pos data)
+       (push-mark)
+       (goto-char (wisi--parse-error-pos data)))
+
+       ((string-match ":\\([0-9]+\\):\\([0-9]+\\):" (wisi--parse-error-message 
data))
+       (let* ((msg (wisi--parse-error-message data))
+              (line (string-to-number (match-string 1 msg)))
+              (col (string-to-number (match-string 2 msg))))
+         (push-mark)
+         (goto-char (point-min))
+         (condition-case nil
+             (progn
+               ;; line can be wrong if parser screws up, or user edits buffer
+               (forward-line (1- line))
+               (forward-char col))
+           (error
+            ;; just stay at eob.
+            nil))))
+       )))
+   ((wisi-parser-lexer-errors wisi--parser)
+    (push-mark)
+    (goto-char (wisi--lexer-error-pos (car (wisi-parser-lexer-errors 
wisi--parser)))))
+   ))
+
+(defun wisi-show-parse-error ()
+  "Show current wisi-parse errors."
+  (interactive)
+  (cond
+   ((or (wisi-parser-lexer-errors wisi--parser)
+       (wisi-parser-parse-errors wisi--parser))
+    (if (and (= 1 (+ (length (wisi-parser-lexer-errors wisi--parser))
+                    (length (wisi-parser-parse-errors wisi--parser))))
+            (or (and (wisi-parser-parse-errors wisi--parser)
+                     (not (wisi--parse-error-repair (car 
(wisi-parser-parse-errors wisi--parser)))))
+                (and (wisi-parser-lexer-errors wisi--parser)
+                     (not (wisi--lexer-error-inserted (car 
(wisi-parser-lexer-errors wisi--parser)))))))
+       ;; There is exactly one error; if there is error correction
+       ;; information, use a ’compilation’ buffer, so
+       ;; *-fix-compiler-error will call
+       ;; wisi-repair-error. Otherwise, just position cursor at
+       ;; error.
+       (progn
+         (wisi-goto-error)
+         (message (or (and (wisi-parser-parse-errors wisi--parser)
+                           (wisi--parse-error-message (car 
(wisi-parser-parse-errors wisi--parser))))
+                      (and (wisi-parser-lexer-errors wisi--parser)
+                           (wisi--lexer-error-message (car 
(wisi-parser-lexer-errors wisi--parser)))))
+                  ))
+
+      ;; else show all errors in a ’compilation’ buffer
+      (setq wisi-error-buffer (get-buffer-create wisi-error-buffer-name))
+
+      (let ((lexer-errs (nreverse (cl-copy-seq (wisi-parser-lexer-errors 
wisi--parser))))
+           (parse-errs (nreverse (cl-copy-seq (wisi-parser-parse-errors 
wisi--parser))))
+           (dir default-directory))
+       (with-current-buffer wisi-error-buffer
+         (setq window-size-fixed nil)
+         (compilation-mode)
+         (setq-local compilation-search-path (list dir))
+         (setq default-directory dir)
+         (setq next-error-last-buffer (current-buffer))
+         (setq buffer-read-only nil)
+         (erase-buffer)
+         ;; compilation-nex-error-function assumes there is not an
+         ;; error at point-min, so we need a comment.
+         (insert "wisi syntax errors")
+         (newline)
+         (dolist (err lexer-errs)
+           (insert (wisi--lexer-error-message err))
+           (put-text-property (line-beginning-position) (1+ 
(line-beginning-position)) 'wisi-error-data err)
+           (newline 2))
+         (dolist (err parse-errs)
+           (insert (wisi--parse-error-message err))
+           (put-text-property (line-beginning-position) (1+ 
(line-beginning-position)) 'wisi-error-data err)
+           (newline 2))
+         (compilation--flush-parse (point-min) (point-max))
+         (compilation--ensure-parse (point-max))
+         (when compilation-filter-hook
+           (let ((compilation-filter-start (point-min)))
+             (run-hooks 'compilation-filter-hook)))
+
+         (setq buffer-read-only t)
+         (goto-char (point-min)))
+
+       (let ((win (display-buffer
+                   wisi-error-buffer
+                   (cons #'display-buffer-at-bottom
+                         (list (cons 'window-height 
#'shrink-window-if-larger-than-buffer))))))
+         (set-window-dedicated-p win t))
+
+       (with-current-buffer wisi-error-buffer
+         (setq window-size-fixed t))
+       (next-error))
+      ))
+
+   ((wisi-parse-try wisi--last-parse-action)
+    (message "need parse"))
+
+   (t
+    (message "parse succeeded"))
+   ))
+
+(defun wisi-kill-parser ()
+  "Kill the background process running the parser for the current buffer.
+Usefull if the parser appears to be hung."
+  (interactive)
+  (wisi-parse-kill wisi--parser)
+  ;; also force re-parse
+  (dolist (parse-action '(face navigate indent))
+    (wisi-set-parse-try t parse-action)
+    (wisi-invalidate-cache parse-action (point-min)))
+  )
+
+(defun wisi-partial-parse-p (begin end)
+  (and (wisi-process--parser-p wisi--parser)
+       (not (and (= begin (point-min))
+                (= end (point-max))))
+       (>= (point-max) wisi-partial-parse-threshold)))
+
+(defun wisi--run-parse (begin parse-end)
+  "Run the parser, on at least region BEGIN PARSE-END."
+  (unless (or (buffer-narrowed-p)
+             (= (point-min) (point-max))) ;; some parsers can’t handle an 
empty buffer.
+    (let* ((partial-parse-p (wisi-partial-parse-p begin parse-end))
+          (msg (when (> wisi-debug 0)
+                 (format "wisi: %sparsing %s %s:%d %d %d ..."
+                         (if partial-parse-p "partial " "")
+                         wisi--parse-action
+                         (buffer-name)
+                         begin
+                         (if (markerp parse-end) (marker-position parse-end) 
parse-end)
+                         (line-number-at-pos begin))))
+          (parsed-region nil))
+
+      (when msg
+       (message msg))
+
+      (setq wisi--last-parse-action wisi--parse-action)
+
+      (unless (eq wisi--parse-action 'face)
+       (when (buffer-live-p wisi-error-buffer)
+         (with-current-buffer wisi-error-buffer
+           (setq buffer-read-only nil)
+           (erase-buffer)
+           (setq buffer-read-only t))))
+
+      (condition-case-unless-debug err
+         (save-excursion
+           (if partial-parse-p
+             (let ((send-region (wisi-parse-expand-region wisi--parser begin 
parse-end)))
+               (setq parsed-region (wisi-parse-current wisi--parser (car 
send-region) (cdr send-region) parse-end))
+               (wisi-cache-add-region parsed-region))
+
+             ;; parse full buffer
+             (setq parsed-region (cons (point-min) (point-max)))
+             (wisi-cache-set-region (wisi-parse-current wisi--parser 
(point-min) (point-max) (point-max))))
+
+           (when (> wisi-debug 0) (message "... parsed %s" parsed-region))
+           (setq wisi-parse-failed nil))
+       (wisi-parse-error
+        (cl-ecase wisi--parse-action
+          (face
+           ;; Caches set by failed elisp parse are ok, but some parse
+           ;; failures return 'nil' in parse-region.
+           (when (cdr parsed-region)
+             (wisi--delete-face-cache (cdr parsed-region))))
+
+          (navigate
+           ;; elisp parse partially resets caches
+           (wisi--delete-navigate-cache (point-min)))
+
+          (indent
+           ;; parse does not set caches; see `wisi-indent-region'
+           nil))
+        (setq wisi-parse-failed t)
+        ;; parser should have stored this error message in parser-error-msgs
+        )
+       (error
+        ;; parser failed for other reason
+        (setq wisi-parse-failed t)
+        (signal (car err) (cdr err)))
+       )
+
+      (unless partial-parse-p
+       (wisi-fringe-display-errors
+        (append
+         (seq-map (lambda (err) (wisi--lexer-error-pos err)) 
(wisi-parser-lexer-errors wisi--parser))
+         (seq-map (lambda (err) (wisi--parse-error-pos err)) 
(wisi-parser-parse-errors wisi--parser)))))
+
+      (when (> wisi-debug 1)
+       (if (or (wisi-parser-lexer-errors wisi--parser)
+               (wisi-parser-parse-errors wisi--parser))
+           (progn
+             (message "%s error" msg)
+             (wisi-goto-error)
+             (error (or (and (wisi-parser-lexer-errors wisi--parser)
+                             (wisi--lexer-error-message (car 
(wisi-parser-lexer-errors wisi--parser))))
+                        (and (wisi-parser-parse-errors wisi--parser)
+                             (wisi--parse-error-message (car 
(wisi-parser-parse-errors wisi--parser))))
+                        )))
+
+         ;; no error
+         (message "%s done" msg))
+       ))))
+
+(defun wisi--check-change ()
+  "Process `wisi--change-beg', `wisi--change-end'.
+`wisi--parse-action' must be bound."
+  (when (and wisi--change-beg
+            wisi--change-end
+            (<= wisi--change-beg wisi--change-end))
+    (wisi--post-change wisi--change-beg (marker-position wisi--change-end))
+    (setq wisi--change-beg most-positive-fixnum)
+    (move-marker wisi--change-end (point-min))
+    ))
+
+(defun wisi-validate-cache (begin end error-on-fail parse-action)
+  "Ensure cached data for PARSE-ACTION is valid in region BEGIN END in current 
buffer."
+  (if (and (not wisi-inhibit-parse)
+          (< (point-max) wisi-size-threshold))
+      (let ((wisi--parse-action parse-action))
+       (wisi--check-change)
+
+       ;; Now we can rely on wisi-cache-covers-region
+
+       (if (and (or (not wisi-parse-failed)
+                    (wisi-parse-try))
+               (not (wisi-cache-covers-region begin end)))
+           (progn
+             ;; Don't keep retrying failed parse until text changes again.
+             (wisi-set-parse-try nil)
+             (wisi--run-parse begin end))
+
+         (when (> wisi-debug 0)
+           (message "parse %s skipped: parse-failed %s parse-try %s 
cache-covers-region %s %s.%s"
+                    parse-action
+                    wisi-parse-failed
+                    (wisi-parse-try)
+                    (wisi-cache-covers-region begin end)
+                    begin end)))
+
+       ;; We want this error even if we did not try to parse; it means
+       ;; the parse results are not valid.
+       (when (and error-on-fail wisi-parse-failed)
+         (error "parse %s failed" parse-action))
+       )
+    (when (> wisi-debug 0)
+      (message "parse %s skipped inihibit-parse %s wisi-size-threshold %d"
+              parse-action
+              wisi-inhibit-parse
+              wisi-size-threshold))))
+
+(defun wisi-fontify-region (begin end)
+  "For `jit-lock-functions'."
+  (wisi-validate-cache begin end nil 'face))
+
+(defun wisi-get-containing-cache (cache)
+  "Return cache from (wisi-cache-containing CACHE)."
+  (when cache
+    (let ((containing (wisi-cache-containing cache)))
+      (and containing
+          (wisi-get-cache containing)))))
+
+(defun wisi-cache-text (cache)
+  "Return property-less buffer substring designated by cache.
+Point must be at cache."
+  (buffer-substring-no-properties (point) (+ (point) (wisi-cache-last cache))))
+
+;;;; navigation
+
+(defun wisi-forward-find-class (class limit)
+  "Search at point or forward for a token that has a cache with CLASS.
+Return cache, or nil if at end of buffer.
+If LIMIT (a buffer position) is reached, throw an error."
+  (let ((cache (or (wisi-get-cache (point))
+                  (wisi-forward-cache))))
+    (while (not (eq class (wisi-cache-class cache)))
+      (setq cache (wisi-forward-cache))
+      (when (>= (point) limit)
+       (error "cache with class %s not found" class)))
+    cache))
+
+(defun wisi-forward-find-token (token limit &optional noerror)
+  "Search forward for TOKEN.
+If point is at a matching token, return that token.  TOKEN may be
+a list; stop on any member of the list.  Return `wisi-tok'
+struct, or if LIMIT (a buffer position) is reached, then if
+NOERROR is nil, throw an error, if non-nil, return nil."
+  (let ((token-list (cond
+                    ((listp token) token)
+                    (t (list token))))
+       (tok (wisi-forward-token))
+       (done nil))
+    (while (not (or done
+                   (memq (wisi-tok-token tok) token-list)))
+      (setq tok (wisi-forward-token))
+      (when (or (>= (point) limit)
+               (eobp))
+       (goto-char limit)
+       (setq tok nil)
+       (if noerror
+           (setq done t)
+         (error "token %s not found" token))))
+    tok))
+
+(defun wisi-forward-find-cache-token (ids limit)
+  "Search forward for a cache with token in IDS (a list of token ids).
+Return cache, or nil if at LIMIT or end of buffer."
+  (let ((cache (wisi-forward-cache)))
+    (while (and (< (point) limit)
+               (not (eobp))
+               (not (memq (wisi-cache-token cache) ids)))
+      (setq cache (wisi-forward-cache)))
+    cache))
+
+(defun wisi-forward-find-nonterm (nonterm limit)
+  "Search forward for a token that has a cache with NONTERM.
+NONTERM may be a list; stop on any cache that has a member of the list.
+Return cache, or nil if at end of buffer.
+If LIMIT (a buffer position) is reached, throw an error."
+  (let ((nonterm-list (cond
+                      ((listp nonterm) nonterm)
+                      (t (list nonterm))))
+       (cache (wisi-forward-cache)))
+    (while (not (memq (wisi-cache-nonterm cache) nonterm-list))
+      (setq cache (wisi-forward-cache))
+      (when (>= (point) limit)
+       (error "cache with nonterm %s not found" nonterm)))
+    cache))
+
+(defun wisi-goto-cache-next (cache)
+  (goto-char (wisi-cache-next cache))
+  (wisi-get-cache (point))
+  )
+
+(defun wisi-forward-statement-keyword ()
+  "If not at a cached token, move forward to next
+cache. Otherwise move to cache-next, or cache-end, or next cache
+if both nil.  Return cache found."
+  (unless (eobp)
+    (wisi-validate-cache (point-min) (point-max) t 'navigate)
+    (let ((cache (wisi-get-cache (point))))
+      (if (and cache
+              (not (eq (wisi-cache-class cache) 'statement-end)))
+         (let ((next (or (wisi-cache-next cache)
+                         (wisi-cache-end cache))))
+           (if next
+               (goto-char next)
+             (wisi-forward-cache)))
+       (wisi-forward-cache))
+      )
+    (wisi-get-cache (point))
+    ))
+
+(defun wisi-backward-statement-keyword ()
+  "If not at a cached token, move backward to prev
+cache. Otherwise move to cache-prev, or prev cache if nil."
+  (wisi-validate-cache (point-min) (point-max) t 'navigate)
+  (let ((cache (wisi-get-cache (point)))
+       prev)
+    (when cache
+      (setq prev (wisi-cache-prev cache))
+      (unless prev
+       (unless (eq 'statement-start (wisi-cache-class cache))
+         (setq prev (wisi-cache-containing cache)))))
+    (if prev
+       (goto-char prev)
+      (wisi-backward-cache))
+  ))
+
+(defun wisi-forward-sexp (&optional arg)
+  "For `forward-sexp-function'."
+  (interactive "^p")
+  (or arg (setq arg 1))
+  (cond
+   ((and (> arg 0) (= 4 (syntax-class (syntax-after (point)))))  ;; on open 
paren
+    (let ((forward-sexp-function nil))
+      (forward-sexp arg)))
+
+   ((and (< arg 0) (= 5 (syntax-class (syntax-after (1- (point)))))) ;; after 
close paren
+    (let ((forward-sexp-function nil))
+      (forward-sexp arg)))
+
+   ((and (> arg 0) (= 7 (syntax-class (syntax-after (point)))))  ;; on (open) 
string quote
+    (let ((forward-sexp-function nil))
+      (forward-sexp arg)))
+
+   ((and (< arg 0) (= 7 (syntax-class (syntax-after (1- (point)))))) ;; after 
(close) string quote
+    (let ((forward-sexp-function nil))
+      (forward-sexp arg)))
+
+   (t
+    (dotimes (_i (abs arg))
+      (if (> arg 0)
+         (wisi-forward-statement-keyword)
+       (wisi-backward-statement-keyword))))
+   ))
+
+(defun wisi-goto-containing (cache &optional error)
+  "Move point to containing token for CACHE, return cache at that point.
+If ERROR, throw error when CACHE has no container; else return nil."
+  (cond
+   ((and (markerp (wisi-cache-containing cache))
+
+        (not (= (wisi-cache-containing cache) (point))))
+    ;; This check is only needed if some cache points to itself as a
+    ;; container. Apparently that happend once that I caught in the
+    ;; debugger; emacs hung because we got here in the font-lock
+    ;; timer.
+
+    (goto-char (wisi-cache-containing cache))
+    (wisi-get-cache (point)))
+   (t
+    (when error
+      (error "already at outermost containing token")))
+   ))
+
+(defun wisi-goto-containing-paren (cache)
+  "Move point to just after the open-paren containing CACHE.
+Return cache for paren, or nil if no containing paren."
+  (while (and cache
+             (not (eq (wisi-cache-class cache) 'open-paren)))
+    (setq cache (wisi-goto-containing cache)))
+  (when cache
+    (forward-char 1))
+  cache)
+
+(defun wisi-goto-start (cache)
+  "Move point to containing ancestor of CACHE that has class statement-start.
+Return start cache."
+  ;; cache nil at bob, or on cache in partially parsed statement
+  (while (and cache
+             (not (eq (wisi-cache-class cache) 'statement-start)))
+    (setq cache (wisi-goto-containing cache)))
+  cache)
+
+(defun wisi-goto-end-1 (cache)
+  (goto-char (wisi-cache-end cache)))
+
+(defun wisi-goto-statement-start ()
+  "Move point to token at start of statement point is in or after.
+Return start cache."
+  (interactive)
+  (wisi-validate-cache (point-min) (point-max) t 'navigate)
+  (wisi-goto-start (or (wisi-get-cache (point))
+                      (wisi-backward-cache))))
+
+(defun wisi-goto-statement-end ()
+  "Move point to token at end of statement point is in or before."
+  (interactive)
+  (wisi-validate-cache (point-min) (point-max) t 'navigate)
+  (let ((cache (or (wisi-get-cache (point))
+                  (wisi-forward-cache))))
+    (when (wisi-cache-end cache)
+      ;; nil when cache is statement-end
+      (wisi-goto-end-1 cache))
+    ))
+
+(defun wisi-next-statement-cache (cache)
+  "Move point to CACHE-next, return cache; error if nil."
+  (when (not (markerp (wisi-cache-next cache)))
+    (error "no next statement cache"))
+  (goto-char (wisi-cache-next cache))
+  (wisi-get-cache (point)))
+
+(defun wisi-prev-statement-cache (cache)
+  "Move point to CACHE-prev, return cache; error if nil."
+  (when (not (markerp (wisi-cache-prev cache)))
+    (error "no prev statement cache"))
+  (goto-char (wisi-cache-prev cache))
+  (wisi-get-cache (point)))
+
+;;;; indentation
+
+(defun wisi-comment-indent ()
+  "For `comment-indent-function'. Indent single line comment to
+the comment on the previous line."
+  ;; Called from `comment-indent', either to insert a new comment, or
+  ;; to indent the first line of an existing one.  In either case, the
+  ;; comment may be after code on the same line.  For an existing
+  ;; comment, point is at the start of the starting delimiter.
+  (or
+   (save-excursion
+     ;; Check for a preceding comment line; fail if comment follows code.
+     (when (forward-comment -1)
+       ;; For the case:
+       ;;
+       ;; code;-- comment
+       ;;
+       ;; point is on '--', and 'forward-comment' does not move point,
+       ;; returns nil.
+       (when (looking-at comment-start)
+         (current-column))))
+
+   (save-excursion
+     (back-to-indentation)
+     (if (looking-at comment-start)
+         ;; An existing comment, no code preceding comment, and
+         ;; no comment on preceding line. Return nil, so
+         ;; `comment-indent' will call `indent-according-to-mode'
+         nil
+
+       ;; A comment after code on the same line.
+       comment-column))
+   ))
+
+(defun wisi-indent-statement ()
+  "Indent region given by `wisi-goto-start', `wisi-cache-end'."
+  (interactive)
+  (wisi-validate-cache (point-min) (point-max) t 'navigate)
+
+  (save-excursion
+    (let ((cache (or (wisi-get-cache (point))
+                    (wisi-backward-cache))))
+      (when cache
+       ;; can be nil if in header comment
+       (let ((start (progn (wisi-goto-start cache) (point)))
+             (end (if (wisi-cache-end cache)
+                        ;; nil when cache is statement-end
+                        (wisi-cache-end cache)
+                      (point))))
+         (indent-region start end)
+         ))
+      )))
+
+(defvar-local wisi-indent-calculate-functions nil
+  "Functions to compute indentation special cases.
+Called with point at current indentation of a line; return
+indentation column, or nil if function does not know how to
+indent that line. Run after parser indentation, so other lines
+are indented correctly.")
+
+(defvar-local wisi-post-indent-fail-hook
+  "Function to reindent portion of buffer.
+Called from `wisi-indent-region' when a parse succeeds after
+failing; assumes user was editing code that is now syntactically
+correct. Must leave point at indentation of current line.")
+
+(defvar-local wisi-indent-failed nil
+  "Non-nil when wisi-indent-region fails due to parse failing; cleared when 
indent succeeds.")
+
+(defvar-local wisi-indent-region-fallback 'wisi-indent-region-fallback-default
+  "Function to compute indent for lines in region when wisi parse fails.
+Called with BEGIN END.")
+
+(defun wisi-indent-region-fallback-default (begin end)
+  ;; Assume there is no indent info at point; user is editing. Indent
+  ;; to previous lines.
+  (goto-char begin)
+  (forward-line -1);; safe at bob
+  (back-to-indentation)
+  (let ((col (current-column)))
+    (while (and (not (eobp))
+               (< (point) end))
+      (forward-line 1)
+      (indent-line-to col)
+      (when (bobp)
+       ;; single line in buffer; terminate loop
+       (goto-char (point-max))))))
+
+(defun wisi-list-memq (a b)
+  "Return non-nil if any member of A is a memq of B."
+  (let ((temp (copy-sequence a))
+       result)
+    (while (and (not result)
+               temp)
+      (when (memq (pop temp) b)
+       (setq result t)))
+    result))
+
+(defun wisi--get-cached-indent (begin end)
+  "Return cached indent for point (must be bol), after correcting
+for parse errors. BEGIN, END is the parsed region."
+  (let ((indent (get-text-property (1- (point)) 'wisi-indent)))
+    (if indent
+       (when (and (wisi-partial-parse-p begin end)
+                  (< 0 (length (wisi-parser-parse-errors wisi--parser))))
+         (dolist (err (wisi-parser-parse-errors wisi--parser))
+           (dolist (repair (wisi--parse-error-repair err))
+             ;; point is at bol; error pos may be at first token on same line.
+             (save-excursion
+               (back-to-indentation)
+               (when (>= (point) (wisi--parse-error-repair-pos repair))
+                 (setq indent (max 0 (wisi-parse-adjust-indent wisi--parser 
indent repair))))
+               ))))
+      ;; parse did not compute indent for point. Assume the error will
+      ;; go away soon as the user edits the code, so just return 0.
+      (if (= wisi-debug 0)
+         (setq indent 0)
+       (error "nil indent for line %d" (line-number-at-pos (point)))))
+
+    indent))
+
+(defun wisi-indent-region (begin end &optional indent-blank-lines)
+  "For `indent-region-function', using the wisi indentation engine.
+If INDENT-BLANK-LINES is non-nil, also indent blank lines (for use as
+`indent-line-function')."
+  (when (< 0 wisi-debug)
+    (message "wisi-indent-region %d %d"
+            (wisi-safe-marker-pos begin)
+            (wisi-safe-marker-pos end)))
+
+  (let ((wisi--parse-action 'indent)
+       (parse-required nil)
+       (end-mark (copy-marker end))
+       (prev-indent-failed wisi-indent-failed))
+
+    (wisi--check-change)
+
+    ;; BEGIN is inclusive; END is exclusive.
+    (save-excursion
+      (goto-char begin)
+      (setq begin (line-beginning-position))
+
+      (when (bobp) (forward-line))
+      (while (and (not parse-required)
+                 (or (and (= begin end) (= (point) end))
+                     (< (point) end))
+                 (not (eobp)))
+       (unless (get-text-property (1- (point)) 'wisi-indent)
+         (setq parse-required t))
+       (forward-line))
+      )
+
+    ;; A parse either succeeds and sets the indent cache on all
+    ;; lines in the parsed region, or fails and leaves valid caches
+    ;; untouched.
+    (when (and parse-required
+              (or (not wisi-parse-failed)
+                  (wisi-parse-try 'indent)))
+
+      (wisi-set-parse-try nil)
+
+      (wisi--run-parse begin end)
+
+      ;; If there were errors corrected, the indentation is
+      ;; potentially ambiguous; see
+      ;; test/ada_mode-interactive_2.adb. Or it was a partial parse,
+      ;; where errors producing bad indent are pretty much expected.
+      (unless (wisi-partial-parse-p begin end)
+       (setq wisi-indent-failed (< 0 (+ (length (wisi-parser-lexer-errors 
wisi--parser))
+                                        (length (wisi-parser-parse-errors 
wisi--parser))))))
+      )
+
+    (if wisi-parse-failed
+       (progn
+         ;; primary indent failed
+         (setq wisi-indent-failed t)
+         (when (functionp wisi-indent-region-fallback)
+           (when (< 0 wisi-debug)
+             (message "wisi-indent-region fallback"))
+           (funcall wisi-indent-region-fallback begin end)))
+
+      (save-excursion
+       ;; Apply cached indents.
+       (goto-char begin)
+       (let ((wisi-indenting-p t))
+         (while (and (not (eobp))
+                     (or (and (= begin end) (= (point) end))
+                         (< (point) end-mark))) ;; end-mark is exclusive
+           (when (or indent-blank-lines (not (eolp)))
+             ;; ’indent-region’ doesn’t indent an empty line; ’indent-line’ 
does
+             (let ((indent (if (bobp) 0 (wisi--get-cached-indent begin end))))
+                   (indent-line-to indent))
+             )
+           (forward-line 1))
+
+         ;; Run wisi-indent-calculate-functions
+         (when wisi-indent-calculate-functions
+           (goto-char begin)
+           (while (and (not (eobp))
+                       (< (point) end-mark))
+             (back-to-indentation)
+             (let ((indent
+                    (run-hook-with-args-until-success 
'wisi-indent-calculate-functions)))
+               (when indent
+                 (indent-line-to indent)))
+
+             (forward-line 1)))
+         )
+
+       (when
+           (and prev-indent-failed
+                (not wisi-indent-failed))
+         ;; Previous parse failed or indent was potentially
+         ;; ambiguous, this one is not.
+         (goto-char end-mark)
+         (when (< 0 wisi-debug)
+           (message "wisi-indent-region post-parse-fail-hook"))
+         (run-hooks 'wisi-post-indent-fail-hook))
+       ))
+    ))
+
+(defun wisi-indent-line ()
+  "For `indent-line-function'."
+  (let ((savep (copy-marker (point)))
+       (to-indent nil))
+    (back-to-indentation)
+    (when (>= (point) savep)
+      (setq to-indent t))
+
+    (wisi-indent-region (line-beginning-position) (line-end-position) t)
+
+    (goto-char savep)
+    (when to-indent (back-to-indentation))
+    ))
+
+(defun wisi-repair-error-1 (data)
+  "Repair error reported in DATA (a ’wisi--parse-error’ or 
’wisi--lexer-error’)"
+  (let ((wisi--parse-action 'navigate) ;; tell wisi-forward-token not to 
compute indent stuff.
+       tok-2)
+    (cond
+     ((wisi--lexer-error-p data)
+      (goto-char (1+ (wisi--lexer-error-pos data)))
+      (insert (wisi--lexer-error-inserted data)))
+     ((wisi--parse-error-p data)
+      (dolist (repair (wisi--parse-error-repair data))
+       (goto-char (wisi--parse-error-repair-pos repair))
+       (dolist (tok-1 (wisi--parse-error-repair-deleted repair))
+         (setq tok-2 (wisi-forward-token))
+         (if (eq tok-1 (wisi-tok-token tok-2))
+             (delete-region (car (wisi-tok-region tok-2)) (cdr 
(wisi-tok-region tok-2)))
+           (error "mismatched tokens: %d: parser %s, buffer %s %s"
+                  (point) tok-1 (wisi-tok-token tok-2) (wisi-tok-region 
tok-2))))
+
+       (dolist (id (wisi--parse-error-repair-inserted repair))
+         (insert (cdr (assoc id (wisi-elisp-lexer-id-alist wisi--lexer))))
+         (insert " "))
+       ))
+     )))
+
+(defun wisi-repair-error ()
+  "Repair the current error."
+  (interactive)
+  (let ((wisi-inhibit-parse t)) ;; don’t let the error list change while we 
are processing it.
+    (if (= 1 (+ (length (wisi-parser-lexer-errors wisi--parser))
+               (length (wisi-parser-parse-errors wisi--parser))))
+       (progn
+         (wisi-goto-error)
+         (wisi-repair-error-1 (or (car (wisi-parser-lexer-errors wisi--parser))
+                                  (car (wisi-parser-parse-errors 
wisi--parser)))))
+      (if (buffer-live-p wisi-error-buffer)
+         (let ((err
+                (with-current-buffer wisi-error-buffer
+                  (get-text-property (point) 'wisi-error-data))))
+           (wisi-repair-error-1 err))
+       (error "no current error found")
+       ))))
+
+(defun wisi-repair-errors (&optional beg end)
+  "Repair errors reported by last parse.
+If non-nil, only repair errors in BEG END region."
+  (interactive)
+  (let ((wisi-inhibit-parse t)) ;; don’t let the error list change while we 
are processing it.
+    (dolist (data (wisi-parser-lexer-errors wisi--parser))
+      (when (or (null beg)
+               (and (not (= 0 (wisi--lexer-error-inserted data)))
+                    (wisi--lexer-error-pos data)
+                    (<= beg (wisi--lexer-error-pos data))
+                    (<= (wisi--lexer-error-pos data) end)))
+       (wisi-repair-error-1 data)))
+
+    (dolist (data (wisi-parser-parse-errors wisi--parser))
+      (when (or (null beg)
+               (and (wisi--parse-error-pos data)
+                    (<= beg (wisi--parse-error-pos data))
+                    (<= (wisi--parse-error-pos data) end)))
+       (wisi-repair-error-1 data)))
+    ))
+
+;;; xref integration
+(defconst wisi-xref-ident-regexp "\\([^<]*\\)\\(?:<\\([0-9]+\\)>\\)?"
+  "Match line number encoded into identifier by 
`wisi-xref-identifier-at-point'.")
+
+(defun wisi-xref-ident-make (identifier &optional other-function)
+  (let* ((t-prop (get-text-property 0 'xref-identifier identifier))
+        ;; If t-prop is non-nil: identifier is from
+        ;; identifier-at-point, the desired location is the ’other’
+        ;; (spec/body).
+        ;;
+        ;; If t-prop is nil: identifier is from prompt/completion,
+        ;; the line number may be included in the identifier
+        ;; wrapped in <>, and the desired file is the current file.
+        (ident
+         (if t-prop
+             (substring-no-properties identifier 0 nil)
+           (string-match wisi-xref-ident-regexp identifier)
+           (match-string 1 identifier)
+           ))
+        (file
+         (if t-prop
+             (plist-get t-prop ':file)
+           (buffer-file-name)))
+        (line
+         (if t-prop
+             (plist-get t-prop ':line)
+           (when (match-string 2 identifier)
+             (string-to-number (match-string 2 identifier)))))
+        (column
+         (if t-prop
+             (plist-get t-prop ':column)
+           0))
+        )
+
+    (if t-prop
+       (funcall other-function ident file line column)
+
+      (list (xref-make ident (xref-make-file-location file (or line 1) 
column)))
+      )))
+
+(defun wisi-xref-identifier-at-point ()
+  (let ((ident (thing-at-point 'symbol)))
+    (when ident
+      (put-text-property
+       0 1
+       'xref-identifier
+       (list ':file (buffer-file-name)
+            ':line (line-number-at-pos)
+            ':column (current-column))
+       ident)
+      ident)))
+
+(defun wisi-next-name-region ()
+  "Return the next region at or after point with text property 'wisi-name'."
+  (let* ((begin
+         (if (get-text-property (point) 'wisi-name)
+             (point)
+           (next-single-property-change (point) 'wisi-name)))
+        (end (next-single-property-change begin 'wisi-name)))
+    (cons begin end)))
+
+(defun wisi-prev-name-region ()
+  "Return the prev region at or before point with text property 'wisi-name'."
+  (let* ((end
+         (if (get-text-property (point) 'wisi-name)
+             (point)
+           (previous-single-property-change (point) 'wisi-name)))
+        (begin (previous-single-property-change end 'wisi-name)))
+    (cons begin end)))
+
+(defun wisi-next-name ()
+  "Return the text at or after point with text property 'wisi-name'."
+  (let ((region (wisi-next-name-region)))
+    (buffer-substring-no-properties (car region) (cdr region))))
+
+(defun wisi-prev-name ()
+  "Return the text at or before point with text property 'wisi-name'."
+  (let ((region (wisi-prev-name-region)))
+    (buffer-substring-no-properties (car region) (cdr region))))
+
+(defun wisi-xref-identifier-completion-table ()
+  (wisi-validate-cache (point-min) (point-max) t 'navigate)
+  (let ((table nil)
+       (pos (point-min))
+       end-pos)
+    (while (setq pos (next-single-property-change pos 'wisi-name))
+      ;; We can’t store location data in a string text property -
+      ;; it does not survive completion. So we include the line
+      ;; number in the identifier string. This also serves to
+      ;; disambiguate overloaded identifiers.
+      (setq end-pos (next-single-property-change pos 'wisi-name))
+      (push
+       (format "%s<%d>"
+              (buffer-substring-no-properties pos end-pos)
+              (line-number-at-pos pos))
+       table)
+      (setq pos end-pos)
+      )
+    table))
+
+;;;; debugging
+
+(defun wisi-show-region (string)
+  (interactive "Mregion: ")
+  (when (not (= ?\( (aref string 0)))
+    (setq string (concat "(" string ")")))
+
+  (let ((region (read string)))
+    (cond
+     ((consp (cdr region))
+      ;; region is a list; (begin end)
+      (set-mark  (nth 0 region))
+      (goto-char (nth 1 region)))
+
+     ((consp region)
+      ;; region is a cons; (begin . end)
+      (set-mark  (car region))
+      (goto-char (cdr region)))
+     )))
+
+(defun wisi-debug-keys ()
+  "Add debug key definitions to `global-map'."
+  (interactive)
+  (define-key global-map "\M-h" 'wisi-show-containing-or-previous-cache)
+  (define-key global-map "\M-i" 'wisi-show-indent)
+  (define-key global-map "\M-j" 'wisi-show-cache)
+  )
+
+(defun wisi-read-parse-action ()
+  "Read a parse action symbol from the minibuffer."
+  (intern-soft (completing-read "parse action (indent): " '(face navigate 
indent) nil t nil nil 'indent)))
+
+(defun wisi-parse-buffer (&optional parse-action begin end)
+  (interactive)
+  (unless parse-action
+    (setq parse-action (wisi-read-parse-action)))
+  (if (use-region-p)
+      (progn
+       (setq begin (region-beginning))
+       (setq end   (region-end)))
+
+    (unless begin (setq begin (point-min)))
+    (unless end (setq end (point-max))))
+
+  (wisi-set-parse-try t parse-action)
+  (wisi-invalidate-cache parse-action begin)
+
+  (cl-ecase parse-action
+    (face
+     (with-silent-modifications
+       (remove-text-properties
+       begin end
+       (list
+        'font-lock-face nil
+        'fontified nil)))
+     (wisi-validate-cache begin end t parse-action)
+     (when (fboundp 'font-lock-ensure) (font-lock-ensure))) ;; emacs < 25
+
+    (navigate
+     (wisi-validate-cache begin end t parse-action))
+
+    (indent
+     (wisi-indent-region begin end))
+    ))
+
+(defun wisi-time (func count &optional report-wait-time)
+  "call FUNC COUNT times, show total time"
+  (interactive "afunction \nncount ")
+
+  (let ((start-time (float-time))
+       (start-gcs gcs-done)
+       (cum-wait-time 0.0)
+        (i 0)
+        diff-time
+       diff-gcs)
+    (while (not (eq (1+ count) (setq i (1+ i))))
+      (save-excursion
+        (funcall func))
+      (when report-wait-time
+       (setq cum-wait-time (+ cum-wait-time 
(wisi-process--parser-total-wait-time wisi--parser)))))
+    (setq diff-time (- (float-time) start-time))
+    (setq diff-gcs (- gcs-done start-gcs))
+    (if report-wait-time
+       (progn
+         (message "Total %f seconds, %d gcs; per iteration %f seconds %d gcs 
%d responses %f wait"
+                  diff-time
+                  diff-gcs
+                  (/ diff-time count)
+                  (/ (float diff-gcs) count)
+                  (wisi-process--parser-response-count wisi--parser)
+                  (/ cum-wait-time count)))
+
+      (message "Total %f seconds, %d gcs; per iteration %f seconds %d gcs"
+              diff-time
+              diff-gcs
+              (/ diff-time count)
+              (/ (float diff-gcs) count))
+      ))
+  nil)
+
+(defun wisi-time-indent-middle-line-cold-cache (count &optional 
report-wait-time)
+  (goto-char (point-min))
+  (forward-line (1- (/ (count-lines (point-min) (point-max)) 2)))
+  (let ((cum-wait-time 0.0))
+    (wisi-time
+     (lambda ()
+       (wisi-set-parse-try t 'indent)
+       (wisi-invalidate-cache 'indent (point-min))
+       (wisi-indent-line)
+       (when (wisi-process--parser-p wisi--parser)
+        (setq cum-wait-time (+ cum-wait-time 
(wisi-process--parser-total-wait-time wisi--parser)))))
+     count
+     report-wait-time)
+    ))
+
+(defun wisi-time-indent-middle-line-warm-cache (count)
+  (wisi-set-parse-try t 'indent)
+  (wisi-invalidate-cache 'indent (point-min))
+  (goto-char (point-min))
+  (forward-line (/ (count-lines (point-min) (point-max)) 2))
+  (wisi-indent-line)
+  (wisi-time #'wisi-indent-line count))
+
+(defun wisi-show-indent ()
+  "Show indent cache for current line."
+  (interactive)
+  (message "%s" (get-text-property (1- (line-beginning-position)) 
'wisi-indent)))
+
+(defun wisi-show-cache ()
+  "Show wisi text properties at point."
+  (interactive)
+  (message "%s:%s:%s:%s"
+          (wisi-get-cache (point))
+          (get-text-property (point) 'face)
+          (get-text-property (point) 'font-lock-face)
+          (get-text-property (point) 'wisi-name)
+          ))
+
+(defun wisi-show-containing-or-previous-cache ()
+  (interactive)
+  (let ((cache (wisi-get-cache (point))))
+    (if cache
+       (message "containing %s" (wisi-goto-containing cache t))
+      (message "previous %s" (wisi-backward-cache)))
+    ))
+
+;;;;; setup
+
+(cl-defun wisi-setup (&key indent-calculate post-indent-fail parser lexer)
+  "Set up a buffer for parsing files with wisi."
+  (when wisi--parser
+    (wisi-kill-parser))
+
+  (setq wisi--parser parser)
+  (setq wisi--lexer lexer)
+  (setq wisi--cached-regions
+       (list
+        (cons 'face nil)
+        (cons 'navigate nil)
+        (cons 'indent nil)))
+
+  (setq wisi--parse-try
+       (list
+        (cons 'face t)
+        (cons 'navigate t)
+        (cons 'indent t)))
+
+  ;; file local variables may have added opentoken, gnatprep
+  (setq wisi-indent-calculate-functions (append 
wisi-indent-calculate-functions indent-calculate))
+  (set (make-local-variable 'indent-line-function) #'wisi-indent-line)
+  (set (make-local-variable 'indent-region-function) #'wisi-indent-region)
+  (set (make-local-variable 'forward-sexp-function) #'wisi-forward-sexp)
+
+  (setq wisi-post-indent-fail-hook post-indent-fail)
+  (setq wisi-indent-failed nil)
+
+  (add-hook 'before-change-functions #'wisi-before-change 'append t)
+  (add-hook 'after-change-functions #'wisi-after-change nil t)
+  (setq wisi--change-end (copy-marker (point-min) t))
+
+  ;; See comments above on syntax-propertize.
+  (when (< emacs-major-version 25) (syntax-propertize (point-max)))
+
+  ;; In Emacs >= 26, ‘run-mode-hooks’ (in the major mode function)
+  ;; runs ‘hack-local-variables’ after ’*-mode-hooks’; we need
+  ;; ‘wisi-post-local-vars’ to run after ‘hack-local-variables’.
+  (add-hook 'hack-local-variables-hook 'wisi-post-local-vars nil t)
+  )
+
+(defun wisi-post-local-vars ()
+  "See wisi-setup."
+  (setq hack-local-variables-hook (delq 'wisi-post-local-vars 
hack-local-variables-hook))
+
+  (unless wisi-disable-face
+    (jit-lock-register #'wisi-fontify-region)))
+
+
+(provide 'wisi)
+;;; wisi.el ends here
diff --git a/wisitoken-bnf-generate.adb b/wisitoken-bnf-generate.adb
index d74b4a5..ca98c75 100644
--- a/wisitoken-bnf-generate.adb
+++ b/wisitoken-bnf-generate.adb
@@ -34,19 +34,18 @@ with WisiToken.BNF.Output_Ada_Common;
 with WisiToken.BNF.Output_Ada_Emacs;
 with WisiToken.BNF.Output_Elisp;
 with WisiToken.BNF.Output_Elisp_Common;
-with WisiToken.Generate.Packrat;
 with WisiToken.Generate.LR.LALR_Generate;
 with WisiToken.Generate.LR.LR1_Generate;
+with WisiToken.Generate.Packrat;
 with WisiToken.Parse.LR.Parser_No_Recover; -- for reading BNF file
 with WisiToken.Productions;
+with WisiToken.Syntax_Trees;
 with WisiToken.Text_IO_Trace;
 with WisiToken_Grammar_Runtime;
 with Wisitoken_Grammar_Actions;
 with Wisitoken_Grammar_Main;
 procedure WisiToken.BNF.Generate
 is
-   use all type Ada.Containers.Count_Type;
-
    procedure Put_Usage
    is
       use Ada.Text_IO;
@@ -54,7 +53,7 @@ is
    begin
       --  verbosity meaning is actually determined by output choice;
       --  they should be consistent with this description.
-      Put_Line (Standard_Error, "version 1.1.0");
+      Put_Line (Standard_Error, "version 1.2.0");
       Put_Line (Standard_Error, "wisi-generate [options] {wisi grammar file}");
       Put_Line (Standard_Error, "Generate source code implementing a parser 
for the grammar.");
       New_Line (Standard_Error);
@@ -111,7 +110,9 @@ is
       Put_Line (Standard_Error, "     1 - add diagnostics to standard out");
       Put_Line (Standard_Error, "     2 - more diagnostics to standard out, 
ignore unused tokens, unknown conflicts");
       Put_Line (Standard_Error, "  --generate ...: override grammar file 
%generate directive");
+      Put_Line (Standard_Error, "  --output_bnf <file_name> : output 
translated BNF source to file_name");
       Put_Line (Standard_Error, "  --suffix <string>; appended to grammar file 
name");
+      Put_Line (Standard_Error, "  --ignore_conflicts; ignore excess/unknown 
conflicts");
       Put_Line (Standard_Error,
                 "  --test_main; generate standalone main program for running 
the generated parser, modify file names");
       Put_Line (Standard_Error, "  --time; output execution time of various 
stages");
@@ -121,12 +122,16 @@ is
    Language_Name         : Ada.Strings.Unbounded.Unbounded_String; -- The 
language the grammar defines
    Output_File_Name_Root : Ada.Strings.Unbounded.Unbounded_String;
    Suffix                : Ada.Strings.Unbounded.Unbounded_String;
+   BNF_File_Name         : Ada.Strings.Unbounded.Unbounded_String;
+   Output_BNF            : Boolean := False;
+   Ignore_Conflicts      : Boolean := False;
    Test_Main             : Boolean := False;
 
    Command_Generate_Set : Generate_Set_Access; -- override grammar file 
declarations
 
    Trace          : aliased WisiToken.Text_IO_Trace.Trace 
(Wisitoken_Grammar_Actions.Descriptor'Access);
    Input_Data     : aliased WisiToken_Grammar_Runtime.User_Data_Type;
+   Elisp_Tokens   : WisiToken.BNF.Tokens;
    Grammar_Parser : WisiToken.Parse.LR.Parser_No_Recover.Parser;
 
    Do_Time : Boolean := False;
@@ -183,6 +188,10 @@ begin
             WisiToken.Trace_Generate := Integer'Value (Argument (Arg_Next));
             Arg_Next  := Arg_Next + 1;
 
+         elsif Argument (Arg_Next) = "--ignore_conflicts" then
+            Ignore_Conflicts := True;
+            Arg_Next         := Arg_Next + 1;
+
          elsif Argument (Arg_Next) = "--generate" then
             Arg_Next  := Arg_Next + 1;
             declare
@@ -196,39 +205,46 @@ begin
                when Constraint_Error =>
                   raise User_Error with "invalid value for 
generator_algorithm: '" & Argument (Arg_Next) & ";";
                end;
-               begin
-                  Tuple.Out_Lang := To_Output_Language (Argument (Arg_Next));
-                  Arg_Next       := Arg_Next + 1;
-               end;
-
-               loop
-                  exit when Done;
-                  declare
-                     Text : constant String := Argument (Arg_Next);
+               if Tuple.Gen_Alg /= None then
                   begin
-                     if Text = "text_rep" then
-                        Tuple.Text_Rep := True;
-                        Arg_Next := Arg_Next + 1;
-
-                     elsif (for some I of Lexer_Image => To_Lower (Text) =  
I.all) then
-                        Tuple.Lexer := To_Lexer (Text);
-                        Arg_Next := Arg_Next + 1;
-
-                     elsif (for some I in Valid_Interface =>
-                              To_Lower (Text) = To_Lower 
(Valid_Interface'Image (I)))
-                     then
-                        Tuple.Interface_Kind := 
WisiToken.BNF.Valid_Interface'Value (Text);
-                        Arg_Next := Arg_Next + 1;
-
-                     else
-                        Done := True;
-                     end if;
+                     Tuple.Out_Lang := To_Output_Language (Argument 
(Arg_Next));
+                     Arg_Next       := Arg_Next + 1;
                   end;
-               end loop;
 
+                  loop
+                     exit when Done;
+                     declare
+                        Text : constant String := Argument (Arg_Next);
+                     begin
+                        if Text = "text_rep" then
+                           Tuple.Text_Rep := True;
+                           Arg_Next := Arg_Next + 1;
+
+                        elsif (for some I of Lexer_Image => To_Lower (Text) =  
I.all) then
+                           Tuple.Lexer := To_Lexer (Text);
+                           Arg_Next := Arg_Next + 1;
+
+                        elsif (for some I in Valid_Interface =>
+                                 To_Lower (Text) = To_Lower 
(Valid_Interface'Image (I)))
+                        then
+                           Tuple.Interface_Kind := 
WisiToken.BNF.Valid_Interface'Value (Text);
+                           Arg_Next := Arg_Next + 1;
+
+                        else
+                           Done := True;
+                        end if;
+                     end;
+                  end loop;
+               end if;
                Add (Command_Generate_Set, Tuple);
             end;
 
+         elsif Argument (Arg_Next) = "--output_bnf" then
+            Output_BNF    := True;
+            Arg_Next      := Arg_Next + 1;
+            BNF_File_Name := +Argument (Arg_Next);
+            Arg_Next      := Arg_Next + 1;
+
          elsif Argument (Arg_Next) = "--suffix" then
             Arg_Next := Arg_Next + 1;
             Suffix   := +Argument (Arg_Next);
@@ -286,28 +302,83 @@ begin
       --  cache results in those cases; they only happen in test grammars,
       --  which are small.
 
-      procedure Parse_Check (Lexer : in Lexer_Type; Parser : in 
Generate_Algorithm)
-      is begin
+      procedure Parse_Check
+        (Lexer  : in Lexer_Type;
+         Parser : in Generate_Algorithm;
+         Phase  : in WisiToken_Grammar_Runtime.Action_Phase)
+      is
+         use all type Ada.Containers.Count_Type;
+         use all type WisiToken_Grammar_Runtime.Action_Phase;
+         use all type WisiToken_Grammar_Runtime.Meta_Syntax;
+      begin
          Input_Data.User_Parser := Parser;
          Input_Data.User_Lexer  := Lexer;
          --  Specifying the parser and lexer can change the parsed grammar, due
          --  to %if {parser | lexer}.
 
-         Input_Data.Reset;
+         Input_Data.Reset; -- only resets Other data
+
+         Input_Data.Phase := Phase;
          Grammar_Parser.Execute_Actions;
-         --  Ensures Input_Data.User_{Parser|Lexer} are set if needed.
 
-         if Input_Data.Rule_Count = 0 or Input_Data.Tokens.Rules.Length = 0 
then
-            raise WisiToken.Grammar_Error with "no rules";
-         end if;
+         case Phase is
+         when Meta =>
+            case Input_Data.Meta_Syntax is
+            when Unknown =>
+               Input_Data.Meta_Syntax := BNF_Syntax;
+
+            when BNF_Syntax =>
+               null;
+
+            when EBNF_Syntax =>
+               declare
+                  Tree  : WisiToken.Syntax_Trees.Tree renames 
Grammar_Parser.Parsers.First_State_Ref.Tree;
+               begin
+                  if Trace_Generate > Outline then
+                     Ada.Text_IO.Put_Line ("Translate EBNF tree to BNF");
+                  end if;
+
+                  if Trace_Generate > Detail then
+                     Ada.Text_IO.Put_Line ("EBNF tree:");
+                     Tree.Print_Tree (Wisitoken_Grammar_Actions.Descriptor);
+                     Ada.Text_IO.New_Line;
+                  end if;
+
+                  WisiToken_Grammar_Runtime.Translate_EBNF_To_BNF (Tree, 
Input_Data);
+
+                  if Trace_Generate > Detail then
+                     Ada.Text_IO.New_Line;
+                     Ada.Text_IO.Put_Line ("BNF tree:");
+                     Tree.Print_Tree (Wisitoken_Grammar_Actions.Descriptor);
+                  end if;
+
+                  if Output_BNF then
+                     WisiToken_Grammar_Runtime.Print_Source (-BNF_File_Name, 
Tree, Input_Data);
+                  end if;
+
+                  if WisiToken.Generate.Error then
+                     raise WisiToken.Grammar_Error with "errors during 
translating EBNF to BNF: aborting";
+                  end if;
+               end;
+            end case;
 
+         when Other =>
+            if Input_Data.Rule_Count = 0 or Input_Data.Tokens.Rules.Length = 0 
then
+               raise WisiToken.Grammar_Error with "no rules";
+            end if;
+         end case;
+      exception
+      when E : WisiToken.Syntax_Error | WisiToken.Parse_Error =>
+         Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, 
Ada.Exceptions.Exception_Message (E));
+         Grammar_Parser.Put_Errors;
+         raise;
       end Parse_Check;
 
    begin
-      if Command_Generate_Set = null then
-         --  Get the first quad from the input file
-         Parse_Check (None, None);
+      --  Get the the input file quads, translate EBNF
+      Parse_Check (None, None, WisiToken_Grammar_Runtime.Meta);
 
+      if Command_Generate_Set = null then
          if Input_Data.Generate_Set = null then
             raise User_Error with
               WisiToken.Generate.Error_Message
@@ -315,10 +386,7 @@ begin
                  "generate algorithm, output_language, lexer, interface not 
specified");
          end if;
 
-         --  Input_Data.Generate_Set will be free'd and regenerated if
-         --  Parse_Check is called, but the content won't change. So make a
-         --  copy.
-         Generate_Set := new 
WisiToken.BNF.Generate_Set'(Input_Data.Generate_Set.all);
+         Generate_Set := Input_Data.Generate_Set;
       else
          Generate_Set := Command_Generate_Set;
       end if;
@@ -326,11 +394,10 @@ begin
       Multiple_Tuples := Generate_Set'Length > 1;
 
       for Tuple of Generate_Set.all loop
-
-         Input_Data.User_Parser := Tuple.Gen_Alg;
-         Input_Data.User_Lexer  := Tuple.Lexer;
-
-         Parse_Check (Input_Data.User_Lexer, Input_Data.User_Parser);
+         Parse_Check
+           (Lexer  => Tuple.Lexer,
+            Parser => Tuple.Gen_Alg,
+            Phase  => WisiToken_Grammar_Runtime.Other);
 
          declare
             use Ada.Real_Time;
@@ -339,21 +406,33 @@ begin
             Time_End   : Time;
 
             Generate_Data : aliased WisiToken.BNF.Generate_Utils.Generate_Data 
:=
-              WisiToken.BNF.Generate_Utils.Initialize (Input_Data);
+              WisiToken.BNF.Generate_Utils.Initialize (Input_Data, 
Ignore_Conflicts);
 
             Packrat_Data : WisiToken.Generate.Packrat.Data
               (Generate_Data.Descriptor.First_Terminal, 
Generate_Data.Descriptor.First_Nonterminal,
                Generate_Data.Descriptor.Last_Nonterminal);
+
+            Do_Parse_Table_File : constant Boolean := WisiToken.Trace_Generate 
= 0 and
+              Tuple.Gen_Alg in LALR .. Packrat_Proc;
          begin
             if not Lexer_Done (Input_Data.User_Lexer) then
                Lexer_Done (Input_Data.User_Lexer) := True;
-               if Input_Data.User_Lexer = re2c_Lexer then
+               case Input_Data.User_Lexer is
+               when re2c_Lexer =>
                   WisiToken.BNF.Output_Ada_Common.Create_re2c
                     (Input_Data, Tuple, Generate_Data, -Output_File_Name_Root);
-               end if;
+                  if Tuple.Out_Lang = Ada_Emacs_Lang and 
Elisp_Tokens.Keywords.Is_Empty then
+                     --  elisp code needs keywords for font-lock.
+                     Elisp_Tokens.Keywords := Input_Data.Tokens.Keywords;
+                  end if;
+               when Elisp_Lexer =>
+                  Elisp_Tokens := Input_Data.Tokens;
+               when others =>
+                  null;
+               end case;
             end if;
 
-            if WisiToken.Trace_Generate = 0 and Tuple.Gen_Alg /= External then
+            if Do_Parse_Table_File then
                Create
                  (Parse_Table_File, Out_File,
                   -Output_File_Name_Root & "_" & To_Lower 
(Generate_Algorithm'Image (Tuple.Gen_Alg)) &
@@ -365,6 +444,10 @@ begin
             end if;
 
             case Tuple.Gen_Alg is
+            when None =>
+               --  Just translate EBNF to BNF, done in Parse_Check
+               null;
+
             when LALR =>
 
                Time_Start := Clock;
@@ -374,9 +457,11 @@ begin
                   Generate_Data.Descriptor.all,
                   Generate_Utils.To_Conflicts
                     (Generate_Data, Input_Data.Conflicts, 
Input_Data.Grammar_Lexer.File_Name),
-                  Generate_Utils.To_McKenzie_Param
-                    (Generate_Data, Input_Data.McKenzie_Recover, 
Input_Data.Grammar_Lexer.File_Name),
-                  Put_Parse_Table => True);
+                  Generate_Utils.To_McKenzie_Param (Generate_Data, 
Input_Data.McKenzie_Recover),
+                  Put_Parse_Table   => True,
+                  Include_Extra     => Test_Main,
+                  Ignore_Conflicts  => Ignore_Conflicts,
+                  Partial_Recursion => 
Input_Data.Language_Params.Partial_Recursion);
 
                if Do_Time then
                   Time_End := Clock;
@@ -400,9 +485,11 @@ begin
                   Generate_Data.Descriptor.all,
                   Generate_Utils.To_Conflicts
                     (Generate_Data, Input_Data.Conflicts, 
Input_Data.Grammar_Lexer.File_Name),
-                  Generate_Utils.To_McKenzie_Param
-                    (Generate_Data, Input_Data.McKenzie_Recover, 
Input_Data.Grammar_Lexer.File_Name),
-                  Put_Parse_Table => True);
+                  Generate_Utils.To_McKenzie_Param (Generate_Data, 
Input_Data.McKenzie_Recover),
+                  Put_Parse_Table   => True,
+                  Include_Extra     => Test_Main,
+                  Ignore_Conflicts  => Ignore_Conflicts,
+                  Partial_Recursion => 
Input_Data.Language_Params.Partial_Recursion);
 
                if Do_Time then
                   Time_End := Clock;
@@ -438,7 +525,7 @@ begin
                null;
             end case;
 
-            if WisiToken.Trace_Generate = 0 and Tuple.Gen_Alg /= External then
+            if Do_Parse_Table_File then
                Set_Output (Standard_Output);
                Close (Parse_Table_File);
             end if;
@@ -455,27 +542,33 @@ begin
                      -Output_File_Name_Root & "_" &
                        To_Lower (Generate_Algorithm_Image (Tuple.Gen_Alg).all) 
&
                        "_parse_table.txt",
-                    Generate_Data.Action_Names.all, 
Generate_Data.Check_Names.all);
+                     Generate_Data.Action_Names.all, 
Generate_Data.Check_Names.all);
                end if;
 
             when others =>
                null;
             end case;
 
-            case Tuple.Out_Lang is
-            when Ada_Lang =>
-               WisiToken.BNF.Output_Ada
-                 (Input_Data, -Output_File_Name_Root, Generate_Data, 
Packrat_Data, Tuple, Test_Main, Multiple_Tuples);
+            if Tuple.Gen_Alg /= None then
+               case Tuple.Out_Lang is
+               when Ada_Lang =>
+                  WisiToken.BNF.Output_Ada
+                    (Input_Data, -Output_File_Name_Root, Generate_Data, 
Packrat_Data, Tuple, Test_Main,
+                     Multiple_Tuples);
 
-            when Ada_Emacs_Lang =>
-               WisiToken.BNF.Output_Ada_Emacs
-                 (Input_Data, -Output_File_Name_Root, Generate_Data, 
Packrat_Data, Tuple, Test_Main, Multiple_Tuples,
-                  -Language_Name);
+               when Ada_Emacs_Lang =>
+                  WisiToken.BNF.Output_Ada_Emacs
+                    (Input_Data, Elisp_Tokens, -Output_File_Name_Root, 
Generate_Data, Packrat_Data, Tuple,
+                     Test_Main, Multiple_Tuples, -Language_Name);
 
-            when Elisp_Lang =>
-               WisiToken.BNF.Output_Elisp (Input_Data, -Output_File_Name_Root, 
Generate_Data, Packrat_Data, Tuple);
+               when Elisp_Lang =>
+                  WisiToken.BNF.Output_Elisp (Input_Data, 
-Output_File_Name_Root, Generate_Data, Packrat_Data, Tuple);
 
-            end case;
+               end case;
+               if WisiToken.Generate.Error then
+                  raise WisiToken.Grammar_Error with "errors: aborting";
+               end if;
+            end if;
          end;
       end loop;
    end;
diff --git a/wisitoken-bnf-generate_grammar.adb 
b/wisitoken-bnf-generate_grammar.adb
index 2264dcf..165f00f 100644
--- a/wisitoken-bnf-generate_grammar.adb
+++ b/wisitoken-bnf-generate_grammar.adb
@@ -1,86 +1,86 @@
---  Abstract :
---
---  Output Ada source code to recreate Grammar.
---
---  Copyright (C) 2018 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with Ada.Text_IO; use Ada.Text_IO;
-with WisiToken.Generate;
-with WisiToken.Productions;
-procedure WisiToken.BNF.Generate_Grammar
-  (Grammar      : in WisiToken.Productions.Prod_Arrays.Vector;
-   Action_Names : in WisiToken.Names_Array_Array)
-is
-   use all type Ada.Containers.Count_Type;
-   use Ada.Strings.Unbounded;
-   use WisiToken.Generate;
-   use WisiToken.Productions;
-   Text : Unbounded_String;
-   Need_Comma : Boolean := False;
-begin
-   Indent_Line ("Grammar.Set_First (" & Trimmed_Image (Grammar.First_Index) & 
");");
-   Indent_Line ("Grammar.Set_Last (" & Trimmed_Image (Grammar.Last_Index) & 
");");
-
-   for Prod of Grammar loop
-      Indent_Line ("declare");
-      Indent_Line ("   Prod : Instance;");
-      Indent_Line ("begin");
-      Indent := Indent + 3;
-      Indent_Line ("Prod.LHS := " & Trimmed_Image (Prod.LHS) & ";");
-      Indent_Line ("Prod.RHSs.Set_First (0);");
-      Indent_Line ("Prod.RHSs.Set_Last (" & Trimmed_Image 
(Prod.RHSs.Last_Index) & ");");
-      for RHS_Index in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
-         declare
-            RHS : Right_Hand_Side renames Prod.RHSs (RHS_Index);
-         begin
-            Indent_Line ("declare");
-            Indent_Line ("   RHS : Right_Hand_Side;");
-            Indent_Line ("begin");
-            Indent := Indent + 3;
-            if RHS.Tokens.Length > 0 then
-               Indent_Line ("RHS.Tokens.Set_First (1);");
-               Indent_Line ("RHS.Tokens.Set_Last (" & Trimmed_Image (Prod.RHSs 
(RHS_Index).Tokens.Last_Index) & ");");
-
-               if RHS.Tokens.Length = 1 then
-                  Indent_Line ("To_Vector ((1 => " & Trimmed_Image (RHS.Tokens 
(1)) & "), RHS.Tokens);");
-               else
-                  Need_Comma := False;
-                  Text := +"To_Vector ((";
-                  for ID of RHS.Tokens  loop
-                     if Need_Comma then
-                        Text := Text & ", ";
-                     else
-                        Need_Comma := True;
-                     end if;
-                     Text := Text & Trimmed_Image (ID);
-                  end loop;
-                  Text := Text & "), RHS.Tokens);";
-                  Indent_Wrap (-Text);
-               end if;
-            end if;
-            if Action_Names (Prod.LHS) /= null and then Action_Names 
(Prod.LHS)(RHS_Index) /= null then
-               Indent_Line ("RHS.Action     := " & Action_Names 
(Prod.LHS)(RHS_Index).all & "'Access;");
-            end if;
-            Indent_Line ("Prod.RHSs (" & Trimmed_Image (RHS_Index) & ") := 
RHS;");
-            Indent := Indent - 3;
-            Indent_Line ("end;");
-         end;
-      end loop;
-      Indent_Line ("Grammar (" & Trimmed_Image (Prod.LHS) & ") := Prod;");
-      Indent := Indent - 3;
-      Indent_Line ("end;");
-   end loop;
-end WisiToken.BNF.Generate_Grammar;
+--  Abstract :
+--
+--  Output Ada source code to recreate Grammar.
+--
+--  Copyright (C) 2018 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Text_IO; use Ada.Text_IO;
+with WisiToken.Generate;
+with WisiToken.Productions;
+procedure WisiToken.BNF.Generate_Grammar
+  (Grammar      : in WisiToken.Productions.Prod_Arrays.Vector;
+   Action_Names : in WisiToken.Names_Array_Array)
+is
+   use all type Ada.Containers.Count_Type;
+   use Ada.Strings.Unbounded;
+   use WisiToken.Generate;
+   use WisiToken.Productions;
+   Text : Unbounded_String;
+   Need_Comma : Boolean := False;
+begin
+   Indent_Line ("Grammar.Set_First (" & Trimmed_Image (Grammar.First_Index) & 
");");
+   Indent_Line ("Grammar.Set_Last (" & Trimmed_Image (Grammar.Last_Index) & 
");");
+
+   for Prod of Grammar loop
+      Indent_Line ("declare");
+      Indent_Line ("   Prod : Instance;");
+      Indent_Line ("begin");
+      Indent := Indent + 3;
+      Indent_Line ("Prod.LHS := " & Trimmed_Image (Prod.LHS) & ";");
+      Indent_Line ("Prod.RHSs.Set_First (0);");
+      Indent_Line ("Prod.RHSs.Set_Last (" & Trimmed_Image 
(Prod.RHSs.Last_Index) & ");");
+      for RHS_Index in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
+         declare
+            RHS : Right_Hand_Side renames Prod.RHSs (RHS_Index);
+         begin
+            Indent_Line ("declare");
+            Indent_Line ("   RHS : Right_Hand_Side;");
+            Indent_Line ("begin");
+            Indent := Indent + 3;
+            if RHS.Tokens.Length > 0 then
+               Indent_Line ("RHS.Tokens.Set_First (1);");
+               Indent_Line ("RHS.Tokens.Set_Last (" & Trimmed_Image (Prod.RHSs 
(RHS_Index).Tokens.Last_Index) & ");");
+
+               if RHS.Tokens.Length = 1 then
+                  Indent_Line ("To_Vector ((1 => " & Trimmed_Image (RHS.Tokens 
(1)) & "), RHS.Tokens);");
+               else
+                  Need_Comma := False;
+                  Text := +"To_Vector ((";
+                  for ID of RHS.Tokens  loop
+                     if Need_Comma then
+                        Text := Text & ", ";
+                     else
+                        Need_Comma := True;
+                     end if;
+                     Text := Text & Trimmed_Image (ID);
+                  end loop;
+                  Text := Text & "), RHS.Tokens);";
+                  Indent_Wrap (-Text);
+               end if;
+            end if;
+            if Action_Names (Prod.LHS) /= null and then Action_Names 
(Prod.LHS)(RHS_Index) /= null then
+               Indent_Line ("RHS.Action     := " & Action_Names 
(Prod.LHS)(RHS_Index).all & "'Access;");
+            end if;
+            Indent_Line ("Prod.RHSs (" & Trimmed_Image (RHS_Index) & ") := 
RHS;");
+            Indent := Indent - 3;
+            Indent_Line ("end;");
+         end;
+      end loop;
+      Indent_Line ("Grammar (" & Trimmed_Image (Prod.LHS) & ") := Prod;");
+      Indent := Indent - 3;
+      Indent_Line ("end;");
+   end loop;
+end WisiToken.BNF.Generate_Grammar;
diff --git a/wisitoken-bnf-generate_utils.adb b/wisitoken-bnf-generate_utils.adb
index 8e52ca4..0f84547 100644
--- a/wisitoken-bnf-generate_utils.adb
+++ b/wisitoken-bnf-generate_utils.adb
@@ -2,7 +2,7 @@
 --
 --  see spec
 --
---  Copyright (C) 2014, 2015, 2017, 2018  All Rights Reserved.
+--  Copyright (C) 2014, 2015, 2017 - 2019  All Rights Reserved.
 --
 --  This program is free software; you can redistribute it and/or
 --  modify it under terms of the GNU General Public License as
@@ -47,7 +47,7 @@ package body WisiToken.BNF.Generate_Utils is
 
    function Name_1 (Cursor : in Token_Cursor) return String
    is begin
-      --   This function is used to compute LR1_descriptor.Image
+      --   This function is used to compute Descriptor.Image
       case Cursor.Kind is
       when Non_Grammar_Kind =>
          declare
@@ -128,7 +128,8 @@ package body WisiToken.BNF.Generate_Utils is
       when Not_Found =>
          Put_Error
            (Error_Message
-              (Source_File_Name, 1, "start token '" & (Start_Token) & "' not 
found; need %start?"));
+              (Source_File_Name, 1,
+               "start token '" & (Start_Token) & "' not found; need %start?"));
       end;
 
       for Rule of Data.Tokens.Rules loop
@@ -138,8 +139,8 @@ package body WisiToken.BNF.Generate_Utils is
             LHS              : Token_ID; -- not initialized for exception 
handler
             Action_Names     : Names_Array (0 .. Integer 
(Rule.Right_Hand_Sides.Length) - 1);
             Action_All_Empty : Boolean := True;
-            Check_Names     : Names_Array (0 .. Integer 
(Rule.Right_Hand_Sides.Length) - 1);
-            Check_All_Empty : Boolean := True;
+            Check_Names      : Names_Array (0 .. Integer 
(Rule.Right_Hand_Sides.Length) - 1);
+            Check_All_Empty  : Boolean := True;
          begin
             LHS := Find_Token_ID (Data, -Rule.Left_Hand_Side);
 
@@ -161,7 +162,7 @@ package body WisiToken.BNF.Generate_Utils is
                      Tokens.Set_First (I);
                      Tokens.Set_Last (Integer (Right_Hand_Side.Tokens.Length));
                      for Token of Right_Hand_Side.Tokens loop
-                        Tokens (I) := Find_Token_ID (Data, Token);
+                        Tokens (I) := Find_Token_ID (Data, -Token.Identifier);
                         I := I + 1;
                      end loop;
                   end if;
@@ -211,7 +212,10 @@ package body WisiToken.BNF.Generate_Utils is
    ----------
    --  Public subprograms, declaration order
 
-   function Initialize (Input_Data : aliased in 
WisiToken_Grammar_Runtime.User_Data_Type) return Generate_Data
+   function Initialize
+     (Input_Data       : aliased in WisiToken_Grammar_Runtime.User_Data_Type;
+      Ignore_Conflicts :         in Boolean := False)
+     return Generate_Data
    is
       EOI_ID : constant Token_ID := Token_ID
         (Count (Input_Data.Tokens.Non_Grammar) + Count 
(Input_Data.Tokens.Tokens)) + Token_ID
@@ -235,21 +239,16 @@ package body WisiToken.BNF.Generate_Utils is
       do
          Result.Descriptor.Case_Insensitive := 
Input_Data.Language_Params.Case_Insensitive;
          Result.Descriptor.New_Line_ID      := Find_Kind (Result, "new-line");
-         Result.Descriptor.Comment_ID       := Find_Kind (Result, "comment");
-         Result.Descriptor.Left_Paren_ID    := Find_Kind (Result, 
"left-paren");
-         Result.Descriptor.Right_Paren_ID   := Find_Kind (Result, 
"right-paren");
          Result.Descriptor.String_1_ID      := Find_Kind (Result, 
"string-single");
          Result.Descriptor.String_2_ID      := Find_Kind (Result, 
"string-double");
 
-         Result.Descriptor.Embedded_Quote_Escape_Doubled := 
Input_Data.Language_Params.Embedded_Quote_Escape_Doubled;
-
          --  Image set in loop below, which also updates these widths.
          Result.Descriptor.Terminal_Image_Width := 0;
          Result.Descriptor.Image_Width          := 0;
 
          Result.Descriptor.Last_Lookahead       :=
            (case (Input_Data.User_Parser) is
-            when None                                  => raise 
SAL.Programmer_Error,
+            when None                                  => Invalid_Token_ID,
             when LR1                                   => 
Result.Descriptor.Last_Terminal,
             when LALR                                  => 
Result.Descriptor.First_Nonterminal,
             when Packrat_Generate_Algorithm | External => Invalid_Token_ID);
@@ -271,6 +270,7 @@ package body WisiToken.BNF.Generate_Utils is
          end loop;
 
          To_Grammar (Result, Input_Data.Grammar_Lexer.File_Name, 
-Input_Data.Language_Params.Start_Token);
+         Result.Ignore_Conflicts := Ignore_Conflicts;
       end return;
    end Initialize;
 
@@ -416,6 +416,7 @@ package body WisiToken.BNF.Generate_Utils is
          end if;
 
       when Terminals_Others =>
+
          Cursor :=
            (Data        => Cursor.Data,
             Kind        => EOI,
@@ -448,6 +449,7 @@ package body WisiToken.BNF.Generate_Utils is
          end if;
 
       when WisiToken_Accept =>
+
          Cursor :=
            (Data        => Cursor.Data,
             Kind        => Nonterminal,
@@ -462,7 +464,6 @@ package body WisiToken.BNF.Generate_Utils is
 
       when Nonterminal =>
          Cursor.Kind := Done;
-
          return True;
 
       when Done =>
@@ -574,10 +575,15 @@ package body WisiToken.BNF.Generate_Utils is
 
       when Nonterminal =>
          Rule_Lists.Next (Cursor.Nonterminal);
-         if not Rule_Lists.Has_Element (Cursor.Nonterminal) then
-            Cursor.Kind := Done;
+         if Rule_Lists.Has_Element (Cursor.Nonterminal) then
+            return;
          end if;
 
+         loop
+            exit when Next_Kind_Internal (Cursor, Nonterminals);
+         end loop;
+         return;
+
       when Done =>
          null;
       end case;
@@ -672,14 +678,9 @@ package body WisiToken.BNF.Generate_Utils is
      return WisiToken.Generate.LR.Conflict_Lists.List
    is
       use WisiToken.Generate.LR;
-      use all type WisiToken.Parse.LR.Parse_Action_Verbs;
       Result   : WisiToken.Generate.LR.Conflict_Lists.List;
       Conflict : WisiToken.Generate.LR.Conflict;
    begin
-      Data.Accept_Reduce_Conflict_Count := 0;
-      Data.Shift_Reduce_Conflict_Count  := 0;
-      Data.Reduce_Reduce_Conflict_Count := 0;
-
       for Item of Conflicts loop
          begin
             Conflict :=
@@ -690,21 +691,14 @@ package body WisiToken.BNF.Generate_Utils is
                -1,
                Find_Token_ID (Data, -Item.On));
 
-            case Conflict.Action_A is
-            when Shift =>
-               Data.Shift_Reduce_Conflict_Count := 
Data.Shift_Reduce_Conflict_Count + 1;
-            when Reduce =>
-               Data.Reduce_Reduce_Conflict_Count := 
Data.Reduce_Reduce_Conflict_Count + 1;
-            when Accept_It =>
-               Data.Accept_Reduce_Conflict_Count := 
Data.Reduce_Reduce_Conflict_Count + 1;
-            end case;
-
             Result.Append (Conflict);
          exception
          when E : Not_Found =>
-            Put_Error
-              (Error_Message
-                 (Source_File_Name, Item.Source_Line, 
Ada.Exceptions.Exception_Message (E)));
+            if not Data.Ignore_Conflicts then
+               Put_Error
+                 (Error_Message
+                    (Source_File_Name, Item.Source_Line, 
Ada.Exceptions.Exception_Message (E)));
+            end if;
          end;
       end loop;
       return Result;
@@ -724,9 +718,8 @@ package body WisiToken.BNF.Generate_Utils is
    end To_Nonterminal_ID_Set;
 
    function To_McKenzie_Param
-     (Data             : aliased in Generate_Data;
-      Item             :         in McKenzie_Recover_Param_Type;
-      Source_File_Name :         in String)
+     (Data : aliased in Generate_Data;
+      Item :         in McKenzie_Recover_Param_Type)
      return WisiToken.Parse.LR.McKenzie_Param_Type
    is
       use Ada.Strings.Unbounded;
@@ -738,28 +731,21 @@ package body WisiToken.BNF.Generate_Utils is
          Data.Descriptor.Last_Terminal,
          Data.Descriptor.First_Nonterminal,
          Data.Descriptor.Last_Nonterminal,
-         Insert            => (others => Item.Default_Insert),
-         Delete            => (others => Item.Default_Delete_Terminal),
-         Push_Back         => (others => Item.Default_Push_Back),
-         Ignore_Check_Fail => Item.Ignore_Check_Fail,
-         Task_Count        => 0,
-         Cost_Limit        => Item.Cost_Limit,
-         Check_Limit       => Item.Check_Limit,
-         Check_Delta_Limit => Item.Check_Delta_Limit,
-         Enqueue_Limit     => Item.Enqueue_Limit);
-
-      ID : Token_ID;
+         Insert                      => (others => Item.Default_Insert),
+         Delete                      => (others => 
Item.Default_Delete_Terminal),
+         Push_Back                   => (others => Item.Default_Push_Back),
+         Undo_Reduce                 => (others => Item.Default_Push_Back), -- 
no separate default for undo_reduce
+         Minimal_Complete_Cost_Delta => Item.Minimal_Complete_Cost_Delta,
+         Fast_Forward                => Item.Fast_Forward,
+         Matching_Begin              => Item.Matching_Begin,
+         Ignore_Check_Fail           => Item.Ignore_Check_Fail,
+         Task_Count                  => 0,
+         Check_Limit                 => Item.Check_Limit,
+         Check_Delta_Limit           => Item.Check_Delta_Limit,
+         Enqueue_Limit               => Item.Enqueue_Limit);
    begin
       for Pair of Item.Delete loop
-         ID := Find_Token_ID (Data, -Pair.Name);
-         if ID in Result.Delete'Range then
-            Result.Delete (ID) := Natural'Value (-Pair.Value);
-         else
-            Put_Error
-              (Error_Message
-                 (Source_File_Name, Item.Source_Line, "delete cost is only 
valid for terminals (" &
-                    WisiToken.Image (ID, Data.Descriptor.all) & ")"));
-         end if;
+         Result.Delete (Find_Token_ID (Data, -Pair.Name)) := Natural'Value 
(-Pair.Value);
       end loop;
       for Pair of Item.Insert loop
          Result.Insert (Find_Token_ID (Data, -Pair.Name)) := Natural'Value 
(-Pair.Value);
@@ -767,6 +753,9 @@ package body WisiToken.BNF.Generate_Utils is
       for Pair of Item.Push_Back loop
          Result.Push_Back (Find_Token_ID (Data, -Pair.Name)) := Natural'Value 
(-Pair.Value);
       end loop;
+      for Pair of Item.Undo_Reduce loop
+         Result.Undo_Reduce (Find_Token_ID (Data, -Pair.Name)) := 
Natural'Value (-Pair.Value);
+      end loop;
 
       return Result;
    end To_McKenzie_Param;
@@ -793,10 +782,6 @@ package body WisiToken.BNF.Generate_Utils is
            Integer'Image (Input_Data.Check_Count) & " checks," &
            WisiToken.State_Index'Image (Generate_Data.Parser_State_Count) & " 
states," &
            Integer'Image (Generate_Data.Table_Actions_Count) & " parse 
actions");
-      Put_Line
-        (Integer'Image (Generate_Data.Accept_Reduce_Conflict_Count) & " 
accept/reduce conflicts," &
-           Integer'Image (Generate_Data.Shift_Reduce_Conflict_Count) & " 
shift/reduce conflicts," &
-           Integer'Image (Generate_Data.Reduce_Reduce_Conflict_Count) & " 
reduce/reduce conflicts");
    end Put_Stats;
 
    function Actions_Length (State : in Parse.LR.Parse_State) return Integer
diff --git a/wisitoken-bnf-generate_utils.ads b/wisitoken-bnf-generate_utils.ads
index 7720e21..bcf599f 100644
--- a/wisitoken-bnf-generate_utils.ads
+++ b/wisitoken-bnf-generate_utils.ads
@@ -3,7 +3,7 @@
 --  Utilities for translating input file structures to WisiToken
 --  structures needed for LALR.Generate.
 --
---  Copyright (C) 2014, 2015, 2017, 2018 Free Software Foundation, Inc.
+--  Copyright (C) 2014, 2015, 2017 - 2019 Free Software Foundation, Inc.
 --
 --  The WisiToken package is free software; you can redistribute it
 --  and/or modify it under terms of the GNU General Public License as
@@ -35,11 +35,11 @@ package WisiToken.BNF.Generate_Utils is
 
    type Generate_Data is limited record
       Tokens     : access constant WisiToken.BNF.Tokens;
-      Descriptor : access WisiToken.Descriptor;
+      Descriptor : WisiToken.Descriptor_Access;
       Grammar    : WisiToken.Productions.Prod_Arrays.Vector;
 
-      Action_Names : access Names_Array_Array;
-      Check_Names  : access Names_Array_Array;
+      Action_Names : Names_Array_Array_Access;
+      Check_Names  : Names_Array_Array_Access;
       --  Names of subprograms for each grammar semantic action and check;
       --  non-null only if there is an action or check in the grammar.
 
@@ -49,16 +49,17 @@ package WisiToken.BNF.Generate_Utils is
       --  The following fields are LR specific; so far, it's not worth
       --  splitting them out.
 
-      Conflicts                    : WisiToken.Generate.LR.Conflict_Lists.List;
-      LR_Parse_Table               : WisiToken.Parse.LR.Parse_Table_Ptr;
-      Table_Actions_Count          : Integer                       := -1; -- 
parse, not user, actions
-      Parser_State_Count           : WisiToken.Unknown_State_Index := 0;
-      Accept_Reduce_Conflict_Count : Integer                       := 0;
-      Shift_Reduce_Conflict_Count  : Integer                       := 0;
-      Reduce_Reduce_Conflict_Count : Integer                       := 0;
+      Ignore_Conflicts    : Boolean                       := False;
+      Conflicts           : WisiToken.Generate.LR.Conflict_Lists.List;
+      LR_Parse_Table      : WisiToken.Parse.LR.Parse_Table_Ptr;
+      Table_Actions_Count : Integer                       := -1; -- parse, not 
user, actions
+      Parser_State_Count  : WisiToken.Unknown_State_Index := 0;
    end record;
 
-   function Initialize (Input_Data : aliased in 
WisiToken_Grammar_Runtime.User_Data_Type) return Generate_Data;
+   function Initialize
+     (Input_Data       : aliased in WisiToken_Grammar_Runtime.User_Data_Type;
+      Ignore_Conflicts :         in Boolean := False)
+     return Generate_Data;
 
    function Find_Token_ID (Data : aliased in Generate_Data; Token : in String) 
return Token_ID;
 
@@ -129,7 +130,7 @@ package WisiToken.BNF.Generate_Utils is
    --  Return the token value from the .wy file:
    --  Keywords: Keywords (i).value
    --  Tokens  : Tokens (i).Tokens (j).Value
-   --  Rules   : "" - they have no Value
+   --  Rules   : empty string (they have no Value)
 
    function To_Conflicts
      (Data             : aliased in out Generate_Data;
@@ -144,9 +145,8 @@ package WisiToken.BNF.Generate_Utils is
      return Token_ID_Set;
 
    function To_McKenzie_Param
-     (Data             : aliased in Generate_Data;
-      Item             :         in McKenzie_Recover_Param_Type;
-      Source_File_Name :         in String)
+     (Data : aliased in Generate_Data;
+      Item :         in McKenzie_Recover_Param_Type)
      return WisiToken.Parse.LR.McKenzie_Param_Type;
 
    procedure Count_Actions (Data : in out Generate_Utils.Generate_Data);
diff --git a/wisitoken-bnf-output_ada.adb b/wisitoken-bnf-output_ada.adb
index d27c602..944e216 100644
--- a/wisitoken-bnf-output_ada.adb
+++ b/wisitoken-bnf-output_ada.adb
@@ -58,14 +58,15 @@ is
    procedure Create_Ada_Actions_Body
      (Action_Names : not null access WisiToken.Names_Array_Array;
       Check_Names  : not null access WisiToken.Names_Array_Array;
+      Label_Count  : in              Ada.Containers.Count_Type;
       Package_Name : in              String)
    is
+      use all type Ada.Containers.Count_Type;
       use GNAT.Regexp;
       use Generate_Utils;
       use WisiToken.Generate;
 
       File_Name : constant String := Output_File_Name_Root & "_actions.adb";
-      --  No generate_algorithm when Test_Main; the generated actions file is 
independent of that.
 
       User_Data_Regexp : constant Regexp := Compile (Symbol_Regexp 
("User_Data"), Case_Sensitive => False);
       Tree_Regexp      : constant Regexp := Compile (Symbol_Regexp ("Tree"), 
Case_Sensitive      => False);
@@ -82,6 +83,10 @@ is
       Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Actions_Body_Context));
       New_Line;
 
+      if Label_Count > 0 then
+         Put_Line ("with SAL;");
+      end if;
+
       Put_Line ("package body " & Package_Name & " is");
       Indent := Indent + 3;
       New_Line;
@@ -111,6 +116,42 @@ is
                     Slice (Action, 1, 5) = "wisi-");
             end Is_Elisp;
 
+            procedure Put_Labels (RHS : in RHS_Type; Line : in String)
+            is
+               Output : array (Rule.Labels.First_Index .. 
Rule.Labels.Last_Index) of Boolean := (others => False);
+
+               procedure Update_Output (Label : in String)
+               is begin
+                  for I in Rule.Labels.First_Index .. Rule.Labels.Last_Index 
loop
+                     if Label = Rule.Labels (I) then
+                        Output (I) := True;
+                     end if;
+                  end loop;
+               end Update_Output;
+            begin
+               for I in RHS.Tokens.First_Index .. RHS.Tokens.Last_Index loop
+                  if Length (RHS.Tokens (I).Label) > 0 then
+                     declare
+                        Label : constant String := -RHS.Tokens (I).Label;
+                     begin
+                        if Match (Line, Compile (Symbol_Regexp (Label), 
Case_Sensitive => False)) then
+                           Indent_Line
+                             (Label & " : constant SAL.Peek_Type :=" & 
SAL.Peek_Type'Image (I) & ";");
+                           Update_Output (Label);
+                        end if;
+                     end;
+                  end if;
+               end loop;
+
+               for I in Rule.Labels.First_Index .. Rule.Labels.Last_Index loop
+                  if not Output (I) and
+                    Match (Line, Compile (Symbol_Regexp (-Rule.Labels (I)), 
Case_Sensitive => False))
+                  then
+                     Indent_Line (-Rule.Labels (I) & " : constant 
SAL.Base_Peek_Type := SAL.Base_Peek_Type'First;");
+                  end if;
+               end loop;
+            end Put_Labels;
+
          begin
             for RHS of Rule.Right_Hand_Sides loop
                if Length (RHS.Action) > 0 and then not Is_Elisp (RHS.Action) 
then
@@ -141,7 +182,6 @@ is
                            Unref_Tokens := False;
                         end if;
                      end Check_Unref;
-
                   begin
                      Check_Unref (Line);
                      Indent_Line ("procedure " & Name);
@@ -151,11 +191,12 @@ is
                      Indent_Line ("  Tokens    : in     
WisiToken.Syntax_Trees.Valid_Node_Index_Array)");
                      Indent_Line ("is");
 
+                     Indent := Indent + 3;
                      if Unref_User_Data or Unref_Tree or Unref_Nonterm or 
Unref_Tokens then
-                        Indent_Start ("   pragma Unreferenced (");
+                        Indent_Start ("pragma Unreferenced (");
 
                         if Unref_User_Data then
-                           Put ((if Need_Comma then ", " else "") & 
"User_Data");
+                           Put ("User_Data");
                            Need_Comma := True;
                         end if;
                         if Unref_Tree then
@@ -173,6 +214,8 @@ is
                         Put_Line (");");
                      end if;
 
+                     Put_Labels (RHS, Line);
+                     Indent := Indent - 3;
                      Indent_Line ("begin");
                      Indent := Indent + 3;
 
@@ -192,6 +235,7 @@ is
                      Unref_Nonterm : constant Boolean := 0 = Index (Line, 
"Nonterm");
                      Unref_Tokens  : constant Boolean := 0 = Index (Line, 
"Tokens");
                      Unref_Recover : constant Boolean := 0 = Index (Line, 
"Recover_Active");
+                     Need_Comma    : Boolean          := False;
                   begin
                      Indent_Line ("function " & Name);
                      Indent_Line (" (Lexer          : access constant 
WisiToken.Lexer.Instance'Class;");
@@ -201,19 +245,32 @@ is
                      Indent_Line (" return 
WisiToken.Semantic_Checks.Check_Status");
                      Indent_Line ("is");
 
-                     if Unref_Lexer then
-                        Indent_Line ("   pragma Unreferenced (Lexer);");
-                     end if;
-                     if Unref_Nonterm then
-                        Indent_Line ("   pragma Unreferenced (Nonterm);");
-                     end if;
-                     if Unref_Tokens then
-                        Indent_Line ("   pragma Unreferenced (Tokens);");
-                     end if;
-                     if Unref_Recover then
-                        Indent_Line ("   pragma Unreferenced 
(Recover_Active);");
+                     Indent := Indent + 3;
+                     if Unref_Lexer or Unref_Nonterm or Unref_Tokens or 
Unref_Recover then
+                        Indent_Start ("pragma Unreferenced (");
+
+                        if Unref_Lexer then
+                           Put ("Lexer");
+                           Need_Comma := True;
+                        end if;
+                        if Unref_Nonterm then
+                           Put ((if Need_Comma then ", " else "") & "Nonterm");
+                           Need_Comma := True;
+                        end if;
+                        if Unref_Tokens then
+                           Put ((if Need_Comma then ", " else "") & "Tokens");
+                           Need_Comma := True;
+                        end if;
+                        if Unref_Recover then
+                           Put ((if Need_Comma then ", " else "") & 
"Recover_Active");
+                           Need_Comma := True;
+                        end if;
+                        Put_Line (");");
                      end if;
 
+                     Put_Labels (RHS, Line);
+                     Indent := Indent - 3;
+
                      Indent_Line ("begin");
                      Indent := Indent + 3;
                      Indent_Line (Line);
@@ -274,9 +331,7 @@ is
 
       case Common_Data.Generate_Algorithm is
       when LR_Generate_Algorithm =>
-         if Tuple.Text_Rep then
-            Put_Line ("with WisiToken.Productions;");
-         end if;
+         null;
 
       when Packrat_Gen =>
          Put_Line ("with WisiToken.Parse.Packrat.Generated;");
@@ -351,7 +406,7 @@ is
       Unit_Name : constant String := File_Name_To_Ada (Output_File_Name_Root) &
         "_" & Generate_Algorithm'Image (Common_Data.Generate_Algorithm) & 
"_Run";
 
-      Language_Package_Name : constant String := 
"WisiToken.Parse.LR.McKenzie_Recover." & File_Name_To_Ada
+      Default_Language_Runtime_Package : constant String := 
"WisiToken.Parse.LR.McKenzie_Recover." & File_Name_To_Ada
         (Output_File_Name_Root);
 
       File_Name : constant String := To_Lower (Unit_Name) & ".ads";
@@ -369,8 +424,19 @@ is
       Put_Line ("with " & Generic_Package_Name & ";");
       Put_Line ("with " & Actions_Package_Name & ";");
       Put_Line ("with " & Main_Package_Name & ";");
-      if Input_Data.Language_Params.Error_Recover then
-         Put_Line ("with " & Language_Package_Name & "; use " & 
Language_Package_Name & ";");
+      if Input_Data.Language_Params.Error_Recover and
+        Input_Data.Language_Params.Use_Language_Runtime
+      then
+         declare
+            Pkg : constant String :=
+              (if -Input_Data.Language_Params.Language_Runtime_Name = ""
+               then Default_Language_Runtime_Package
+               else -Input_Data.Language_Params.Language_Runtime_Name);
+         begin
+            --  For language-specific names in actions, checks.
+            Put_Line ("with " & Pkg & ";");
+            Put_Line ("use " & Pkg & ";");
+         end;
       end if;
 
       Put_Line ("procedure " & Unit_Name & " is new " & Generic_Package_Name);
@@ -381,7 +447,11 @@ is
                      "_parse_table.txt"",");
       end if;
       if Input_Data.Language_Params.Error_Recover then
-         Put_Line ("Fixes'Access, Use_Minimal_Complete_Actions'Access, 
String_ID_Set'Access,");
+         if Input_Data.Language_Params.Use_Language_Runtime then
+            Put_Line ("Fixes'Access, Matching_Begin_Tokens'Access, 
String_ID_Set'Access,");
+         else
+            Put_Line ("null, null, null,");
+         end if;
       end if;
       Put_Line (Main_Package_Name & ".Create_Parser);");
       Close (File);
@@ -414,7 +484,8 @@ begin
    begin
       if Input_Data.Action_Count > 0 or Input_Data.Check_Count > 0 then
          --  Some WisiToken tests have no actions or checks.
-         Create_Ada_Actions_Body (Generate_Data.Action_Names, 
Generate_Data.Check_Names, Actions_Package_Name);
+         Create_Ada_Actions_Body
+           (Generate_Data.Action_Names, Generate_Data.Check_Names, 
Input_Data.Label_Count, Actions_Package_Name);
       end if;
 
       Create_Ada_Actions_Spec
diff --git a/wisitoken-bnf-output_ada_common.adb 
b/wisitoken-bnf-output_ada_common.adb
index cbd36b7..b3d7dea 100644
--- a/wisitoken-bnf-output_ada_common.adb
+++ b/wisitoken-bnf-output_ada_common.adb
@@ -133,23 +133,18 @@ package body WisiToken.BNF.Output_Ada_Common is
       Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Actions_Spec_Pre));
 
       Indent_Line ("Descriptor : aliased WisiToken.Descriptor :=");
-      Indent_Line ("  (First_Terminal                =>" & 
WisiToken.Token_ID'Image (Descriptor.First_Terminal) & ",");
+      Indent_Line ("  (First_Terminal    =>" & WisiToken.Token_ID'Image 
(Descriptor.First_Terminal) & ",");
       Indent := Indent + 3;
-      Indent_Line ("Last_Terminal                 =>" & 
WisiToken.Token_ID'Image (Descriptor.Last_Terminal) & ",");
-      Indent_Line ("First_Nonterminal             =>" & 
WisiToken.Token_ID'Image (Descriptor.First_Nonterminal) & ",");
-      Indent_Line ("Last_Nonterminal              =>" & 
WisiToken.Token_ID'Image (Descriptor.Last_Nonterminal) & ",");
-      Indent_Line ("EOI_ID                        =>" & 
WisiToken.Token_ID'Image (Descriptor.EOI_ID) & ",");
-      Indent_Line ("Accept_ID                     =>" & 
WisiToken.Token_ID'Image (Descriptor.Accept_ID) & ",");
-      Indent_Line ("Case_Insensitive              => " & Image 
(Input_Data.Language_Params.Case_Insensitive) & ",");
-      Indent_Line ("New_Line_ID                   =>" & 
WisiToken.Token_ID'Image (Descriptor.New_Line_ID) & ",");
-      Indent_Line ("Comment_ID                    =>" & 
WisiToken.Token_ID'Image (Descriptor.Comment_ID) & ",");
-      Indent_Line ("Left_Paren_ID                 =>" & 
WisiToken.Token_ID'Image (Descriptor.Left_Paren_ID) & ",");
-      Indent_Line ("Right_Paren_ID                =>" & 
WisiToken.Token_ID'Image (Descriptor.Right_Paren_ID) & ",");
-      Indent_Line ("String_1_ID                   =>" & 
WisiToken.Token_ID'Image (Descriptor.String_1_ID) & ",");
-      Indent_Line ("String_2_ID                   =>" & 
WisiToken.Token_ID'Image (Descriptor.String_2_ID) & ",");
-      Indent_Line ("Embedded_Quote_Escape_Doubled => " & Image
-                     
(Input_Data.Language_Params.Embedded_Quote_Escape_Doubled) & ",");
-      Indent_Line ("Image                         =>");
+      Indent_Line ("Last_Terminal     =>" & WisiToken.Token_ID'Image 
(Descriptor.Last_Terminal) & ",");
+      Indent_Line ("First_Nonterminal =>" & WisiToken.Token_ID'Image 
(Descriptor.First_Nonterminal) & ",");
+      Indent_Line ("Last_Nonterminal  =>" & WisiToken.Token_ID'Image 
(Descriptor.Last_Nonterminal) & ",");
+      Indent_Line ("EOI_ID            =>" & WisiToken.Token_ID'Image 
(Descriptor.EOI_ID) & ",");
+      Indent_Line ("Accept_ID         =>" & WisiToken.Token_ID'Image 
(Descriptor.Accept_ID) & ",");
+      Indent_Line ("Case_Insensitive  => " & Image 
(Input_Data.Language_Params.Case_Insensitive) & ",");
+      Indent_Line ("New_Line_ID       =>" & WisiToken.Token_ID'Image 
(Descriptor.New_Line_ID) & ",");
+      Indent_Line ("String_1_ID       =>" & WisiToken.Token_ID'Image 
(Descriptor.String_1_ID) & ",");
+      Indent_Line ("String_2_ID       =>" & WisiToken.Token_ID'Image 
(Descriptor.String_2_ID) & ",");
+      Indent_Line ("Image             =>");
       Indent_Start ("  (");
       Indent := Indent + 3;
       loop
@@ -264,12 +259,12 @@ package body WisiToken.BNF.Output_Ada_Common is
       is begin
          Indent_Line ("procedure Create_Parser");
          if Input_Data.Language_Params.Error_Recover then
-            Indent_Line ("  (Parser                       :    out 
WisiToken.Parse.LR.Parser.Parser;");
-            Indent_Line ("   Language_Fixes               : in     
WisiToken.Parse.LR.Parser.Language_Fixes_Access;");
-            Indent_Line ("   Language_Use_Minimal_Complete_Actions : in");
-            Indent_Line ("  
WisiToken.Parse.LR.Parser.Language_Use_Minimal_Complete_Actions_Access;");
-            Indent_Line
-              ("   Language_String_ID_Set       : in     
WisiToken.Parse.LR.Parser.Language_String_ID_Set_Access;");
+            Indent_Line ("  (Parser                         :    out 
WisiToken.Parse.LR.Parser.Parser;");
+            Indent_Line ("   Language_Fixes                 : in     
WisiToken.Parse.LR.Parser.Language_Fixes_Access;");
+            Indent_Line ("   Language_Matching_Begin_Tokens : in     " &
+                           
"WisiToken.Parse.LR.Parser.Language_Matching_Begin_Tokens_Access;");
+            Indent_Line ("   Language_String_ID_Set         : in     " &
+                           
"WisiToken.Parse.LR.Parser.Language_String_ID_Set_Access;");
          else
             Indent_Line ("  (Parser                       :    out 
WisiToken.Parse.LR.Parser_No_Recover.Parser;");
          end if;
@@ -418,7 +413,6 @@ package body WisiToken.BNF.Output_Ada_Common is
       Generate_Data : in WisiToken.BNF.Generate_Utils.Generate_Data)
    is
       use Ada.Strings.Unbounded;
-      use all type Ada.Containers.Count_Type;
 
       subtype Nonterminal_ID is Token_ID range
         Generate_Data.Grammar.First_Index .. Generate_Data.Grammar.Last_Index;
@@ -461,10 +455,14 @@ package body WisiToken.BNF.Output_Ada_Common is
       Put ("Insert", Table.McKenzie_Param.Insert);
       Put ("Delete", Table.McKenzie_Param.Delete);
       Put ("Push_Back", Table.McKenzie_Param.Push_Back);
+      Put ("Undo_Reduce", Table.McKenzie_Param.Undo_Reduce);
+      Indent_Line
+        ("Minimal_Complete_Cost_Delta => " & Integer'Image 
(Table.McKenzie_Param.Minimal_Complete_Cost_Delta) & ",");
+      Indent_Line ("Fast_Forward => " & Integer'Image 
(Table.McKenzie_Param.Fast_Forward) & ",");
+      Indent_Line ("Matching_Begin => " & Integer'Image 
(Table.McKenzie_Param.Matching_Begin) & ",");
       Indent_Line ("Ignore_Check_Fail  =>" & Integer'Image 
(Table.McKenzie_Param.Ignore_Check_Fail) & ",");
       Indent_Line ("Task_Count  =>" & System.Multiprocessors.CPU_Range'Image
                      (Table.McKenzie_Param.Task_Count) & ",");
-      Indent_Line ("Cost_Limit  =>" & Integer'Image 
(Table.McKenzie_Param.Cost_Limit) & ",");
       Indent_Line ("Check_Limit =>" & Token_Index'Image 
(Table.McKenzie_Param.Check_Limit) & ",");
       Indent_Line ("Check_Delta_Limit =>" & Integer'Image 
(Table.McKenzie_Param.Check_Delta_Limit) & ",");
       Indent_Line ("Enqueue_Limit =>" & Integer'Image 
(Table.McKenzie_Param.Enqueue_Limit) & ");");
@@ -472,62 +470,48 @@ package body WisiToken.BNF.Output_Ada_Common is
       New_Line;
 
       if Common_Data.Text_Rep then
-         Indent_Line ("function Productions return 
WisiToken.Productions.Prod_Arrays.Vector");
+         Indent_Line ("function Actions return 
WisiToken.Parse.LR.Semantic_Action_Array_Arrays.Vector");
          Indent_Line ("is begin");
          Indent := Indent + 3;
-         Indent_Line ("return Prods : WisiToken.Productions.Prod_Arrays.Vector 
do");
+         Indent_Line ("return Acts : 
WisiToken.Parse.LR.Semantic_Action_Array_Arrays.Vector do");
          Indent := Indent + 3;
          Indent_Line
-           ("Prods.Set_First (" & Trimmed_Image 
(Generate_Data.Grammar.First_Index) & ");");
-         Indent_Line
-           ("Prods.Set_Last (" & Trimmed_Image 
(Generate_Data.Grammar.Last_Index) & ");");
+           ("Acts.Set_First_Last (" & Trimmed_Image 
(Generate_Data.Grammar.First_Index) & ", " &
+              Trimmed_Image (Generate_Data.Grammar.Last_Index) & ");");
 
          for I in Nonterminal_ID loop
             declare
                P : Productions.Instance renames Generate_Data.Grammar (I);
             begin
-               Indent_Line
-                 ("Set_Production (Prods (" & Trimmed_Image (P.LHS) & "), " &
-                    Trimmed_Image (P.LHS) & "," & Integer'Image 
(P.RHSs.Last_Index) & ");");
-
-               for J in P.RHSs.First_Index .. P.RHSs.Last_Index loop
-                  Line := +"Set_RHS (Prods (" & Trimmed_Image (P.LHS) & ")," & 
Natural'Image (J) & ", (";
-                  declare
-                     RHS : Productions.Right_Hand_Side renames P.RHSs (J);
-                  begin
-                     if RHS.Tokens.Length = 0 then
-                        Append ("1 .. 0 => <>");
-                     elsif RHS.Tokens.Length = 1 then
-                        Append ("1 => " & Trimmed_Image (RHS.Tokens (1)));
-                     else
-                        for I in RHS.Tokens.First_Index .. 
RHS.Tokens.Last_Index loop
-                           Append (Trimmed_Image (RHS.Tokens (I)));
-                           if I < RHS.Tokens.Last_Index then
-                              Append (", ");
-                           end if;
-                        end loop;
+               if Generate_Data.Action_Names (P.LHS) /= null or 
Generate_Data.Check_Names (P.LHS) /= null then
+                  Indent_Line
+                    ("Acts (" & Trimmed_Image (P.LHS) & ").Set_First_Last (0," 
&
+                       Integer'Image (P.RHSs.Last_Index) & ");");
+
+                  for J in P.RHSs.First_Index .. P.RHSs.Last_Index loop
+                     if (Generate_Data.Action_Names (P.LHS) /= null and then
+                           Generate_Data.Action_Names (P.LHS)(J) /= null)
+                       or
+                       (Generate_Data.Check_Names (P.LHS) /= null and then
+                          Generate_Data.Check_Names (P.LHS) /= null)
+                     then
+                        Indent_Wrap
+                          ("Acts (" & Trimmed_Image (P.LHS) & ")(" & 
Trimmed_Image (J) & ") := (" &
+                             (if Generate_Data.Action_Names (P.LHS) = null 
then "null"
+                              elsif Generate_Data.Action_Names (P.LHS)(J) = 
null then "null"
+                              else Generate_Data.Action_Names (P.LHS)(J).all & 
"'Access") & ", " &
+                             (if Generate_Data.Check_Names (P.LHS) = null then 
"null"
+                              elsif Generate_Data.Check_Names (P.LHS)(J) = 
null then "null"
+                              else Generate_Data.Check_Names (P.LHS)(J).all & 
"'Access") & ");");
                      end if;
-
-                     Append ("), ");
-                     Append
-                       ((if Generate_Data.Action_Names (P.LHS) = null then 
"null"
-                         elsif Generate_Data.Action_Names (P.LHS)(J) = null 
then "null"
-                         else Generate_Data.Action_Names (P.LHS)(J).all & 
"'Access"));
-                     Append (", ");
-                     Append
-                       ((if Generate_Data.Check_Names (P.LHS) = null then 
"null"
-                         elsif Generate_Data.Check_Names (P.LHS)(J) = null 
then "null"
-                         else Generate_Data.Check_Names (P.LHS)(J).all & 
"'Access"));
-                  end;
-                  Append (");");
-                  Indent_Wrap (-Line);
-               end loop;
+                  end loop;
+               end if;
             end;
          end loop;
          Indent := Indent - 3;
          Indent_Line ("end return;");
          Indent := Indent - 3;
-         Indent_Line ("end Productions;");
+         Indent_Line ("end Actions;");
          New_Line;
       end if;
    end Create_LR_Parser_Core_1;
@@ -536,7 +520,8 @@ package body WisiToken.BNF.Output_Ada_Common is
      (Input_Data    : in WisiToken_Grammar_Runtime.User_Data_Type;
       Generate_Data : in WisiToken.BNF.Generate_Utils.Generate_Data)
    is
-      use all type WisiToken.Parse.LR.All_Parse_Action_Verbs;
+      use all type Ada.Containers.Count_Type;
+      use WisiToken.Parse.LR;
       use Ada.Strings.Unbounded;
 
       Table            : WisiToken.Parse.LR.Parse_Table_Ptr renames 
Generate_Data.LR_Parse_Table;
@@ -566,17 +551,9 @@ package body WisiToken.BNF.Output_Ada_Common is
 
       Declare_Subroutines :
       for State_Index in Table.States'Range loop
-
-         if Input_Data.Language_Params.Error_Recover then
-            Indent_Wrap
-              ("Table.States (" & Trimmed_Image (State_Index) & ").Productions 
:= WisiToken.To_Vector (" &
-                 Image (Table.States (State_Index).Productions, Strict => 
True) & ");");
-         end if;
-
          Actions :
          declare
             use Ada.Containers;
-            use WisiToken.Parse.LR;
             Base_Indent : constant Ada.Text_IO.Count := Indent;
             Node        : Action_Node_Ptr := Table.States 
(State_Index).Action_List;
          begin
@@ -622,6 +599,7 @@ package body WisiToken.BNF.Output_Ada_Common is
                           Trimmed_Image (Node.Symbol);
                         Append (", ");
                         Append (Trimmed_Image (Action_Node.Item.State));
+                        Append (");");
 
                      when Reduce | Accept_It =>
                         Line := +"Add_Action (Table.States (" & Trimmed_Image 
(State_Index) & "), " &
@@ -651,18 +629,23 @@ package body WisiToken.BNF.Output_Ada_Common is
                             else Generate_Data.Check_Names
                               
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS).all &
                                "'Access"));
+                        Append (");");
 
                      when Parse.LR.Error =>
-                        Line := +"Add_Error (Table.States (" & Trimmed_Image 
(State_Index) & ")";
+                        Line := +"Add_Error (Table.States (" & Trimmed_Image 
(State_Index) & "));";
                      end case;
+                     Indent_Wrap (-Line);
+                     Line_Count := Line_Count + 1;
 
-                     Action_Node := Action_Node.Next;
-                     if Action_Node /= null then
+                     loop
+                        Action_Node := Action_Node.Next;
+                        exit when Action_Node = null;
                         --  There is a conflict; must be Shift/{Reduce|Accept} 
or Reduce/{Reduce|Accept}.
                         --  The added parameters are the same in either case.
                         case Action_Node.Item.Verb is
                         when Reduce | Accept_It =>
-                           Append (", ");
+                           Line := +"Add_Conflict (Table.States (" & 
Trimmed_Image (State_Index) & "), " &
+                             Trimmed_Image (Node.Symbol) & ", ";
                            Append (Image (Action_Node.Item.Production) & ",");
                            Append (Count_Type'Image 
(Action_Node.Item.Token_Count) & ", ");
                            Append
@@ -682,24 +665,23 @@ package body WisiToken.BNF.Output_Ada_Common is
                                else Generate_Data.Check_Names
                                  
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS).all &
                                   "'Access"));
+                           Indent_Wrap (-Line & ");");
+                           Line_Count := Line_Count + 1;
 
                         when others =>
-                           raise SAL.Programmer_Error with "conflict second 
action verb: " &
+                           raise SAL.Programmer_Error with "invalid conflict 
action verb: " &
                              Parse.LR.Parse_Action_Verbs'Image 
(Action_Node.Item.Verb);
                         end case;
-                     end if;
+                     end loop;
                   end;
-                  Indent_Wrap (-Line & ");");
-                  Line_Count := Line_Count + 1;
-                  Indent     := Base_Indent;
-                  Node       := Node.Next;
+                  Indent := Base_Indent;
+                  Node   := Node.Next;
                end loop;
             end if;
          end Actions;
 
          Gotos :
          declare
-            use WisiToken.Parse.LR;
             Node : Goto_Node_Ptr := Table.States (State_Index).Goto_List;
          begin
             loop
@@ -712,12 +694,21 @@ package body WisiToken.BNF.Output_Ada_Common is
             end loop;
          end Gotos;
 
-         if Input_Data.Language_Params.Error_Recover and
-           Table.States (State_Index).Minimal_Complete_Action.Verb /= 
Parse.LR.Pause
-         then
-            Indent_Wrap
-              ("Table.States (" & Trimmed_Image (State_Index) & 
").Minimal_Complete_Action := " &
-                 WisiToken.Parse.LR.Strict_Image (Table.States 
(State_Index).Minimal_Complete_Action) & ";");
+         if Input_Data.Language_Params.Error_Recover then
+            if Table.States (State_Index).Kernel.Length > 0 then
+               Indent_Wrap
+                 ("Table.States (" & Trimmed_Image (State_Index) & ").Kernel 
:= To_Vector (" &
+                    Image (Table.States (State_Index).Kernel, Strict => True) 
& ");");
+            end if;
+            if Table.States (State_Index).Minimal_Complete_Actions.Length > 0 
then
+               Indent_Wrap
+                 ("Table.States (" & Trimmed_Image (State_Index) & 
").Minimal_Complete_Actions := To_Vector (" &
+                    Strict_Image (Table.States 
(State_Index).Minimal_Complete_Actions, Strict => True) & ");");
+               if Table.States 
(State_Index).Minimal_Complete_Actions_Recursive then
+                  Indent_Wrap
+                    ("Table.States (" & Trimmed_Image (State_Index) & 
").Minimal_Complete_Actions_Recursive := True;");
+               end if;
+            end if;
          end if;
 
          if Line_Count > Lines_Per_Subr then
@@ -765,14 +756,14 @@ package body WisiToken.BNF.Output_Ada_Common is
       case Common_Data.Interface_Kind is
       when Process =>
          if Input_Data.Language_Params.Error_Recover then
-            Indent_Line ("  (Parser                       :    out 
WisiToken.Parse.LR.Parser.Parser;");
-            Indent_Line ("   Language_Fixes               : in     
WisiToken.Parse.LR.Parser.Language_Fixes_Access;");
-            Indent_Line ("   Language_Use_Minimal_Complete_Actions : in");
-            Indent_Line ("  
WisiToken.Parse.LR.Parser.Language_Use_Minimal_Complete_Actions_Access;");
+            Indent_Line ("  (Parser                         :    out 
WisiToken.Parse.LR.Parser.Parser;");
+            Indent_Line ("   Language_Fixes                 : in     
WisiToken.Parse.LR.Parser.Language_Fixes_Access;");
+            Indent_Line ("   Language_Matching_Begin_Tokens : in     " &
+                           
"WisiToken.Parse.LR.Parser.Language_Matching_Begin_Tokens_Access;");
             Indent_Line
               ("   Language_String_ID_Set       : in     
WisiToken.Parse.LR.Parser.Language_String_ID_Set_Access;");
          else
-            Indent_Line ("  (Parser                       :    out 
WisiToken.Parse.LR.Parser_No_Recover.Parser;");
+            Indent_Line ("  (Parser                         :    out 
WisiToken.Parse.LR.Parser_No_Recover.Parser;");
          end if;
          Indent_Line ("   Trace                        : not null access 
WisiToken.Trace'Class;");
          Indent_Start ("   User_Data                    : in     
WisiToken.Syntax_Trees.User_Data_Access");
@@ -798,7 +789,7 @@ package body WisiToken.BNF.Output_Ada_Common is
       if Common_Data.Text_Rep then
          Create_LR_Parser_Core_1 (Common_Data, Generate_Data);
          Indent_Line ("Table : constant Parse_Table_Ptr := Get_Text_Rep");
-         Indent_Line ("  (Text_Rep_File_Name, McKenzie_Param, Productions);");
+         Indent_Line ("  (Text_Rep_File_Name, McKenzie_Param, Actions);");
          Indent := Indent - 3;
          Indent_Line ("begin");
          Indent := Indent + 3;
@@ -841,7 +832,7 @@ package body WisiToken.BNF.Output_Ada_Common is
          Indent_Line ("   Table,");
          if Input_Data.Language_Params.Error_Recover then
             Indent_Line ("   Language_Fixes,");
-            Indent_Line ("   Language_Use_Minimal_Complete_Actions,");
+            Indent_Line ("   Language_Matching_Begin_Tokens,");
             Indent_Line ("   Language_String_ID_Set,");
          end if;
          Indent_Line ("   User_Data,");
diff --git a/wisitoken-bnf-output_ada_emacs.adb 
b/wisitoken-bnf-output_ada_emacs.adb
index a16d289..ae109fa 100644
--- a/wisitoken-bnf-output_ada_emacs.adb
+++ b/wisitoken-bnf-output_ada_emacs.adb
@@ -40,6 +40,7 @@ with WisiToken.Generate.Packrat;
 with WisiToken_Grammar_Runtime;
 procedure WisiToken.BNF.Output_Ada_Emacs
   (Input_Data            :         in WisiToken_Grammar_Runtime.User_Data_Type;
+   Elisp_Tokens          :         in WisiToken.BNF.Tokens;
    Output_File_Name_Root :         in String;
    Generate_Data         : aliased in 
WisiToken.BNF.Generate_Utils.Generate_Data;
    Packrat_Data          :         in WisiToken.Generate.Packrat.Data;
@@ -50,9 +51,10 @@ procedure WisiToken.BNF.Output_Ada_Emacs
 is
    use all type Ada.Containers.Count_Type;
 
-   Language_Runtime_Package : constant String := "Wisi." & Language_Name;
+   Default_Language_Runtime_Package : constant String := "Wisi." & 
Language_Name;
 
    Blank_Set : constant Ada.Strings.Maps.Character_Set := 
Ada.Strings.Maps.To_Set (" ");
+   Numeric   : constant Ada.Strings.Maps.Character_Set := 
Ada.Strings.Maps.To_Set ("0123456789");
 
    Common_Data : Output_Ada_Common.Common_Data := 
WisiToken.BNF.Output_Ada_Common.Initialize
      (Input_Data, Tuple, Output_File_Name_Root, Check_Interface => True);
@@ -155,7 +157,9 @@ is
    procedure Create_Ada_Action
      (Name          : in String;
       RHS           : in RHS_Type;
+      Prod_ID       : in WisiToken.Production_ID;
       Unsplit_Lines : in Ada.Strings.Unbounded.Unbounded_String;
+      Labels        : in String_Arrays.Vector;
       Check         : in Boolean)
    is
       --  Create Action (if Check = False; Lines must be RHS.Action) or
@@ -180,9 +184,63 @@ is
       Indent_Action_Line : Unbounded_String;
       Check_Line         : Unbounded_String;
 
+      Label_Needed   : array (Labels.First_Index .. Labels.Last_Index) of 
Boolean := (others => False);
+      Nonterm_Needed : Boolean := False;
+
+      function Label_Used (Label : in String) return Boolean
+      is
+         Found : Boolean := False;
+      begin
+         for Tok of RHS.Tokens loop
+            if -Tok.Label = Label then
+               Found := True;
+               exit;
+            end if;
+         end loop;
+
+         if not Found then
+            return False;
+         end if;
+
+         for I in Labels.First_Index .. Labels.Last_Index loop
+            if Label = Labels (I) then
+               Label_Needed (I) := True;
+               return True;
+            end if;
+         end loop;
+         raise SAL.Programmer_Error;
+      end Label_Used;
+
+      function Count_Label_Needed return Ada.Containers.Count_Type
+      is
+         use Ada.Containers;
+         Result : Count_Type := 0;
+      begin
+         for B of Label_Needed loop
+            if B then Result := Result + 1; end if;
+         end loop;
+         return Result;
+      end Count_Label_Needed;
+
+      function Find_Token_Index (I : in Base_Identifier_Index) return 
SAL.Base_Peek_Type
+      is
+         Rule_Label : constant String := -Labels (I);
+      begin
+         for I in RHS.Tokens.First_Index .. RHS.Tokens.Last_Index loop
+            if Length (RHS.Tokens (I).Label) > 0 and then
+              -RHS.Tokens (I).Label = Rule_Label
+            then
+               return I;
+            end if;
+         end loop;
+         return SAL.Base_Peek_Type'First;
+      end Find_Token_Index;
+
       function Statement_Params (Params : in String) return String
       is
          --  Input looks like: [1 function 2 other ...]
+         --  Numbers can be token labels.
+
          Last       : Integer := Index_Non_Blank (Params); -- skip [
          First      : Integer;
          Second     : Integer;
@@ -195,30 +253,46 @@ is
             Second := Index (Params, Blank_Set, First);
             exit when Second = 0;
 
-            Count := Count + 1;
-            Last  := Index (Params, Space_Paren_Set, Second + 1);
+            Last := Index (Params, Space_Paren_Set, Second + 1);
 
-            Result := Result & (if Need_Comma then ", " else "") &
-              "(" & Params (First .. Second - 1) & ", " &
-              Elisp_Name_To_Ada (Params (Second + 1 .. Last - 1), Append_ID => 
False, Trim => 0) & ")";
+            declare
+               Label : constant String := Params (First .. Second - 1);
+            begin
+               if 0 = Index (Label, Numeric, Outside) or else Label_Used 
(Label) then
+                  Count := Count + 1;
+                  Result := Result & (if Need_Comma then ", " else "") &
+                    "(" & Label & ", " &
+                    Elisp_Name_To_Ada (Params (Second + 1 .. Last - 1), 
Append_ID => False, Trim => 0) & ")";
 
-            Need_Comma := True;
+                  Need_Comma := True;
+               --  else skip
+               end if;
+            end;
          end loop;
-         if Count = 1 then
-            return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
-         else
-            return " (Parse_Data, Tree, Nonterm, Tokens, (" & (-Result) & "))";
-         end if;
+         Nonterm_Needed := True;
+         return " (Parse_Data, Tree, Nonterm, Tokens, " &
+           (case Count is
+            when 0 => "(1 .. 0 => (1, Motion)))",
+            when 1 => "(1 => " & (-Result) & "))",
+            when others =>  "(" & (-Result) & "))");
       end Statement_Params;
 
       function Containing_Params (Params : in String) return String
       is
          --  Input looks like: 1 2)
-         First  : constant Integer := Index_Non_Blank (Params);
-         Second : constant Integer := Index (Params, Blank_Set, First);
+         First        : constant Integer := Index_Non_Blank (Params);
+         Second       : constant Integer := Index (Params, Blank_Set, First);
+         First_Label  : constant String  := Params (First .. Second - 1);
+         Second_Label : constant String  := Params (Second + 1 .. Params'Last 
- 1);
       begin
-         return " (Parse_Data, Tree, Nonterm, Tokens, " &
-           Params (First .. Second - 1) & ',' & Params (Second .. Params'Last);
+         if (0 = Index (First_Label, Numeric, Outside) or else Label_Used 
(First_Label)) and
+           (0 = Index (Second_Label, Numeric, Outside) or else Label_Used 
(Second_Label))
+         then
+            Nonterm_Needed := True;
+            return " (Parse_Data, Tree, Nonterm, Tokens, " & First_Label & ", 
" & Second_Label & ")";
+         else
+            return "";
+         end if;
       end Containing_Params;
 
       function Motion_Params (Params : in String) return String
@@ -273,14 +347,27 @@ is
                   end;
                end loop;
 
-               Result := Result & (if Need_Comma_1 then " & " else "") & "(" &
-                 Params (Index_First .. Index_Last) & ", " &
-                 (if IDs_Count = 1 then "+" else "") & IDs & ")";
+               declare
+                  Label : constant String := Params (Index_First .. 
Index_Last);
+               begin
+                  if 0 = Index (Label, Numeric, Outside) or else Label_Used 
(Label) then
+                     Nonterm_Needed := True;
+                     Result := Result & (if Need_Comma_1 then " & " else "") & 
"(" &
+                       Label & ", " &
+                       (if IDs_Count = 1 then "+" else "") & IDs & ")";
+                  end if;
+               end;
             else
                First  := Index_Non_Blank (Params, Last);
                Last   := Index (Params, Delim, First);
-               Result := Result & (if Need_Comma_1 then " & " else "") &
-                 "(" & Params (First .. Last - 1) & ", Empty_IDs)";
+               declare
+                  Label : constant String := Params (First .. Last - 1);
+               begin
+                  if 0 = Index (Label, Numeric, Outside) or else Label_Used 
(Label) then
+                     Nonterm_Needed := True;
+                     Result := Result & (if Need_Comma_1 then " & " else "") & 
"(" & Label & ", Empty_IDs)";
+                  end if;
+               end;
             end if;
             Need_Comma_1 := True;
          end loop;
@@ -290,6 +377,8 @@ is
       function Face_Apply_Params (Params : in String) return String
       is
          --  Params is a vector of triples: [1 nil font-lock-keyword-face 3 
nil font-lock-function-name-face ...]
+         --  Each triple is <token_number> <prefix-face> <suffix-face>.
+         --  The token_number can be a label; faces are "nil" or an elisp name.
          --  Result: ((1, 3, 1), (3, 3, 2), ...)
          use Ada.Strings.Maps;
          Delim : constant Character_Set := To_Set ("]") or Blank_Set;
@@ -299,46 +388,55 @@ is
          Result     : Unbounded_String;
          Need_Comma : Boolean          := False;
          Count      : Integer          := 0;
-      begin
-         loop
-            Last := Index_Non_Blank (Params, Last + 1);
-
-            exit when Params (Last) = ']' or Params (Last) = ')';
-
-            Count  := Count + 1;
-            First  := Last;
-            Last   := Index (Params, Delim, First);
-            Result := Result & (if Need_Comma then ", (" else "(") & Params 
(First .. Last - 1);
 
+         procedure Elisp_Param (Skip : in Boolean)
+         is begin
             if Params (Last) = ']' then
                Put_Error
                  (Error_Message
                     (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"invalid wisi-face-apply argument"));
-               exit;
+               return;
             end if;
 
             First  := Index_Non_Blank (Params, Last + 1);
             Last   := Index (Params, Delim, First);
-            Result := Result & ',' & Integer'Image
-              (Find_Elisp_ID (Input_Data.Tokens.Faces, Params (First .. Last - 
1)));
-
-            if Params (Last) = ']' then
-               Put_Error
-                 (Error_Message
-                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"invalid wisi-face-apply argument"));
-               exit;
+            if not Skip then
+               Result := Result & ',' & Integer'Image
+                 (Find_Elisp_ID (Input_Data.Tokens.Faces, Params (First .. 
Last - 1)));
             end if;
+         end Elisp_Param;
 
-            First  := Index_Non_Blank (Params, Last + 1);
-            Last   := Index (Params, Delim, First);
-            Result := Result & ',' &
-              Integer'Image (Find_Elisp_ID (Input_Data.Tokens.Faces, Params 
(First .. Last - 1))) & ")";
+      begin
+         loop
+            Last := Index_Non_Blank (Params, Last + 1);
 
-            Need_Comma := True;
+            exit when Params (Last) = ']' or Params (Last) = ')';
+
+            First := Last;
+            Last  := Index (Params, Delim, First);
+            declare
+               Label : constant String := Params (First .. Last - 1);
+            begin
+               if 0 = Index (Label, Numeric, Outside) or else Label_Used 
(Label) then
+                  Count  := Count + 1;
+                  Result := Result & (if Need_Comma then ", (" else "(") & 
Label;
+                  Need_Comma := True;
+                  Elisp_Param (Skip => False);
+                  Elisp_Param (Skip => False);
+                  Result := Result  & ")";
+               else
+                  Elisp_Param (Skip => True);
+                  Elisp_Param (Skip => True);
+               end if;
+            end;
          end loop;
-         if Count = 1 then
+         if Count = 0 then
+            return "";
+         elsif Count = 1 then
+               Nonterm_Needed := True;
             return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
          else
+               Nonterm_Needed := True;
             return " (Parse_Data, Tree, Nonterm, Tokens, (" & (-Result) & "))";
          end if;
       exception
@@ -353,25 +451,36 @@ is
       function Face_Mark_Params (Params : in String) return String
       is
          --  Params is a vector of pairs: [1 prefix 3 suffix ...]
+         --  The token_number can be a label; faces are "nil" or an elisp name.
          --  Result: ((1, Prefix), (3, Suffix), ...)
          use Ada.Strings.Maps;
          Delim : constant Character_Set := To_Set ("]") or Blank_Set;
 
-         Last       : Integer          := Index_Non_Blank (Params); -- skip [
+         Last       : Integer := Index_Non_Blank (Params); -- skip [
          First      : Integer;
          Result     : Unbounded_String;
-         Need_Comma : Boolean          := False;
-         Count      : Integer          := 0;
+         Need_Comma : Boolean := False;
+         Count      : Integer := 0;
+         Skip       : Boolean;
       begin
          loop
             Last := Index_Non_Blank (Params, Last + 1);
 
             exit when Params (Last) = ']' or Params (Last) = ')';
 
-            Count  := Count + 1;
-            First  := Last;
-            Last   := Index (Params, Delim, First);
-            Result := Result & (if Need_Comma then ", (" else "(") & Params 
(First .. Last - 1);
+            First := Last;
+            Last  := Index (Params, Delim, First);
+            declare
+               Label : constant String := Params (First .. Last - 1);
+            begin
+               if 0 = Index (Label, Numeric, Outside) or else Label_Used 
(Label) then
+                  Count  := Count + 1;
+                  Skip   := False;
+                  Result := Result & (if Need_Comma then ", (" else "(") & 
Label;
+               else
+                  Skip := True;
+               end if;
+            end;
 
             if Params (Last) = ']' then
                Put_Error
@@ -382,15 +491,17 @@ is
 
             First  := Index_Non_Blank (Params, Last + 1);
             Last   := Index (Params, Delim, First);
-            Result := Result & ", " & Elisp_Name_To_Ada (Params (First .. Last 
- 1), False, 0) & ")";
-
-            Need_Comma := True;
+            if not Skip then
+               Result := Result & ", " & Elisp_Name_To_Ada (Params (First .. 
Last - 1), False, 0) & ")";
+               Need_Comma := True;
+            end if;
          end loop;
-         if Count = 1 then
-            return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
-         else
-            return " (Parse_Data, Tree, Nonterm, Tokens, (" & (-Result) & "))";
-         end if;
+         Nonterm_Needed := True;
+         return " (Parse_Data, Tree, Nonterm, Tokens, " &
+           (case Count is
+            when 0 => "(1 .. 0 => (1, Prefix))",
+            when 1 => "(1 => " & (-Result) & "))",
+            when others => "(" & (-Result) & "))");
       exception
       when E : others =>
          Put_Error
@@ -425,6 +536,7 @@ is
 
             Need_Comma := True;
          end loop;
+         Nonterm_Needed := True;
          if Count = 1 then
             return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
          else
@@ -447,11 +559,13 @@ is
          --
          --  - an integer; copy to output
          --
-         --  - a symbol; convert to Ada name syntax
+         --  - a symbol; convert to Ada name syntax, except 'nil' => None
          --
          --  - a lisp function call with arbitrary args; convert to 
Indent_Param type
          --
          --  - a vector with two elements [code_indent comment_indent]; 
convert to Indent_Pair.
+         --
+         --  - a cons of a token label with any of the above.
 
          use Ada.Strings.Maps;
          use Ada.Containers;
@@ -466,7 +580,7 @@ is
          Need_Comma    : Boolean         := False;
          Param_Count   : Count_Type      := 0;            -- in Params
 
-         function Indent_Label (Elisp_Name : in String) return String
+         function Indent_Function (Elisp_Name : in String) return String
          is begin
             if    Elisp_Name = "wisi-anchored"   then return "Anchored_0";
             elsif Elisp_Name = "wisi-anchored%"  then return "Anchored_1";
@@ -474,8 +588,9 @@ is
             elsif Elisp_Name = "wisi-anchored*"  then return "Anchored_3";
             elsif Elisp_Name = "wisi-anchored*-" then return "Anchored_4";
             elsif Elisp_Name = "wisi-hanging"    then return "Hanging_0";
-            elsif Elisp_Name = "wisi-hanging%"   then return "Hanging_1";
-            elsif Elisp_Name = "wisi-hanging%-"  then return "Hanging_2";
+            elsif Elisp_Name = "wisi-hanging-"   then return "Hanging_1";
+            elsif Elisp_Name = "wisi-hanging%"   then return "Hanging_2";
+            elsif Elisp_Name = "wisi-hanging%-"  then return "Hanging_3";
             else
                Put_Error
                  (Error_Message
@@ -483,7 +598,20 @@ is
                     Elisp_Name & "'"));
                return "";
             end if;
-         end Indent_Label;
+         end Indent_Function;
+
+         function Check_Cons return Integer
+         is
+            --  Params (Last) = '('; check for "(label .", return label'last
+            Blank : constant Integer := Index (Params, " ", Last);
+         begin
+            if Blank = 0 then return 0; end if;
+            if Params'Last > Blank + 1 and then Params (Blank + 1) = '.' then
+               return Blank - 1;
+            else
+               return 0;
+            end if;
+         end Check_Cons;
 
          function Ensure_Simple_Indent (Item : in String) return String
          is begin
@@ -494,6 +622,9 @@ is
                --  Anchored or Language
                return Item;
 
+            elsif Item = "nil" then
+               return "(Label => None)";
+
             else
                --  simple integer
                return "(Int, " & Item & ")";
@@ -507,6 +638,8 @@ is
             --
             --  Handles this syntax:
             --
+            --  nil => nil
+            --
             --  integer literal:
             --  2 => 2
             --  -1 => -1
@@ -599,7 +732,7 @@ is
 
                else
                   --  wisi lisp function call
-                  Function_Name := +Indent_Label (-Function_Name);
+                  Function_Name := +Indent_Function (-Function_Name);
                   if Length (Function_Name) = 0 then
                      --  not a recognized function
                      Last := 1 + Index (Params, ")", Last);
@@ -621,10 +754,15 @@ is
                end if;
 
             else
-               --  Assume it is a language-specific integer indent option, 
like "ada-indent",
-               --  declared in Language_Runtime_Package, which is use-visible.
+               --  Assume it is 'nil' or a language-specific integer indent 
option,
+               --  like "ada-indent", declared in Language_Runtime_Package, 
which is
+               --  use-visible.
                Last  := Index (Params, Delim, First);
-               return Elisp_Name_To_Ada (Params (First .. Last - 1), False, 0);
+               if Params (First .. Last - 1) = "nil" then
+                  return "nil";
+               else
+                  return Elisp_Name_To_Ada (Params (First .. Last - 1), False, 
0);
+               end if;
             end if;
          exception
          when E : others =>
@@ -634,6 +772,14 @@ is
             return "";
          end Expression;
 
+         procedure Skip_Expression (Param_First : in Integer)
+         is
+            Junk : constant String := Expression (Param_First);
+            pragma Unreferenced (Junk);
+         begin
+            null;
+         end Skip_Expression;
+
          function Ensure_Indent_Param (Item : in String) return String
          is begin
             --  Return an aggregate for Indent_Param. Item can be anything
@@ -649,56 +795,134 @@ is
                --  Anchored or Language
                return "(Simple, " & Item & ")";
 
+            elsif Item = "nil" then
+               return "(Simple, (Label => None))";
+
             else
                --  simple integer
                return "(Simple, (Int, " & Item & "))";
             end if;
          end Ensure_Indent_Param;
 
-      begin
-         loop
-            if Params (Last) /= ']' then
-               Last := Index_Non_Blank (Params, Last + 1);
-            end if;
-
-            exit when Params (Last) = ']';
-
-            if Need_Comma then
-               Result := Result & ", ";
-            else
-               Need_Comma := True;
-            end if;
-
+         procedure One_Param (Prefix : in Boolean := False; Skip : in Boolean 
:= False)
+         is
+            procedure Comma
+            is begin
+               if Need_Comma then
+                  if not Prefix then
+                     Result := Result & ", ";
+                  end if;
+               else
+                  Need_Comma := True;
+               end if;
+            end Comma;
+         begin
             case Params (Last) is
             when '(' =>
-               Result := Result & "(False, " & Ensure_Indent_Param (Expression 
(Last)) & ')';
+               --  cons or function
+               declare
+                  Label_Last : constant Integer := Check_Cons;
+               begin
+                  if Label_Last > 0 then
+                     declare
+                        Label : constant String := Params (Last + 1 .. 
Label_Last);
+                     begin
+                        Last := Index_Non_Blank (Params, Label_Last + 3);
+                        if Label_Used (Label) then
+                           Comma;
+                           Result := Result & Label & " => ";
+                           One_Param (Prefix => True);
+                        else
+                           --  This token is not present in this RHS; skip 
this param
+                           One_Param (Skip => True);
+                        end if;
+                        if Params (Last) /= ')' then
+                           Put_Error
+                             (Error_Message
+                                (Input_Data.Grammar_Lexer.File_Name,
+                                 RHS.Source_Line, "invalid indent syntax; 
missing ')'"));
+                        end if;
+                        Last := Last + 1;
+                     end;
+                  else
+                     if Skip then
+                        Skip_Expression (Last);
+                     else
+                        Comma;
+                        Result := Result & "(False, " & Ensure_Indent_Param 
(Expression (Last)) & ')';
+                     end if;
+                  end if;
+               end;
 
             when '[' =>
                --  vector
-               Result := Result & "(True, " & Ensure_Indent_Param (Expression 
(Last + 1));
-               Result := Result & ", " & Ensure_Indent_Param (Expression (Last 
+ 1)) & ')';
+               if Skip then
+                  Skip_Expression (Last + 1);
+                  Skip_Expression (Last + 1);
+               else
+                  Comma;
+                  Result := Result & "(True, " & Ensure_Indent_Param 
(Expression (Last + 1));
+                  Result := Result & ", " & Ensure_Indent_Param (Expression 
(Last + 1)) & ')';
+               end if;
                if Params (Last) /= ']' then
                   Put_Error
                     (Error_Message
-                       (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"invalid indent syntax"));
+                       (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"indent missing ']'"));
                end if;
                Last := Last + 1;
 
             when others =>
                --  integer or symbol
-               Result := Result & "(False, " & Ensure_Indent_Param (Expression 
(Last)) & ')';
-
+               if Skip then
+                  Skip_Expression (Last);
+               else
+                  Comma;
+                  Result := Result & "(False, " & Ensure_Indent_Param 
(Expression (Last)) & ')';
+               end if;
             end case;
+         end One_Param;
+
+      begin
+         loop
+            if Params (Last) /= ']' then
+               Last := Index_Non_Blank (Params, Last + 1);
+               if Last = 0 then
+                  Put_Error (Error_Message 
(Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, "indent missing ']'"));
+                  return -Result;
+               end if;
+            end if;
+
+            exit when Params (Last) = ']';
+
+            One_Param;
+
             Param_Count := Param_Count + 1;
          end loop;
 
+         --  In translated EBNF, token counts vary in each RHS; require each
+         --  parameter to be labeled if any are, both for catching errors, and
+         --  becase that would produce mixed positional and named association
+         --  in the Ada action subprogram.
          if Param_Count /= RHS.Tokens.Length then
-            Put_Error
-              (Error_Message
-                 (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, "indent 
parameters count of" & Count_Type'Image
-                    (Param_Count) & " /= production token count of" & 
Count_Type'Image (RHS.Tokens.Length)));
+            if Labels.Length = 0 then
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Image (Prod_ID) &
+                       ": indent parameters count of" & Count_Type'Image 
(Param_Count) &
+                       " /= production token count of" & Count_Type'Image 
(RHS.Tokens.Length)));
+
+            elsif Count_Label_Needed /= RHS.Tokens.Length then
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Image (Prod_ID) &
+                       ": indent parameter(s) not labeled"));
+            else
+               --  all parameters labeled
+               null;
+            end if;
          end if;
 
+         Nonterm_Needed := True;
          if Param_Count = 1 then
             Result := Prefix & "1 => " & Result;
          else
@@ -711,11 +935,29 @@ is
       function Merge_Names_Params (Params : in String) return String
       is
          --  Input looks like "1 2)"
-         First  : constant Integer := Index_Non_Blank (Params);
-         Second : constant Integer := Index (Params, Blank_Set, First);
+         First             : constant Integer := Index_Non_Blank (Params);
+         Second            : constant Integer := Index (Params, Blank_Set, 
First);
+         Label_First       : constant String  := Params (First .. Second - 1);
+         Label_Used_First  : constant Boolean := 0 = Index (Label_First, 
Numeric, Outside) or else
+           Label_Used (Label_First);
+         Label_Second      : constant String  := Params (Second + 1 .. 
Params'Last - 1);
+         Label_Used_Second : constant Boolean := 0 = Index (Label_Second, 
Numeric, Outside) or else
+           Label_Used (Label_Second);
       begin
-         return " (Nonterm, Tokens, " & Params (First .. Second - 1) & ',' &
-           Params (Second .. Params'Last);
+         Nonterm_Needed := True;
+
+         if Label_Used_First and Label_Used_Second then
+            return " (Nonterm, Tokens, " & Label_First & ", " & Label_Second & 
")";
+
+         elsif (not Label_Used_First) and Label_Used_Second then
+            --  A copied EBNF RHS; see subprograms.wy Name
+            return " (Nonterm, Tokens, " & Label_Second & ")";
+         else
+            Put_Error
+              (Error_Message
+                 (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"merge_names token label error"));
+            return " (Nonterm, Tokens)";
+         end if;
       end Merge_Names_Params;
 
       function Match_Names_Params (Params : in String) return String
@@ -732,7 +974,49 @@ is
             else "False") & ")";
       end Match_Names_Params;
 
-      procedure Translate_Line (Line : in String)
+      function Language_Action_Params (Params : in String; Action_Name : in 
String) return String
+      is
+         --  Input looks like: [1 2 ...])
+         Result      : Unbounded_String;
+         Need_Comma  : Boolean := False;
+         Param_Count : Integer := 0;
+         First       : Integer;
+         Last        : Integer := Params'First; --  '['
+      begin
+         loop
+            First := Index_Non_Blank (Params, Last + 1);
+            Last  := Index (Params, Space_Paren_Set, First);
+            declare
+               Label : constant String  := Params (First .. Last - 1);
+            begin
+               if 0 = Index (Label, Numeric, Outside) or else Label_Used 
(Label) then
+                  Param_Count := Param_Count + 1;
+                  if Need_Comma then
+                     Result := Result & ", ";
+                  else
+                     Need_Comma := True;
+                  end if;
+                  Result := Result & Label;
+               end if;
+               exit when Params (Last) = ']';
+               if Last = Params'Last then
+                  Put_Error
+                    (Error_Message
+                       (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Action_Name & " missing ']'"));
+                  exit;
+               end if;
+            end;
+         end loop;
+         if Param_Count = 0 then
+            return "";
+         elsif Param_Count = 1 then
+            return "(1 => " & (-Result) & ")";
+         else
+            return "(" & (-Result) & ")";
+         end if;
+      end Language_Action_Params;
+
+      procedure Translate_Sexp (Line : in String)
       is
          Last       : constant Integer := Index (Line, Blank_Set);
          Elisp_Name : constant String  := Line (Line'First + 1 .. (if Last = 0 
then Line'Last else Last) - 1);
@@ -768,56 +1052,101 @@ is
          --  wisi action/check functions, in same order as typically used in
          --  .wy files; Navigate, Face, Indent, Check.
          if Elisp_Name = "wisi-statement-action" then
-            Navigate_Lines.Append
-              (Elisp_Name_To_Ada (Elisp_Name, False, 5) &
-                 Statement_Params (Line (Last + 1 .. Line'Last)) & ";");
+            declare
+               Params : constant String := Statement_Params (Line (Last + 1 .. 
Line'Last));
+            begin
+               if Params'Length > 0 then
+                  Navigate_Lines.Append (Elisp_Name_To_Ada (Elisp_Name, False, 
5) & Params & ";");
+               end if;
+            end;
+
+         elsif Elisp_Name = "wisi-name-action" then
+            declare
+               First : constant Integer := Index_Non_Blank (Line, Last + 1);
+               Last  : constant Integer := Index (Line, Space_Paren_Set, 
First);
+               Label : constant String  := Line (First .. Last - 1);
+            begin
+               if 0 = Index (Label, Numeric, Outside) or else Label_Used 
(Label) then
+                  Nonterm_Needed := True;
+                  Navigate_Lines.Append
+                    ("Name_Action (Parse_Data, Tree, Nonterm, Tokens, " & Line 
(First .. Line'Last) & ";");
+               end if;
+            end;
 
          elsif Elisp_Name = "wisi-containing-action" then
-            Navigate_Lines.Append
-              (Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
-                 Containing_Params (Line (Last + 1 .. Line'Last)) & ";");
+            declare
+               Params : constant String := Containing_Params (Line (Last + 1 
.. Line'Last));
+            begin
+               if Params'Length > 0 then
+                  Navigate_Lines.Append (Elisp_Name_To_Ada (Elisp_Name, False, 
Trim => 5) & Params & ";");
+               end if;
+            end;
 
          elsif Elisp_Name = "wisi-motion-action" then
-            Navigate_Lines.Append
-              (Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
-                 Motion_Params (Line (Last + 1 .. Line'Last)) & ";");
+            declare
+               Params : constant String := Motion_Params (Line (Last + 1 .. 
Line'Last));
+            begin
+               if Params'Length > 0 then
+                  Navigate_Lines.Append (Elisp_Name_To_Ada (Elisp_Name, False, 
Trim => 5) & Params & ";");
+               end if;
+            end;
 
          elsif Elisp_Name = "wisi-face-apply-action" then
             Assert_Face_Empty;
-               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
-                 Face_Apply_Params (Line (Last + 1 .. Line'Last)) & ";";
+            declare
+               Params : constant String := Face_Apply_Params (Line (Last + 1 
.. Line'Last));
+            begin
+               if Params'Length > 0 then
+                  Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 
5) & Params & ";";
+               end if;
+            end;
 
          elsif Elisp_Name = "wisi-face-apply-list-action" then
             Assert_Face_Empty;
-               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
-                 Face_Apply_Params (Line (Last + 1 .. Line'Last)) & ";";
+            declare
+               Params : constant String := Face_Apply_Params (Line (Last + 1 
.. Line'Last));
+            begin
+               if Params'Length > 0 then
+                  Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 
5) & Params & ";";
+               end if;
+            end;
 
          elsif Elisp_Name = "wisi-face-mark-action" then
             Assert_Face_Empty;
-               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
-                 Face_Mark_Params (Line (Last + 1 .. Line'Last)) & ";";
+            declare
+               Params : constant String := Face_Mark_Params (Line (Last + 1 .. 
Line'Last));
+            begin
+               if Params'Length > 0 then
+                  Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 
5) & Params & ";";
+               end if;
+            end;
 
          elsif Elisp_Name = "wisi-face-remove-action" then
             Assert_Face_Empty;
-               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
-                 Face_Remove_Params (Line (Last + 1 .. Line'Last)) & ";";
+            declare
+               Params : constant String := Face_Remove_Params (Line (Last + 1 
.. Line'Last));
+            begin
+               if Params'Length > 0 then
+                  Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 
5) & Params & ";";
+               end if;
+            end;
 
          elsif Elisp_Name = "wisi-indent-action" then
             Assert_Indent_Empty;
-               Indent_Action_Line := +"Indent_Action_0" &
-                 Indent_Params (Line (Last + 1 .. Line'Last)) & ";";
+            Indent_Action_Line := +"Indent_Action_0" & Indent_Params (Line 
(Last + 1 .. Line'Last)) & ";";
 
          elsif Elisp_Name = "wisi-indent-action*" then
             Assert_Indent_Empty;
-               declare
-                  Temp : constant Integer := Index (Line, Blank_Set, Last + 1);
-               begin
-                  Indent_Action_Line := +"Indent_Action_1" &
-                    Indent_Params (Line (Temp + 1 .. Line'Last), Line (Last + 
1 .. Temp - 1) & ", ") & ";";
-               end;
+            declare
+               Temp : constant Integer := Index (Line, Blank_Set, Last + 1);
+            begin
+               Indent_Action_Line := +"Indent_Action_1" &
+                 Indent_Params (Line (Temp + 1 .. Line'Last), Line (Last + 1 
.. Temp - 1) & ", ") & ";";
+            end;
 
          elsif Elisp_Name = "wisi-propagate-name" then
             Assert_Check_Empty;
+            Nonterm_Needed := True;
             Check_Line := +"return " & Elisp_Name_To_Ada (Elisp_Name, False, 
Trim => 5) &
               " (Nonterm, Tokens, " & Line (Last + 1 .. Line'Last) & ";";
 
@@ -833,21 +1162,53 @@ is
 
          elsif Elisp_Name = "wisi-terminate-partial-parse" then
             Assert_Check_Empty;
+            Nonterm_Needed := True;
             Check_Line := +"return Terminate_Partial_Parse 
(Partial_Parse_Active, Partial_Parse_Byte_Goal, " &
               "Recover_Active, Nonterm);";
 
+         elsif Is_Present (Input_Data.Tokens.Actions, Elisp_Name) then
+            --  Language-specific action
+            declare
+               Item   : Elisp_Action_Type renames Input_Data.Tokens.Actions
+                 (Input_Data.Tokens.Actions.Find (+Elisp_Name));
+               Params : constant String := Language_Action_Params (Line (Last 
+ 1 .. Line'Last), Elisp_Name);
+               Code   : constant String := -Item.Ada_Name &
+                 " (Wisi.Parse_Data_Type'Class (User_Data), Tree, Tokens, " & 
Params & ");";
+            begin
+               if Params'Length > 0 then
+                  if "navigate" = -Item.Action_Label then
+                     Navigate_Lines.Append (Code);
+
+                  elsif "face" = -Item.Action_Label then
+                     Assert_Face_Empty;
+                     Face_Line := +Code;
+
+                  elsif "indent" = -Item.Action_Label then
+                     Assert_Indent_Empty;
+                     Indent_Action_Line := +Code;
+
+                  else
+                     Put_Error
+                       (Error_Message
+                          (Input_Data.Grammar_Lexer.File_Name, 
RHS.Source_Line, "unrecognized action label: '" &
+                             (-Item.Action_Label) & "'"));
+                  end if;
+
+                  --  else skip
+               end if;
+            end;
          else
             Put_Error
               (Error_Message
                  (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"unrecognized elisp action: '" &
                     Elisp_Name & "'"));
          end if;
-      end Translate_Line;
+      end Translate_Sexp;
 
    begin
       for Sexp of Sexps loop
          begin
-            Translate_Line (Sexp);
+            Translate_Sexp (Sexp);
          exception
          when E : Not_Found =>
             Put_Error
@@ -869,22 +1230,45 @@ is
             Unref_Nonterm : constant Boolean := 0 = Index (Check_Line, 
"Nonterm");
             Unref_Tokens  : constant Boolean := 0 = Index (Check_Line, 
"Tokens");
             Unref_Recover : constant Boolean := 0 = Index (Check_Line, 
"Recover_Active");
+            Need_Comma    : Boolean          := False;
          begin
-            if Unref_Lexer or Unref_Nonterm or Unref_Tokens or Unref_Recover 
then
+            if Unref_Lexer or Unref_Nonterm or Unref_Tokens or Unref_Recover or
+              (for some I of Label_Needed => I)
+            then
                Indent_Line ("is");
-               if Unref_Lexer then
-                  Indent_Line ("   pragma Unreferenced (Lexer);");
-               end if;
-               if Unref_Nonterm then
-                  Indent_Line ("   pragma Unreferenced (Nonterm);");
-               end if;
-               if Unref_Tokens then
-                  Indent_Line ("   pragma Unreferenced (Tokens);");
-               end if;
-               if Unref_Recover then
-                  Indent_Line ("   pragma Unreferenced (Recover_Active);");
+
+               Indent := Indent + 3;
+               if Unref_Lexer or Unref_Nonterm or Unref_Tokens or 
Unref_Recover then
+                  Indent_Start ("pragma Unreferenced (");
+
+                  if Unref_Lexer then
+                     Put ((if Need_Comma then ", " else "") & "Lexer");
+                     Need_Comma := True;
+                  end if;
+                  if Unref_Nonterm then
+                     Put ((if Need_Comma then ", " else "") & "Nonterm");
+                     Need_Comma := True;
+                  end if;
+                  if Unref_Tokens then
+                     Put ((if Need_Comma then ", " else "") & "Tokens");
+                     Need_Comma := True;
+                  end if;
+                  if Unref_Recover then
+                     Put ((if Need_Comma then ", " else "") & 
"Recover_Active");
+                     Need_Comma := True;
+                  end if;
+                  Put_Line (");");
                end if;
 
+               for I in Label_Needed'Range loop
+                  if Label_Needed (I) then
+                     Indent_Line
+                       (-Labels (I) & " : constant SAL.Peek_Type :=" &
+                          SAL.Peek_Type'Image (Find_Token_Index (I)) & ";");
+                  end if;
+               end loop;
+               Indent := Indent - 3;
+
                Indent_Line ("begin");
             else
                Indent_Line ("is begin");
@@ -900,8 +1284,24 @@ is
          Indent_Line ("  Nonterm   : in     
WisiToken.Syntax_Trees.Valid_Node_Index;");
          Indent_Line ("  Tokens    : in     
WisiToken.Syntax_Trees.Valid_Node_Index_Array)");
          Indent_Line ("is");
-         Indent_Start ("   Parse_Data : Wisi.Parse_Data_Type renames");
-         Put_Line (" Wisi.Parse_Data_Type (User_Data);");
+
+         Indent := Indent + 3;
+         Indent_Line ("Parse_Data : Wisi.Parse_Data_Type renames 
Wisi.Parse_Data_Type (User_Data);");
+
+         if not Nonterm_Needed then
+            --  Language_Action may not use this
+            Indent_Line ("pragma Unreferenced (Nonterm);");
+         end if;
+
+         for I in Label_Needed'Range loop
+            if Label_Needed (I) then
+               Indent_Line
+                 (-Labels (I) & " : constant SAL.Peek_Type :=" &
+                    SAL.Peek_Type'Image (Find_Token_Index (I)) & ";");
+            end if;
+         end loop;
+
+         Indent := Indent - 3;
          Indent_Line ("begin");
          Indent := Indent + 3;
 
@@ -965,6 +1365,7 @@ is
    procedure Create_Ada_Actions_Body
      (Action_Names : not null access WisiToken.Names_Array_Array;
       Check_Names  : not null access WisiToken.Names_Array_Array;
+      Label_Count  : in              Ada.Containers.Count_Type;
       Package_Name : in              String)
    is
       use Ada.Strings.Unbounded;
@@ -989,10 +1390,21 @@ is
       Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
       New_Line;
 
+      if Label_Count > 0 then
+         Put_Line ("with SAL;");
+      end if;
+
       Put_Line ("with Wisi; use Wisi;");
-      if Input_Data.Language_Params.Language_Runtime then
-         Put_Line ("with " & Language_Runtime_Package & "; use " & 
Language_Runtime_Package & ";");
-         --  For language-specific names in actions, checks.
+      if Input_Data.Language_Params.Use_Language_Runtime then
+         declare
+            Pkg : constant String :=
+              (if -Input_Data.Language_Params.Language_Runtime_Name = ""
+               then Default_Language_Runtime_Package
+               else -Input_Data.Language_Params.Language_Runtime_Name);
+         begin
+            --  For language-specific names in actions, checks.
+            Put_Line ("with " & Pkg & "; use " & Pkg & ";");
+         end;
       end if;
 
       case Common_Data.Interface_Kind is
@@ -1023,26 +1435,26 @@ is
          --  No need for a Token_Cursor here, since we only need the
          --  nonterminals.
          declare
-            LHS_ID     : constant WisiToken.Token_ID := Find_Token_ID 
(Generate_Data, -Rule.Left_Hand_Side);
-            Prod_Index : Integer                     := 0; -- Semantic_Action 
defines Prod_Index as zero-origin
+            LHS_ID    : constant WisiToken.Token_ID := Find_Token_ID 
(Generate_Data, -Rule.Left_Hand_Side);
+            RHS_Index : Integer                     := 0; -- Semantic_Action 
defines RHS_Index as zero-origin
          begin
             for RHS of Rule.Right_Hand_Sides loop
                if Length (RHS.Action) > 0 then
                   declare
-                     Name : constant String := Action_Names 
(LHS_ID)(Prod_Index).all;
+                     Name : constant String := Action_Names 
(LHS_ID)(RHS_Index).all;
                   begin
-                     Create_Ada_Action (Name, RHS, RHS.Action, Check => False);
+                     Create_Ada_Action (Name, RHS, (LHS_ID, RHS_Index), 
RHS.Action, Rule.Labels, Check => False);
                   end;
                end if;
 
                if Length (RHS.Check) > 0 then
                   declare
-                     Name : constant String := Check_Names 
(LHS_ID)(Prod_Index).all;
+                     Name : constant String := Check_Names 
(LHS_ID)(RHS_Index).all;
                   begin
-                     Create_Ada_Action (Name, RHS, RHS.Check, Check => True);
+                     Create_Ada_Action (Name, RHS, (LHS_ID, RHS_Index), 
RHS.Check, Rule.Labels, Check => True);
                   end;
                end if;
-               Prod_Index := Prod_Index + 1;
+               RHS_Index := RHS_Index + 1;
             end loop;
          end;
       end loop;
@@ -1070,7 +1482,9 @@ is
       Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
       New_Line;
 
-      Put_Line ("with " & Actions_Package_Name & "; use " & 
Actions_Package_Name & ";");
+      if Input_Data.Action_Count > 0 or Input_Data.Check_Count > 0 then
+         Put_Line ("with " & Actions_Package_Name & "; use " & 
Actions_Package_Name & ";");
+      end if;
 
       case Common_Data.Lexer is
       when None | Elisp_Lexer =>
@@ -1084,9 +1498,7 @@ is
 
       case Common_Data.Generate_Algorithm is
       when LR_Generate_Algorithm =>
-         if Tuple.Text_Rep then
-            Put_Line ("with WisiToken.Productions;");
-         end if;
+         null;
 
       when Packrat_Generate_Algorithm =>
          Put_Line ("with WisiToken.Parse;");
@@ -1232,6 +1644,19 @@ is
       Output_Elisp_Common.Indent_Name_Table
         (Output_File_Name_Root, "process-face-table", Input_Data.Tokens.Faces);
 
+      --  We need the elisp lexer for some operations
+      if Elisp_Tokens.Keywords.Length > 0 then
+         New_Line;
+         Output_Elisp_Common.Indent_Keyword_Table
+           (Output_File_Name_Root, "elisp", Elisp_Tokens.Keywords, 
Ada.Strings.Unbounded.To_String'Access);
+      end if;
+      if Elisp_Tokens.Tokens.Length > 0 then
+         New_Line;
+         Output_Elisp_Common.Indent_Token_Table
+           (Output_File_Name_Root, "elisp", Elisp_Tokens.Tokens, 
Ada.Strings.Unbounded.To_String'Access);
+      end if;
+
+      New_Line;
       Put_Line ("(provide '" & Output_File_Name_Root & "-process)");
       Set_Output (Standard_Output);
       Close (File);
@@ -1467,7 +1892,11 @@ begin
          when Module  => "_Module") &
         Gen_Alg_Name & "_Main";
    begin
-      Create_Ada_Actions_Body (Generate_Data.Action_Names, 
Generate_Data.Check_Names, Actions_Package_Name);
+      if Input_Data.Action_Count > 0 or Input_Data.Check_Count > 0 then
+         --  We typically have no actions when just getting started with a new 
language.
+         Create_Ada_Actions_Body
+           (Generate_Data.Action_Names, Generate_Data.Check_Names, 
Input_Data.Label_Count, Actions_Package_Name);
+      end if;
 
       Create_Ada_Actions_Spec
         (Output_File_Name => Output_File_Name_Root &
diff --git a/wisitoken-bnf-output_elisp.adb b/wisitoken-bnf-output_elisp.adb
index 8007bd2..ff52449 100644
--- a/wisitoken-bnf-output_elisp.adb
+++ b/wisitoken-bnf-output_elisp.adb
@@ -2,7 +2,7 @@
 --
 --  Output Elisp code implementing the grammar defined by the parameters.
 --
---  Copyright (C) 2012 - 2015, 2017, 2018 Free Software Foundation, Inc.
+--  Copyright (C) 2012 - 2015, 2017 - 2019 Free Software Foundation, Inc.
 --
 --  The WisiToken package is free software; you can redistribute it
 --  and/or modify it under terms of the GNU General Public License as
@@ -216,7 +216,7 @@ is
          for RHS of Rule.Right_Hand_Sides loop
             Put ("       ((");
             for Token of RHS.Tokens loop
-               Put (Token & " ");
+               Put (-Token.Identifier & " ");
             end loop;
             if Length (RHS.Action) = 0 then
                Put (")");
diff --git a/wisitoken-bnf-output_elisp_common.adb 
b/wisitoken-bnf-output_elisp_common.adb
index 9d46b4f..a0b7158 100644
--- a/wisitoken-bnf-output_elisp_common.adb
+++ b/wisitoken-bnf-output_elisp_common.adb
@@ -2,7 +2,7 @@
 --
 --  See spec
 --
---  Copyright (C) 2012, 2013, 2015, 2017, 2018 Free Software Foundation, Inc.
+--  Copyright (C) 2012, 2013, 2015, 2017 - 2019 Free Software Foundation, Inc.
 --
 --  This program is free software; you can redistribute it and/or
 --  modify it under terms of the GNU General Public License as
@@ -87,6 +87,20 @@ package body WisiToken.BNF.Output_Elisp_Common is
       use Ada.Strings.Unbounded;
       use Ada.Text_IO;
       use WisiToken.Generate;
+
+      function To_Double_Quotes (Item : in String) return String
+      is
+         Result : String := Item;
+      begin
+         if Result (Result'First) = ''' then
+            Result (Result'First) := '"';
+         end if;
+         if Result (Result'Last) = ''' then
+            Result (Result'Last) := '"';
+         end if;
+         return Result;
+      end To_Double_Quotes;
+
    begin
       Indent_Line ("(defconst " & Output_File_Root & "-" & Label & 
"-token-table-raw");
       Indent_Line ("  '(");
@@ -111,7 +125,7 @@ package body WisiToken.BNF.Output_Elisp_Common is
                      --  value not used by elisp
                      Indent_Line ("(" & Image (Token.Name) & " . """")");
                   else
-                     Indent_Line ("(" & Image (Token.Name) & " . " & 
(-Token.Value) & ")");
+                     Indent_Line ("(" & Image (Token.Name) & " . " & 
To_Double_Quotes (-Token.Value) & ")");
                   end if;
                end if;
             end loop;
@@ -139,7 +153,6 @@ package body WisiToken.BNF.Output_Elisp_Common is
       end loop;
       Indent_Line ("])");
       Indent := Indent - 3;
-      New_Line;
    end Indent_Name_Table;
 
 end WisiToken.BNF.Output_Elisp_Common;
diff --git a/wisitoken-bnf.adb b/wisitoken-bnf.adb
index 22492f0..dc37d56 100644
--- a/wisitoken-bnf.adb
+++ b/wisitoken-bnf.adb
@@ -2,7 +2,7 @@
 --
 --  see spec
 --
---  Copyright (C) 2012 - 2015, 2017, 2018 Free Software Foundation, Inc.
+--  Copyright (C) 2012 - 2015, 2017 - 2019 Free Software Foundation, Inc.
 --
 --  This program is free software; you can redistribute it and/or
 --  modify it under terms of the GNU General Public License as
@@ -39,6 +39,16 @@ package body WisiToken.BNF is
       Free (Prev);
    end Add;
 
+   function To_Generate_Algorithm (Item : in String) return Generate_Algorithm
+   is begin
+      for I in Generate_Algorithm loop
+         if To_Lower (Generate_Algorithm_Image (I).all) = To_Lower (Item) then
+            return I;
+         end if;
+      end loop;
+      raise User_Error with "invalid generate algorithm name: '" & Item & "'";
+   end To_Generate_Algorithm;
+
    function To_Output_Language (Item : in String) return Output_Language
    is begin
       for I in Output_Language loop
@@ -176,6 +186,13 @@ package body WisiToken.BNF is
       raise Not_Found;
    end Value;
 
+   function Is_Present (List : in Elisp_Action_Maps.Map; Name : in String) 
return Boolean
+   is
+      use Elisp_Action_Maps;
+   begin
+      return No_Element /= List.Find (+Name);
+   end Is_Present;
+
    function Count (Tokens : in Token_Lists.List) return Integer
    is
       Result : Integer := 0;
diff --git a/wisitoken-bnf.ads b/wisitoken-bnf.ads
index 3d46c45..3532de7 100644
--- a/wisitoken-bnf.ads
+++ b/wisitoken-bnf.ads
@@ -32,9 +32,11 @@ pragma License (Modified_GPL);
 with Ada.Characters.Handling;
 with Ada.Containers.Doubly_Linked_Lists;
 with Ada.Containers.Indefinite_Doubly_Linked_Lists;
+with Ada.Containers.Ordered_Maps;
+with Ada.Containers.Vectors;
 with Ada.Strings.Unbounded;
 with Ada.Unchecked_Deallocation;
-with WisiToken;
+with WisiToken.Parse.LR;
 package WisiToken.BNF is
 
    --  See also WisiToken exceptions
@@ -47,14 +49,18 @@ package WisiToken.BNF is
    subtype LR_Generate_Algorithm is Generate_Algorithm range LALR .. LR1;
    subtype Packrat_Generate_Algorithm is Generate_Algorithm range Packrat_Gen 
.. Packrat_Proc;
 
-   Generate_Algorithm_Image : constant array (Valid_Generate_Algorithm) of 
access constant String :=
-     (LALR         => new String'("LALR"),
+   Generate_Algorithm_Image : constant array (Generate_Algorithm) of 
String_Access_Constant :=
+     (None         => new String'("None"),
+      LALR         => new String'("LALR"),
       LR1          => new String'("LR1"),
       Packrat_Gen  => new String'("Packrat_Gen"),
       Packrat_Proc => new String'("Packrat_Proc"),
       External     => new String'("External"));
    --  Suitable for Ada package names.
 
+   function To_Generate_Algorithm (Item : in String) return Generate_Algorithm;
+   --  Raises User_Error for invalid Item
+
    type Generate_Algorithm_Set is array (Generate_Algorithm) of Boolean;
    type Generate_Algorithm_Set_Access is access Generate_Algorithm_Set;
 
@@ -63,7 +69,7 @@ package WisiToken.BNF is
    --  _Lang to avoid colliding with the standard package Ada and
    --  WisiToken packages named *.Ada. In the grammar file, they
    --  are named by (case insensitive):
-   Output_Language_Image : constant array (Output_Language) of access constant 
String :=
+   Output_Language_Image : constant array (Output_Language) of 
String_Access_Constant :=
      (Ada_Lang       => new String'("Ada"),
       Ada_Emacs_Lang => new String'("Ada_Emacs"),
       Elisp_Lang     => new String'("elisp"));
@@ -76,7 +82,7 @@ package WisiToken.BNF is
    --  We append "_Lexer" to these names to avoid colliding with the
    --  similarly-named WisiToken packages. In the grammar file, they
    --  are named by:
-   Lexer_Image : constant array (Lexer_Type) of access constant String :=
+   Lexer_Image : constant array (Lexer_Type) of String_Access_Constant :=
      (None        => new String'("none"),
       Elisp_Lexer => new String'("elisp"),
       re2c_Lexer  => new String'("re2c"));
@@ -93,11 +99,11 @@ package WisiToken.BNF is
    subtype Valid_Interface is Interface_Type range Process .. Module;
 
    type Generate_Tuple is record
-      Gen_Alg        : Valid_Generate_Algorithm;
-      Out_Lang       : Output_Language;
-      Lexer          : Lexer_Type     := None;
-      Interface_Kind : Interface_Type := None;
-      Text_Rep       : Boolean        := False;
+      Gen_Alg        : Generate_Algorithm := None;
+      Out_Lang       : Output_Language    := Ada_Lang;
+      Lexer          : Lexer_Type         := None;
+      Interface_Kind : Interface_Type     := None;
+      Text_Rep       : Boolean            := False;
    end record;
 
    type Generate_Set is array (Natural range <>) of Generate_Tuple;
@@ -110,16 +116,21 @@ package WisiToken.BNF is
 
    package String_Lists is new Ada.Containers.Indefinite_Doubly_Linked_Lists 
(String);
 
+   package String_Arrays is new SAL.Gen_Unbounded_Definite_Vectors
+     (WisiToken.Identifier_Index, Ada.Strings.Unbounded.Unbounded_String,
+      Default_Element => Ada.Strings.Unbounded.Null_Unbounded_String);
+
    type Language_Param_Type is record
       --  Set by grammar file declarations or command line options. Error
       --  recover parameters are in McKenzie_Recover_Param_Type below.
-      Case_Insensitive              : Boolean := False;
-      Embedded_Quote_Escape_Doubled : Boolean := False;
-      End_Names_Optional_Option     : Ada.Strings.Unbounded.Unbounded_String;
-      Language_Runtime              : Boolean := True;
-      Declare_Enums                 : Boolean := True;
-      Error_Recover                 : Boolean := False;
-      Start_Token                   : Ada.Strings.Unbounded.Unbounded_String;
+      Case_Insensitive          : Boolean := False;
+      End_Names_Optional_Option : Ada.Strings.Unbounded.Unbounded_String;
+      Use_Language_Runtime      : Boolean := True;
+      Language_Runtime_Name     : Ada.Strings.Unbounded.Unbounded_String;
+      Declare_Enums             : Boolean := True;
+      Error_Recover             : Boolean := False;
+      Start_Token               : Ada.Strings.Unbounded.Unbounded_String;
+      Partial_Recursion         : Boolean := False;
    end record;
 
    type Raw_Code_Location is
@@ -174,26 +185,46 @@ package WisiToken.BNF is
    end record;
 
    package String_Pair_Lists is new Ada.Containers.Doubly_Linked_Lists 
(String_Pair_Type);
-
    function Is_Present (List : in String_Pair_Lists.List; Name : in String) 
return Boolean;
    function Value (List : in String_Pair_Lists.List; Name : in String) return 
String;
 
+   type Elisp_Action_Type is record
+      --  Elisp name is the key
+      Action_Label : Ada.Strings.Unbounded.Unbounded_String;
+      Ada_Name     : Ada.Strings.Unbounded.Unbounded_String;
+   end record;
+
+   package Elisp_Action_Maps is new Ada.Containers.Ordered_Maps
+     (Ada.Strings.Unbounded.Unbounded_String, Elisp_Action_Type, 
Ada.Strings.Unbounded."<");
+
+   function Is_Present (List : in Elisp_Action_Maps.Map; Name : in String) 
return Boolean;
+
    type McKenzie_Recover_Param_Type is record
       Source_Line : WisiToken.Line_Number_Type := 
WisiToken.Invalid_Line_Number;
       --  Of the %mckenzie_cost_default declaration; we assume the others
       --  are near.
 
-      Default_Insert          : Natural               := 0;
-      Default_Delete_Terminal : Natural               := 0;
-      Default_Push_Back       : Natural               := 0;
-      Delete                  : String_Pair_Lists.List;
-      Insert                  : String_Pair_Lists.List;
-      Push_Back               : String_Pair_Lists.List;
-      Ignore_Check_Fail       : Natural               := 0;
-      Cost_Limit              : Natural               := Integer'Last;
-      Check_Limit             : WisiToken.Token_Index := 
WisiToken.Token_Index'Last;
-      Check_Delta_Limit       : Natural               := Integer'Last;
-      Enqueue_Limit           : Natural               := Integer'Last;
+      Default_Insert              : Natural               := 0;
+      Default_Delete_Terminal     : Natural               := 0;
+      Default_Push_Back           : Natural               := 0; -- also 
default for undo_reduce
+      Delete                      : String_Pair_Lists.List;
+      Insert                      : String_Pair_Lists.List;
+      Push_Back                   : String_Pair_Lists.List;
+      Undo_Reduce                 : String_Pair_Lists.List;
+      Minimal_Complete_Cost_Delta : Integer               :=
+        WisiToken.Parse.LR.Default_McKenzie_Param.Minimal_Complete_Cost_Delta;
+      Fast_Forward                : Integer               :=
+        WisiToken.Parse.LR.Default_McKenzie_Param.Fast_Forward;
+      Matching_Begin              : Integer               :=
+        WisiToken.Parse.LR.Default_McKenzie_Param.Matching_Begin;
+      Ignore_Check_Fail           : Natural               :=
+        WisiToken.Parse.LR.Default_McKenzie_Param.Ignore_Check_Fail;
+      Check_Limit                 : WisiToken.Token_Index :=
+        WisiToken.Parse.LR.Default_McKenzie_Param.Check_Limit;
+      Check_Delta_Limit           : Natural               :=
+        WisiToken.Parse.LR.Default_McKenzie_Param.Check_Delta_Limit;
+      Enqueue_Limit               : Natural               :=
+        WisiToken.Parse.LR.Default_McKenzie_Param.Enqueue_Limit;
    end record;
 
    type Token_Kind_Type is record
@@ -231,8 +262,17 @@ package WisiToken.BNF is
 
    package Conflict_Lists is new Ada.Containers.Doubly_Linked_Lists (Conflict);
 
+   type Labeled_Token is record
+      Label      : Ada.Strings.Unbounded.Unbounded_String;
+      Identifier : Ada.Strings.Unbounded.Unbounded_String;
+   end record;
+
+   package Labeled_Token_Arrays is new Ada.Containers.Vectors 
(Positive_Index_Type, Labeled_Token);
+   --  Index matches Syntax_Trees.Valid_Node_Index_Array, used for Tokens
+   --  in call to post parse grammar action.
+
    type RHS_Type is record
-      Tokens      : String_Lists.List;
+      Tokens      : Labeled_Token_Arrays.Vector;
       Action      : Ada.Strings.Unbounded.Unbounded_String;
       Check       : Ada.Strings.Unbounded.Unbounded_String;
       Source_Line : WisiToken.Line_Number_Type := 
WisiToken.Invalid_Line_Number;
@@ -242,6 +282,7 @@ package WisiToken.BNF is
    type Rule_Type is record
       Left_Hand_Side   : aliased Ada.Strings.Unbounded.Unbounded_String;
       Right_Hand_Sides : RHS_Lists.List;
+      Labels           : String_Arrays.Vector;
       Source_Line      : WisiToken.Line_Number_Type;
    end record;
 
@@ -257,13 +298,18 @@ package WisiToken.BNF is
       --  Rules included here because they define the nonterminal tokens, as
       --  well as the productions.
 
-      --  The following are specified in grammar file declarations and used in 
other declarations
-      --  or actions. Faces, Indents only used if .wy action language is
-      --  elisp and output language is not elisp.
+      Virtual_Identifiers : String_Arrays.Vector;
+      --  Nonterminals and terminals introduced by translating from EBNF to
+      --  BNF.
+
+      --  The following are specified in grammar file declarations and used
+      --  in other declarations or actions. Faces, Indents only used if .wy
+      --  action language is elisp and output language is not elisp.
 
       re2c_Regexps : String_Pair_Lists.List; -- %re2c_regexp
       Faces        : String_Lists.List;      -- %elisp_face
       Indents      : String_Pair_Lists.List; -- %elisp_indent
+      Actions      : Elisp_Action_Maps.Map;  -- %elisp_action
    end record;
 
    function "+" (Item : in String) return 
Ada.Strings.Unbounded.Unbounded_String
diff --git a/wisitoken-gen_token_enum.ads b/wisitoken-gen_token_enum.ads
index 36fca14..05bdb99 100644
--- a/wisitoken-gen_token_enum.ads
+++ b/wisitoken-gen_token_enum.ads
@@ -43,44 +43,36 @@ package WisiToken.Gen_Token_Enum is
    subtype Nonterminal_Enum_ID is Token_Enum_ID range First_Nonterminal .. 
Last_Nonterminal;
 
    LR1_Descriptor : aliased WisiToken.Descriptor :=
-     (First_Terminal                => +First_Terminal,
-      Last_Terminal                 => +Last_Terminal,
-      First_Nonterminal             => +First_Nonterminal,
-      Last_Nonterminal              => +Last_Nonterminal,
-      EOI_ID                        => +EOF_ID,
-      Accept_ID                     => +Accept_ID,
-      Case_Insensitive              => Case_Insensitive,
-      New_Line_ID                   => Invalid_Token_ID,
-      Comment_ID                    => Invalid_Token_ID,
-      Left_Paren_ID                 => Invalid_Token_ID,
-      Right_Paren_ID                => Invalid_Token_ID,
-      String_1_ID                   => Invalid_Token_ID,
-      String_2_ID                   => Invalid_Token_ID,
-      Embedded_Quote_Escape_Doubled => False,
-      Image                         => (others => null), --  set in body 
elaboration time code
-      Terminal_Image_Width          => Terminal_Enum_ID'Width,
-      Image_Width                   => Token_Enum_ID'Width,
-      Last_Lookahead                => +Last_Terminal);
+     (First_Terminal       => +First_Terminal,
+      Last_Terminal        => +Last_Terminal,
+      First_Nonterminal    => +First_Nonterminal,
+      Last_Nonterminal     => +Last_Nonterminal,
+      EOI_ID               => +EOF_ID,
+      Accept_ID            => +Accept_ID,
+      Case_Insensitive     => Case_Insensitive,
+      New_Line_ID          => Invalid_Token_ID,
+      String_1_ID          => Invalid_Token_ID,
+      String_2_ID          => Invalid_Token_ID,
+      Image                => (others => null), --  set in body elaboration 
time code
+      Terminal_Image_Width => Terminal_Enum_ID'Width,
+      Image_Width          => Token_Enum_ID'Width,
+      Last_Lookahead       => +Last_Terminal);
 
    LALR_Descriptor : aliased WisiToken.Descriptor :=
-     (First_Terminal                => +First_Terminal,
-      Last_Terminal                 => +Last_Terminal,
-      First_Nonterminal             => +First_Nonterminal,
-      Last_Nonterminal              => +Last_Nonterminal,
-      EOI_ID                        => +EOF_ID,
-      Accept_ID                     => +Accept_ID,
-      Case_Insensitive              => Case_Insensitive,
-      New_Line_ID                   => Invalid_Token_ID,
-      Comment_ID                    => Invalid_Token_ID,
-      Left_Paren_ID                 => Invalid_Token_ID,
-      Right_Paren_ID                => Invalid_Token_ID,
-      String_1_ID                   => Invalid_Token_ID,
-      String_2_ID                   => Invalid_Token_ID,
-      Embedded_Quote_Escape_Doubled => False,
-      Image                         => (others => null),
-      Terminal_Image_Width          => Terminal_Enum_ID'Width,
-      Image_Width                   => Token_Enum_ID'Width,
-      Last_Lookahead                => +First_Nonterminal);
+     (First_Terminal       => +First_Terminal,
+      Last_Terminal        => +Last_Terminal,
+      First_Nonterminal    => +First_Nonterminal,
+      Last_Nonterminal     => +Last_Nonterminal,
+      EOI_ID               => +EOF_ID,
+      Accept_ID            => +Accept_ID,
+      Case_Insensitive     => Case_Insensitive,
+      New_Line_ID          => Invalid_Token_ID,
+      String_1_ID          => Invalid_Token_ID,
+      String_2_ID          => Invalid_Token_ID,
+      Image                => (others => null),
+      Terminal_Image_Width => Terminal_Enum_ID'Width,
+      Image_Width          => Token_Enum_ID'Width,
+      Last_Lookahead       => +First_Nonterminal);
 
    type Enum_Syntax is array (Token_Enum_ID range Token_Enum_ID'First .. 
Last_Terminal) of
      WisiToken.Lexer.Regexp.Syntax_Item;
diff --git a/wisitoken-generate-lr-lalr_generate.adb 
b/wisitoken-generate-lr-lalr_generate.adb
index 2a1f403..f55c822 100644
--- a/wisitoken-generate-lr-lalr_generate.adb
+++ b/wisitoken-generate-lr-lalr_generate.adb
@@ -1,597 +1,610 @@
---  Abstract :
---
---  See spec.
---
---  Copyright (C) 2002 - 2005, 2008 - 2015, 2017 - 2019 Free Software 
Foundation, Inc.
---
---  This file is part of the WisiToken package.
---
---  The WisiToken package is free software; you can redistribute it
---  and/or modify it under 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 library is distributed in
---  the hope that it will be useful, but WITHOUT ANY WARRANTY; without
---  even the implied warranty of MERCHAN- TABILITY or FITNESS FOR A
---  PARTICULAR PURPOSE.
---
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with Ada.Containers;
-with Ada.Text_IO;
-with SAL.Gen_Definite_Doubly_Linked_Lists;
-package body WisiToken.Generate.LR.LALR_Generate is
-
-   package Item_List_Cursor_Lists is new SAL.Gen_Definite_Doubly_Linked_Lists 
(LR1_Items.Item_Lists.Cursor);
-
-   type Item_Map is record
-      --  Keep track of all copies of Item, so Lookaheads can be updated
-      --  after they are initially copied.
-      From : LR1_Items.Item_Lists.Cursor;
-      To   : Item_List_Cursor_Lists.List;
-   end record;
-
-   package Item_Map_Lists is new SAL.Gen_Definite_Doubly_Linked_Lists 
(Item_Map);
-   --  IMPROVEME: should be a 3D array indexed by Prod, rhs_index,
-   --  dot_index. But it's not broken or slow, so we're not fixing it.
-
-   function Propagate_Lookahead (Descriptor : in WisiToken.Descriptor) return 
access LR1_Items.Lookahead
-   is begin
-      return new Token_ID_Set'(LR1_Items.To_Lookahead 
(Descriptor.Last_Lookahead, Descriptor));
-   end Propagate_Lookahead;
-
-   function Null_Lookahead (Descriptor : in WisiToken.Descriptor) return 
access LR1_Items.Lookahead
-   is begin
-      return new Token_ID_Set'(Descriptor.First_Terminal .. 
Descriptor.Last_Lookahead => False);
-   end Null_Lookahead;
-
-   ----------
-   --  Debug output
-
-   procedure Put
-     (Grammar      : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor   : in WisiToken.Descriptor;
-      Propagations : in Item_Map_Lists.List)
-   is
-      use LR1_Items.Item_Lists;
-   begin
-      for Map of Propagations loop
-         Ada.Text_IO.Put ("From ");
-         LR1_Items.Put (Grammar, Descriptor, Constant_Ref (Map.From), 
Show_Lookaheads => True);
-         Ada.Text_IO.New_Line;
-
-         for Cur of Map.To loop
-            Ada.Text_IO.Put ("To   ");
-            LR1_Items.Put (Grammar, Descriptor, Constant_Ref (Cur), 
Show_Lookaheads => True);
-            Ada.Text_IO.New_Line;
-         end loop;
-      end loop;
-   end Put;
-
-   ----------
-   --  Generate utils
-
-   function LALR_Goto_Transitions
-     (Kernel            : in LR1_Items.Item_Set;
-      Symbol            : in Token_ID;
-      First_Nonterm_Set : in Token_Array_Token_Set;
-      Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor        : in WisiToken.Descriptor)
-     return LR1_Items.Item_Set
-   is
-      use Token_ID_Arrays;
-      use LR1_Items;
-      use LR1_Items.Item_Lists;
-
-      Goto_Set : Item_Set;
-      Dot_ID   : Token_ID;
-   begin
-      for Item of Kernel.Set loop
-
-         if Has_Element (Item.Dot) then
-
-            Dot_ID := Element (Item.Dot);
-            --  ID of token after Dot
-
-            --  If Symbol = EOF_Token, this is the start symbol accept
-            --  production; don't need a kernel with dot after EOF.
-            if (Dot_ID = Symbol and Symbol /= Descriptor.EOI_ID) and then
-              not Has_Element (Find (Item.Prod, Next (Item.Dot), Goto_Set))
-            then
-               Goto_Set.Set.Insert
-                 ((Prod       => Item.Prod,
-                   Dot        => Next (Item.Dot),
-                   Lookaheads => new Token_ID_Set'(Item.Lookaheads.all)));
-
-               if Trace_Generate > Detail then
-                  Ada.Text_IO.Put_Line ("LALR_Goto_Transitions 1 " & Image 
(Symbol, Descriptor));
-                  Put (Grammar, Descriptor, Goto_Set);
-               end if;
-            end if;
-
-            if Dot_ID in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal and then
-              First_Nonterm_Set (Dot_ID, Symbol)
-            then
-               --  Find the production(s) that create Dot_ID with first token 
Symbol
-               --  and put them in.
-               --
-               --  This is equivalent to Filter (LR1_Items.Closure, 
In_Kernel), but
-               --  more efficient, because it does not generate non-kernel 
items. See
-               --  Test/compare_goto_transitions.adb.
-               for Prod of Grammar loop
-                  for RHS_2_I in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index 
loop
-                     declare
-                        P_ID  : constant Production_ID          := (Prod.LHS, 
RHS_2_I);
-                        Dot_2 : constant Token_ID_Arrays.Cursor := Prod.RHSs 
(RHS_2_I).Tokens.First;
-                     begin
-                        if (Dot_ID = Prod.LHS or First_Nonterm_Set (Dot_ID, 
Prod.LHS)) and
-                          (Has_Element (Dot_2) and then Element (Dot_2) = 
Symbol)
-                        then
-                           if not Has_Element (Find (P_ID, Next (Dot_2), 
Goto_Set)) then
-                              Goto_Set.Set.Insert
-                                ((Prod       => P_ID,
-                                  Dot        => Next (Dot_2),
-                                  Lookaheads => Null_Lookahead (Descriptor)));
-
-                              if Trace_Generate > Detail then
-                                 Ada.Text_IO.Put_Line ("LALR_Goto_Transitions 
2 " & Image (Symbol, Descriptor));
-                                 Put (Grammar, Descriptor, Goto_Set);
-                              end if;
-
-                              --  else already in goto set
-                           end if;
-                        end if;
-                     end;
-                  end loop;
-               end loop;
-            end if;
-         end if; -- item.dot /= null
-      end loop;
-
-      return Goto_Set;
-   end LALR_Goto_Transitions;
-
-   function LALR_Kernels
-     (Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
-      First_Nonterm_Set : in Token_Array_Token_Set;
-      Descriptor        : in WisiToken.Descriptor)
-     return LR1_Items.Item_Set_List
-   is
-      use all type Token_ID_Arrays.Cursor;
-      use all type Ada.Containers.Count_Type;
-      use LR1_Items;
-
-      First_State_Index : constant State_Index := 0;
-      Kernels           : LR1_Items.Item_Set_List;
-      Kernel_Tree       : LR1_Items.Item_Set_Trees.Tree; -- for fast find
-      States_To_Check   : State_Index_Queues.Queue;
-      Checking_State    : State_Index;
-
-      New_Item_Set : Item_Set :=
-        (Set            => Item_Lists.To_List
-           ((Prod       => (Grammar.First_Index, 0),
-             Dot        => Grammar (Grammar.First_Index).RHSs (0).Tokens.First,
-             Lookaheads => Null_Lookahead (Descriptor))),
-         Goto_List      => <>,
-         Dot_IDs        => <>,
-         State          => First_State_Index);
-
-      Found_State : Unknown_State_Index;
-   begin
-      Kernels.Set_First (First_State_Index);
-
-      Add (New_Item_Set, Kernels, Kernel_Tree, Descriptor, Include_Lookaheads 
=> False);
-
-      States_To_Check.Put (First_State_Index);
-      loop
-         exit when States_To_Check.Is_Empty;
-         Checking_State := States_To_Check.Get;
-
-         if Trace_Generate > Detail then
-            Ada.Text_IO.Put ("Checking ");
-            Put (Grammar, Descriptor, Kernels (Checking_State));
-         end if;
-
-         for Symbol in Descriptor.First_Terminal .. 
Descriptor.Last_Nonterminal loop
-            --  LALR_Goto_Transitions does _not_ ignore Symbol if it is not in
-            --  Item_Set.Dot_IDs, so we can't iterate on that here as we do in
-            --  LR1_Generate.
-
-            New_Item_Set := LALR_Goto_Transitions
-              (Kernels (Checking_State), Symbol, First_Nonterm_Set, Grammar, 
Descriptor);
-
-            if New_Item_Set.Set.Length > 0 then
-
-               Found_State := Find (New_Item_Set, Kernel_Tree, 
Match_Lookaheads => False);
-
-               if Found_State = Unknown_State then
-                  New_Item_Set.State := Kernels.Last_Index + 1;
-
-                  States_To_Check.Put (New_Item_Set.State);
-
-                  Add (New_Item_Set, Kernels, Kernel_Tree, Descriptor, 
Include_Lookaheads => False);
-
-                  if Trace_Generate > Detail then
-                     Ada.Text_IO.Put_Line ("  adding state" & 
Unknown_State_Index'Image (Kernels.Last_Index));
-                  end if;
-
-                  Kernels (Checking_State).Goto_List.Insert ((Symbol, 
Kernels.Last_Index));
-               else
-
-                  --  If there's not already a goto entry between these two 
sets, create one.
-                  if not Is_In ((Symbol, Found_State), Kernels 
(Checking_State).Goto_List) then
-                     if Trace_Generate > Detail then
-                        Ada.Text_IO.Put_Line
-                          ("  state" & Unknown_State_Index'Image 
(Checking_State) &
-                             " adding goto on " & Image (Symbol, Descriptor) & 
" to state" &
-                             Unknown_State_Index'Image (Found_State));
-
-                     end if;
-
-                     Kernels (Checking_State).Goto_List.Insert ((Symbol, 
Found_State));
-                  end if;
-               end if;
-            end if;
-         end loop;
-      end loop;
-
-      if Trace_Generate > Detail then
-         Ada.Text_IO.New_Line;
-      end if;
-
-      return Kernels;
-   end LALR_Kernels;
-
-   --  Add a propagation entry (if it doesn't already exist) from From in
-   --  From_Set to To_Item.
-   procedure Add_Propagation
-     (From         : in     LR1_Items.Item;
-      From_Set     : in     LR1_Items.Item_Set;
-      To_Item      : in     LR1_Items.Item_Lists.Cursor;
-      Propagations : in out Item_Map_Lists.List)
-   is
-      use Item_Map_Lists;
-      use Item_List_Cursor_Lists;
-      use LR1_Items;
-      use LR1_Items.Item_Lists;
-
-      From_Cur : constant Item_Lists.Cursor := Find (From, From_Set);
-
-      From_Match : Item_Map_Lists.Cursor := Propagations.First;
-      To_Match   : Item_List_Cursor_Lists.Cursor;
-   begin
-      Find_From :
-      loop
-         exit Find_From when not Has_Element (From_Match);
-
-         declare
-            Map : Item_Map renames Constant_Ref (From_Match);
-         begin
-            if From_Cur = Map.From then
-
-               To_Match := Map.To.First;
-               loop
-                  exit when not Has_Element (To_Match);
-
-                  declare
-                     use all type SAL.Compare_Result;
-                     Cur       : Item_Lists.Cursor renames Constant_Ref 
(To_Match);
-                     Test_Item : LR1_Items.Item renames Constant_Ref (Cur);
-                  begin
-                     if Equal = LR1_Items.Item_Compare (Test_Item, 
Constant_Ref (To_Item)) then
-                        exit Find_From;
-                     end if;
-                  end;
-                  Next (To_Match);
-               end loop;
-               exit Find_From;
-            end if;
-         end;
-
-         Next (From_Match);
-      end loop Find_From;
-
-      if not Has_Element (From_Match) then
-         Propagations.Append ((From_Cur, To_List (To_Item)));
-
-      elsif not Has_Element (To_Match) then
-         Ref (From_Match).To.Append (To_Item);
-
-      else
-         raise SAL.Programmer_Error with "Add_Propagation: unexpected case";
-      end if;
-   end Add_Propagation;
-
-   --  Calculate the lookaheads from Closure_Item for Source_Item.
-   --  Source_Item must be one of the kernel items in Source_Set.
-   --  Closure_Item must be an item in the lookahead closure of Source_Item 
for #.
-   --
-   --  Spontaneous lookaheads are put in Source_Item.Lookahead,
-   --  propagated lookaheads in Propagations.
-   --
-   --  Set Used_Tokens = True for all tokens in lookaheads.
-   procedure Generate_Lookahead_Info
-     (Source_Item  : in     LR1_Items.Item;
-      Source_Set   : in     LR1_Items.Item_Set;
-      Closure_Item : in     LR1_Items.Item;
-      Propagations : in out Item_Map_Lists.List;
-      Descriptor   : in     WisiToken.Descriptor;
-      Grammar      : in     WisiToken.Productions.Prod_Arrays.Vector;
-      Kernels      : in out LR1_Items.Item_Set_List)
-   is
-      use LR1_Items;
-      use LR1_Items.Item_Lists;
-      use Token_ID_Arrays;
-
-      Spontaneous_Count : Integer := 0;
-   begin
-      if Trace_Generate > Outline then
-         Ada.Text_IO.Put_Line ("  closure_item: ");
-         LR1_Items.Put (Grammar, Descriptor, Closure_Item);
-         Ada.Text_IO.New_Line;
-      end if;
-
-      if not Has_Element (Closure_Item.Dot) then
-         return;
-      end if;
-
-      declare
-         ID         : constant Token_ID               := Element 
(Closure_Item.Dot);
-         Next_Dot   : constant Token_ID_Arrays.Cursor := Next 
(Closure_Item.Dot);
-         Goto_State : constant Unknown_State_Index    := LR1_Items.Goto_State 
(Source_Set, ID);
-         To_Item    : constant Item_Lists.Cursor      :=
-           (if Goto_State = Unknown_State then Item_Lists.No_Element
-            else LR1_Items.Find (Closure_Item.Prod, Next_Dot, Kernels 
(Goto_State)));
-      begin
-         if Closure_Item.Lookaheads (Descriptor.Last_Lookahead) and 
Has_Element (To_Item) then
-            Add_Propagation
-              (From         => Source_Item,
-               From_Set     => Source_Set,
-               To_Item      => To_Item,
-               Propagations => Propagations);
-         end if;
-
-         if Has_Element (To_Item) then
-            if Trace_Generate > Outline then
-               Spontaneous_Count := Spontaneous_Count + 1;
-               Ada.Text_IO.Put_Line ("  spontaneous: " & Lookahead_Image 
(Closure_Item.Lookaheads.all, Descriptor));
-            end if;
-
-            LR1_Items.Include (Ref (To_Item), Closure_Item.Lookaheads.all, 
Descriptor);
-         end if;
-      end;
-   end Generate_Lookahead_Info;
-
-   procedure Propagate_Lookaheads
-     (List       : in Item_Map_Lists.List;
-      Descriptor : in WisiToken.Descriptor)
-   is
-      --  In List, update all To lookaheads from From lookaheads,
-      --  recursively.
-
-      use LR1_Items.Item_Lists;
-
-      More_To_Check : Boolean := True;
-      Added_One     : Boolean;
-   begin
-      while More_To_Check loop
-
-         More_To_Check := False;
-         for Mapping of List loop
-            for Copy of Mapping.To loop
-               LR1_Items.Include (Ref (Copy), Constant_Ref 
(Mapping.From).Lookaheads.all, Added_One, Descriptor);
-
-               More_To_Check := More_To_Check or Added_One;
-            end loop;
-         end loop;
-      end loop;
-   end Propagate_Lookaheads;
-
-   --  Calculate the LALR(1) lookaheads for Grammar.
-   --  Kernels should be the sets of LR(0) kernels on input, and will
-   --  become the set of LALR(1) kernels on output.
-   procedure Fill_In_Lookaheads
-     (Grammar                 : in     
WisiToken.Productions.Prod_Arrays.Vector;
-      Has_Empty_Production    : in     Token_ID_Set;
-      First_Terminal_Sequence : in     Token_Sequence_Arrays.Vector;
-      Kernels                 : in out LR1_Items.Item_Set_List;
-      Descriptor              : in     WisiToken.Descriptor)
-   is
-      pragma Warnings (Off, """Kernel_Item_Set"" is not modified, could be 
declared constant");
-      --  WORKAROUND: GNAT GPL 2018 complains Kernel_Item_Set could be a 
constant, but
-      --  when we declare that, it complains the target of the assignment of
-      --  .Prod, .Dot below must be a variable.
-
-      Kernel_Item_Set : LR1_Items.Item_Set := -- used for temporary arg to 
Closure
-        (Set            => LR1_Items.Item_Lists.To_List
-           ((Prod       => <>,
-             Dot        => <>,
-             Lookaheads => Propagate_Lookahead (Descriptor))),
-         Goto_List      => <>,
-         Dot_IDs        => <>,
-         State          => <>);
-
-      Closure : LR1_Items.Item_Set;
-
-      Propagation_List : Item_Map_Lists.List;
-
-   begin
-      for Kernel of Kernels loop
-         if Trace_Generate > Outline then
-            Ada.Text_IO.Put ("Adding lookaheads for ");
-            LR1_Items.Put (Grammar, Descriptor, Kernel);
-         end if;
-
-         for Kernel_Item of Kernel.Set loop
-            Kernel_Item_Set.Set (Kernel_Item_Set.Set.First).Prod := 
Kernel_Item.Prod;
-            Kernel_Item_Set.Set (Kernel_Item_Set.Set.First).Dot  := 
Kernel_Item.Dot;
-
-            Closure := LR1_Items.Closure
-              (Kernel_Item_Set, Has_Empty_Production, First_Terminal_Sequence, 
Grammar, Descriptor);
-
-            for Closure_Item of Closure.Set loop
-               Generate_Lookahead_Info
-                 (Kernel_Item, Kernel, Closure_Item, Propagation_List, 
Descriptor, Grammar, Kernels);
-            end loop;
-         end loop;
-      end loop;
-
-      if Trace_Generate > Outline then
-         Ada.Text_IO.New_Line;
-         Ada.Text_IO.Put_Line ("Propagations:");
-         Put (Grammar, Descriptor, Propagation_List);
-         Ada.Text_IO.New_Line;
-      end if;
-
-      Propagate_Lookaheads (Propagation_List, Descriptor);
-   end Fill_In_Lookaheads;
-
-   --  Add actions for all Kernels to Table.
-   procedure Add_Actions
-     (Kernels                 : in     LR1_Items.Item_Set_List;
-      Grammar                 : in     
WisiToken.Productions.Prod_Arrays.Vector;
-      Has_Empty_Production    : in     Token_ID_Set;
-      First_Nonterm_Set       : in     Token_Array_Token_Set;
-      First_Terminal_Sequence : in     Token_Sequence_Arrays.Vector;
-      Conflicts               :    out Conflict_Lists.List;
-      Table                   : in out Parse_Table;
-      Descriptor              : in     WisiToken.Descriptor)
-   is
-      Closure : LR1_Items.Item_Set;
-   begin
-      for Kernel of Kernels loop
-         --  IMPROVEME: there are three "closure" computations that could
-         --  probably be refactored to save computation; in
-         --  LALR_Goto_Transitions, Fill_In_Lookaheads, and here.
-         Closure := LR1_Items.Closure (Kernel, Has_Empty_Production, 
First_Terminal_Sequence, Grammar, Descriptor);
-
-         Add_Actions (Closure, Table, Grammar, Has_Empty_Production, 
First_Nonterm_Set, Conflicts, Descriptor);
-      end loop;
-
-      if Trace_Generate > Detail then
-         Ada.Text_IO.New_Line;
-      end if;
-   end Add_Actions;
-
-   function Generate
-     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor      : in WisiToken.Descriptor;
-      Known_Conflicts : in Conflict_Lists.List := Conflict_Lists.Empty_List;
-      McKenzie_Param  : in McKenzie_Param_Type := Default_McKenzie_Param;
-      Put_Parse_Table : in Boolean := False)
-     return Parse_Table_Ptr
-   is
-      use all type Ada.Containers.Count_Type;
-
-      Ignore_Unused_Tokens     : constant Boolean := WisiToken.Trace_Generate 
> Detail;
-      Ignore_Unknown_Conflicts : constant Boolean := WisiToken.Trace_Generate 
> Detail;
-      Unused_Tokens            : constant Boolean := 
WisiToken.Generate.Check_Unused_Tokens (Descriptor, Grammar);
-
-      Table : Parse_Table_Ptr;
-
-      Has_Empty_Production : constant Token_ID_Set := 
WisiToken.Generate.Has_Empty_Production (Grammar);
-
-      Minimal_Terminal_Sequences : constant Minimal_Sequence_Array :=
-        Compute_Minimal_Terminal_Sequences (Descriptor, Grammar);
-
-      Minimal_Terminal_First : constant Token_Array_Token_ID :=
-        Compute_Minimal_Terminal_First (Descriptor, 
Minimal_Terminal_Sequences);
-
-      First_Nonterm_Set : constant Token_Array_Token_Set := 
WisiToken.Generate.First
-        (Grammar, Has_Empty_Production, Descriptor.First_Terminal);
-
-      First_Terminal_Sequence : constant Token_Sequence_Arrays.Vector :=
-        WisiToken.Generate.To_Terminal_Sequence_Array (First_Nonterm_Set, 
Descriptor);
-
-      Kernels : LR1_Items.Item_Set_List := LALR_Kernels (Grammar, 
First_Nonterm_Set, Descriptor);
-
-      Unknown_Conflicts    : Conflict_Lists.List;
-      Known_Conflicts_Edit : Conflict_Lists.List := Known_Conflicts;
-
-   begin
-      WisiToken.Generate.Error := False; -- necessary in unit tests; some 
previous test might have encountered an error.
-
-      Fill_In_Lookaheads (Grammar, Has_Empty_Production, 
First_Terminal_Sequence, Kernels, Descriptor);
-
-      if Unused_Tokens then
-         WisiToken.Generate.Error := not Ignore_Unused_Tokens;
-         Ada.Text_IO.New_Line;
-      end if;
-
-      if Trace_Generate > Detail then
-         Ada.Text_IO.New_Line;
-         Ada.Text_IO.Put_Line ("LR(1) Kernels:");
-         LR1_Items.Put (Grammar, Descriptor, Kernels, Show_Lookaheads => True);
-      end if;
-
-      Table := new Parse_Table
-        (State_First       => Kernels.First_Index,
-         State_Last        => Kernels.Last_Index,
-         First_Terminal    => Descriptor.First_Terminal,
-         Last_Terminal     => Descriptor.Last_Terminal,
-         First_Nonterminal => Descriptor.First_Nonterminal,
-         Last_Nonterminal  => Descriptor.Last_Nonterminal);
-
-      if McKenzie_Param = Default_McKenzie_Param then
-         --  Descriminants in Default are wrong
-         Table.McKenzie_Param :=
-           (First_Terminal    => Descriptor.First_Terminal,
-            Last_Terminal     => Descriptor.Last_Terminal,
-            First_Nonterminal => Descriptor.First_Nonterminal,
-            Last_Nonterminal  => Descriptor.Last_Nonterminal,
-            Insert            => (others => 0),
-            Delete            => (others => 0),
-            Push_Back         => (others => 0),
-            Ignore_Check_Fail => Default_McKenzie_Param.Ignore_Check_Fail,
-            Task_Count        => Default_McKenzie_Param.Task_Count,
-            Cost_Limit        => Default_McKenzie_Param.Cost_Limit,
-            Check_Limit       => Default_McKenzie_Param.Check_Limit,
-            Check_Delta_Limit => Default_McKenzie_Param.Check_Delta_Limit,
-            Enqueue_Limit     => Default_McKenzie_Param.Enqueue_Limit);
-      else
-         Table.McKenzie_Param := McKenzie_Param;
-      end if;
-
-      Add_Actions
-        (Kernels, Grammar, Has_Empty_Production, First_Nonterm_Set, 
First_Terminal_Sequence, Unknown_Conflicts,
-         Table.all, Descriptor);
-
-      --  Set Table.States.Productions, Minimal_Complete_Actions for 
McKenzie_Recover
-      for State in Table.States'Range loop
-         Table.States (State).Productions := LR1_Items.Productions (Kernels 
(State));
-         if Trace_Generate > Extra then
-            Ada.Text_IO.Put_Line ("Set_Minimal_Complete_Actions:" & 
State_Index'Image (State));
-         end if;
-         WisiToken.Generate.LR.Set_Minimal_Complete_Actions
-           (Table.States (State), Kernels (State), Descriptor, Grammar, 
Minimal_Terminal_Sequences,
-            Minimal_Terminal_First);
-      end loop;
-
-      if Put_Parse_Table then
-         WisiToken.Generate.LR.Put_Parse_Table
-           (Table, "LALR", Grammar, Kernels, Unknown_Conflicts, Descriptor);
-      end if;
-
-      Delete_Known (Unknown_Conflicts, Known_Conflicts_Edit);
-
-      if Unknown_Conflicts.Length > 0 then
-         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "unknown 
conflicts:");
-         Put (Unknown_Conflicts, Ada.Text_IO.Current_Error, Descriptor);
-         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
-         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
-      end if;
-
-      if Known_Conflicts_Edit.Length > 0 then
-         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "excess known 
conflicts:");
-         Put (Known_Conflicts_Edit, Ada.Text_IO.Current_Error, Descriptor);
-         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
-         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
-      end if;
-
-      return Table;
-   end Generate;
-
-end WisiToken.Generate.LR.LALR_Generate;
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2002 - 2005, 2008 - 2015, 2017 - 2019 Free Software 
Foundation, Inc.
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under 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 library is distributed in
+--  the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+--  even the implied warranty of MERCHAN- TABILITY or FITNESS FOR A
+--  PARTICULAR PURPOSE.
+--
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Containers;
+with Ada.Text_IO;
+with SAL.Gen_Definite_Doubly_Linked_Lists;
+package body WisiToken.Generate.LR.LALR_Generate is
+
+   package Item_List_Cursor_Lists is new SAL.Gen_Definite_Doubly_Linked_Lists 
(LR1_Items.Item_Lists.Cursor);
+
+   type Item_Map is record
+      --  Keep track of all copies of Item, so Lookaheads can be updated
+      --  after they are initially copied.
+      From : LR1_Items.Item_Lists.Cursor;
+      To   : Item_List_Cursor_Lists.List;
+   end record;
+
+   package Item_Map_Lists is new SAL.Gen_Definite_Doubly_Linked_Lists 
(Item_Map);
+   --  IMPROVEME: should be a 3D array indexed by Prod, rhs_index,
+   --  dot_index. But it's not broken or slow, so we're not fixing it.
+
+   function Propagate_Lookahead (Descriptor : in WisiToken.Descriptor) return 
Token_ID_Set_Access
+   is begin
+      return new Token_ID_Set'(LR1_Items.To_Lookahead 
(Descriptor.Last_Lookahead, Descriptor));
+   end Propagate_Lookahead;
+
+   function Null_Lookahead (Descriptor : in WisiToken.Descriptor) return 
Token_ID_Set_Access
+   is begin
+      return new Token_ID_Set'(Descriptor.First_Terminal .. 
Descriptor.Last_Lookahead => False);
+   end Null_Lookahead;
+
+   ----------
+   --  Debug output
+
+   procedure Put
+     (Grammar      : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor   : in WisiToken.Descriptor;
+      Propagations : in Item_Map_Lists.List)
+   is
+      use LR1_Items.Item_Lists;
+   begin
+      for Map of Propagations loop
+         Ada.Text_IO.Put ("From ");
+         LR1_Items.Put (Grammar, Descriptor, Constant_Ref (Map.From), 
Show_Lookaheads => True);
+         Ada.Text_IO.New_Line;
+
+         for Cur of Map.To loop
+            Ada.Text_IO.Put ("To   ");
+            LR1_Items.Put (Grammar, Descriptor, Constant_Ref (Cur), 
Show_Lookaheads => True);
+            Ada.Text_IO.New_Line;
+         end loop;
+      end loop;
+   end Put;
+
+   ----------
+   --  Generate utils
+
+   function LALR_Goto_Transitions
+     (Kernel            : in LR1_Items.Item_Set;
+      Symbol            : in Token_ID;
+      First_Nonterm_Set : in Token_Array_Token_Set;
+      Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor        : in WisiToken.Descriptor)
+     return LR1_Items.Item_Set
+   is
+      use Token_ID_Arrays;
+      use LR1_Items;
+      use LR1_Items.Item_Lists;
+
+      Goto_Set : Item_Set;
+      Dot_ID   : Token_ID;
+   begin
+      for Item of Kernel.Set loop
+
+         if Has_Element (Item.Dot) then
+
+            Dot_ID := Element (Item.Dot);
+            --  ID of token after Dot
+
+            --  If Symbol = EOF_Token, this is the start symbol accept
+            --  production; don't need a kernel with dot after EOF.
+            if (Dot_ID = Symbol and Symbol /= Descriptor.EOI_ID) and then
+              not Has_Element (Find (Item.Prod, Next (Item.Dot), Goto_Set))
+            then
+               Goto_Set.Set.Insert
+                 ((Prod       => Item.Prod,
+                   Dot        => Next (Item.Dot),
+                   Lookaheads => new Token_ID_Set'(Item.Lookaheads.all)));
+
+               if Trace_Generate > Detail then
+                  Ada.Text_IO.Put_Line ("LALR_Goto_Transitions 1 " & Image 
(Symbol, Descriptor));
+                  Put (Grammar, Descriptor, Goto_Set);
+               end if;
+            end if;
+
+            if Dot_ID in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal and then
+              First_Nonterm_Set (Dot_ID, Symbol)
+            then
+               --  Find the production(s) that create Dot_ID with first token 
Symbol
+               --  and put them in.
+               for Prod of Grammar loop
+                  for RHS_2_I in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index 
loop
+                     declare
+                        P_ID  : constant Production_ID          := (Prod.LHS, 
RHS_2_I);
+                        Dot_2 : constant Token_ID_Arrays.Cursor := Prod.RHSs 
(RHS_2_I).Tokens.First;
+                     begin
+                        if (Dot_ID = Prod.LHS or First_Nonterm_Set (Dot_ID, 
Prod.LHS)) and
+                          (Has_Element (Dot_2) and then Element (Dot_2) = 
Symbol)
+                        then
+                           if not Has_Element (Find (P_ID, Next (Dot_2), 
Goto_Set)) then
+                              Goto_Set.Set.Insert
+                                ((Prod       => P_ID,
+                                  Dot        => Next (Dot_2),
+                                  Lookaheads => Null_Lookahead (Descriptor)));
+
+                              --  else already in goto set
+                           end if;
+                        end if;
+                     end;
+                  end loop;
+               end loop;
+               if Trace_Generate > Detail then
+                  Ada.Text_IO.Put_Line ("LALR_Goto_Transitions 2 " & Image 
(Symbol, Descriptor));
+                  Put (Grammar, Descriptor, Goto_Set);
+               end if;
+            end if;
+         end if; -- item.dot /= null
+      end loop;
+
+      return Goto_Set;
+   end LALR_Goto_Transitions;
+
+   function LALR_Kernels
+     (Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
+      First_Nonterm_Set : in Token_Array_Token_Set;
+      Descriptor        : in WisiToken.Descriptor)
+     return LR1_Items.Item_Set_List
+   is
+      use all type Token_ID_Arrays.Cursor;
+      use all type Ada.Containers.Count_Type;
+      use LR1_Items;
+
+      First_State_Index : constant State_Index := 0;
+      Kernels           : LR1_Items.Item_Set_List;
+      Kernel_Tree       : LR1_Items.Item_Set_Trees.Tree; -- for fast find
+      States_To_Check   : State_Index_Queues.Queue;
+      Checking_State    : State_Index;
+
+      New_Item_Set : Item_Set :=
+        (Set            => Item_Lists.To_List
+           ((Prod       => (Grammar.First_Index, 0),
+             Dot        => Grammar (Grammar.First_Index).RHSs (0).Tokens.First,
+             Lookaheads => Null_Lookahead (Descriptor))),
+         Goto_List      => <>,
+         Dot_IDs        => <>,
+         State          => First_State_Index);
+
+      Found_State : Unknown_State_Index;
+   begin
+      Kernels.Set_First (First_State_Index);
+
+      Add (New_Item_Set, Kernels, Kernel_Tree, Descriptor, Include_Lookaheads 
=> False);
+
+      States_To_Check.Put (First_State_Index);
+      loop
+         exit when States_To_Check.Is_Empty;
+         Checking_State := States_To_Check.Get;
+
+         if Trace_Generate > Detail then
+            Ada.Text_IO.Put ("Checking ");
+            Put (Grammar, Descriptor, Kernels (Checking_State));
+         end if;
+
+         for Symbol in Descriptor.First_Terminal .. 
Descriptor.Last_Nonterminal loop
+            --  LALR_Goto_Transitions does _not_ ignore Symbol if it is not in
+            --  Item_Set.Dot_IDs, so we can't iterate on that here as we do in
+            --  LR1_Generate.
+
+            New_Item_Set := LALR_Goto_Transitions
+              (Kernels (Checking_State), Symbol, First_Nonterm_Set, Grammar, 
Descriptor);
+
+            if New_Item_Set.Set.Length > 0 then
+
+               Found_State := Find (New_Item_Set, Kernel_Tree, 
Match_Lookaheads => False);
+
+               if Found_State = Unknown_State then
+                  New_Item_Set.State := Kernels.Last_Index + 1;
+
+                  States_To_Check.Put (New_Item_Set.State);
+
+                  Add (New_Item_Set, Kernels, Kernel_Tree, Descriptor, 
Include_Lookaheads => False);
+
+                  if Trace_Generate > Detail then
+                     Ada.Text_IO.Put_Line ("  adding state" & 
Unknown_State_Index'Image (Kernels.Last_Index));
+
+                     Ada.Text_IO.Put_Line
+                       ("  state" & Unknown_State_Index'Image (Checking_State) 
&
+                          " adding goto on " & Image (Symbol, Descriptor) & " 
to state" &
+                          Unknown_State_Index'Image (Kernels.Last_Index));
+                  end if;
+
+                  Kernels (Checking_State).Goto_List.Insert ((Symbol, 
Kernels.Last_Index));
+               else
+
+                  --  If there's not already a goto entry between these two 
sets, create one.
+                  if not Is_In ((Symbol, Found_State), Kernels 
(Checking_State).Goto_List) then
+                     if Trace_Generate > Detail then
+                        Ada.Text_IO.Put_Line
+                          ("  state" & Unknown_State_Index'Image 
(Checking_State) &
+                             " adding goto on " & Image (Symbol, Descriptor) & 
" to state" &
+                             Unknown_State_Index'Image (Found_State));
+
+                     end if;
+
+                     Kernels (Checking_State).Goto_List.Insert ((Symbol, 
Found_State));
+                  end if;
+               end if;
+            end if;
+         end loop;
+      end loop;
+
+      if Trace_Generate > Detail then
+         Ada.Text_IO.New_Line;
+      end if;
+
+      return Kernels;
+   end LALR_Kernels;
+
+   --  Add a propagation entry (if it doesn't already exist) from From in
+   --  From_Set to To_Item.
+   procedure Add_Propagation
+     (From         : in     LR1_Items.Item;
+      From_Set     : in     LR1_Items.Item_Set;
+      To_Item      : in     LR1_Items.Item_Lists.Cursor;
+      Propagations : in out Item_Map_Lists.List)
+   is
+      use Item_Map_Lists;
+      use Item_List_Cursor_Lists;
+      use LR1_Items;
+      use LR1_Items.Item_Lists;
+
+      From_Cur : constant Item_Lists.Cursor := Find (From, From_Set);
+
+      From_Match : Item_Map_Lists.Cursor := Propagations.First;
+      To_Match   : Item_List_Cursor_Lists.Cursor;
+   begin
+      Find_From :
+      loop
+         exit Find_From when not Has_Element (From_Match);
+
+         declare
+            Map : Item_Map renames Constant_Ref (From_Match);
+         begin
+            if From_Cur = Map.From then
+
+               To_Match := Map.To.First;
+               loop
+                  exit when not Has_Element (To_Match);
+
+                  declare
+                     use all type SAL.Compare_Result;
+                     Cur       : Item_Lists.Cursor renames Constant_Ref 
(To_Match);
+                     Test_Item : LR1_Items.Item renames Constant_Ref (Cur);
+                  begin
+                     if Equal = LR1_Items.Item_Compare (Test_Item, 
Constant_Ref (To_Item)) then
+                        exit Find_From;
+                     end if;
+                  end;
+                  Next (To_Match);
+               end loop;
+               exit Find_From;
+            end if;
+         end;
+
+         Next (From_Match);
+      end loop Find_From;
+
+      if not Has_Element (From_Match) then
+         Propagations.Append ((From_Cur, To_List (To_Item)));
+
+      elsif not Has_Element (To_Match) then
+         Ref (From_Match).To.Append (To_Item);
+
+      else
+         raise SAL.Programmer_Error with "Add_Propagation: unexpected case";
+      end if;
+   end Add_Propagation;
+
+   --  Calculate the lookaheads from Closure_Item for Source_Item.
+   --  Source_Item must be one of the kernel items in Source_Set.
+   --  Closure_Item must be an item in the lookahead closure of Source_Item 
for #.
+   --
+   --  Spontaneous lookaheads are put in Source_Item.Lookahead,
+   --  propagated lookaheads in Propagations.
+   --
+   --  Set Used_Tokens = True for all tokens in lookaheads.
+   procedure Generate_Lookahead_Info
+     (Source_Item  : in     LR1_Items.Item;
+      Source_Set   : in     LR1_Items.Item_Set;
+      Closure_Item : in     LR1_Items.Item;
+      Propagations : in out Item_Map_Lists.List;
+      Descriptor   : in     WisiToken.Descriptor;
+      Grammar      : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Kernels      : in out LR1_Items.Item_Set_List)
+   is
+      use LR1_Items;
+      use LR1_Items.Item_Lists;
+      use Token_ID_Arrays;
+
+      Spontaneous_Count : Integer := 0;
+   begin
+      if Trace_Generate > Outline then
+         Ada.Text_IO.Put_Line ("  closure_item: ");
+         LR1_Items.Put (Grammar, Descriptor, Closure_Item);
+         Ada.Text_IO.New_Line;
+      end if;
+
+      if not Has_Element (Closure_Item.Dot) then
+         return;
+      end if;
+
+      declare
+         ID         : constant Token_ID               := Element 
(Closure_Item.Dot);
+         Next_Dot   : constant Token_ID_Arrays.Cursor := Next 
(Closure_Item.Dot);
+         Goto_State : constant Unknown_State_Index    := LR1_Items.Goto_State 
(Source_Set, ID);
+         To_Item    : constant Item_Lists.Cursor      :=
+           (if Goto_State = Unknown_State then Item_Lists.No_Element
+            else LR1_Items.Find (Closure_Item.Prod, Next_Dot, Kernels 
(Goto_State)));
+      begin
+         if Closure_Item.Lookaheads (Descriptor.Last_Lookahead) and 
Has_Element (To_Item) then
+            Add_Propagation
+              (From         => Source_Item,
+               From_Set     => Source_Set,
+               To_Item      => To_Item,
+               Propagations => Propagations);
+         end if;
+
+         if Has_Element (To_Item) then
+            if Trace_Generate > Outline then
+               Spontaneous_Count := Spontaneous_Count + 1;
+               Ada.Text_IO.Put_Line ("  spontaneous: " & Lookahead_Image 
(Closure_Item.Lookaheads.all, Descriptor));
+            end if;
+
+            LR1_Items.Include (Ref (To_Item), Closure_Item.Lookaheads.all, 
Descriptor);
+         end if;
+      end;
+   end Generate_Lookahead_Info;
+
+   procedure Propagate_Lookaheads
+     (List       : in Item_Map_Lists.List;
+      Descriptor : in WisiToken.Descriptor)
+   is
+      --  In List, update all To lookaheads from From lookaheads,
+      --  recursively.
+
+      use LR1_Items.Item_Lists;
+
+      More_To_Check : Boolean := True;
+      Added_One     : Boolean;
+   begin
+      while More_To_Check loop
+
+         More_To_Check := False;
+         for Mapping of List loop
+            for Copy of Mapping.To loop
+               LR1_Items.Include (Ref (Copy), Constant_Ref 
(Mapping.From).Lookaheads.all, Added_One, Descriptor);
+
+               More_To_Check := More_To_Check or Added_One;
+            end loop;
+         end loop;
+      end loop;
+   end Propagate_Lookaheads;
+
+   --  Calculate the LALR(1) lookaheads for Grammar.
+   --  Kernels should be the sets of LR(0) kernels on input, and will
+   --  become the set of LALR(1) kernels on output.
+   procedure Fill_In_Lookaheads
+     (Grammar                 : in     
WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production    : in     Token_ID_Set;
+      First_Terminal_Sequence : in     Token_Sequence_Arrays.Vector;
+      Kernels                 : in out LR1_Items.Item_Set_List;
+      Descriptor              : in     WisiToken.Descriptor)
+   is
+      pragma Warnings (Off, """Kernel_Item_Set"" is not modified, could be 
declared constant");
+      --  WORKAROUND: GNAT GPL 2018 complains Kernel_Item_Set could be a 
constant, but
+      --  when we declare that, it complains the target of the assignment of
+      --  .Prod, .Dot below must be a variable.
+
+      Kernel_Item_Set : LR1_Items.Item_Set := -- used for temporary arg to 
Closure
+        (Set            => LR1_Items.Item_Lists.To_List
+           ((Prod       => <>,
+             Dot        => <>,
+             Lookaheads => Propagate_Lookahead (Descriptor))),
+         Goto_List      => <>,
+         Dot_IDs        => <>,
+         State          => <>);
+
+      Closure : LR1_Items.Item_Set;
+
+      Propagation_List : Item_Map_Lists.List;
+
+   begin
+      for Kernel of Kernels loop
+         if Trace_Generate > Outline then
+            Ada.Text_IO.Put ("Adding lookaheads for ");
+            LR1_Items.Put (Grammar, Descriptor, Kernel);
+         end if;
+
+         for Kernel_Item of Kernel.Set loop
+            Kernel_Item_Set.Set (Kernel_Item_Set.Set.First).Prod := 
Kernel_Item.Prod;
+            Kernel_Item_Set.Set (Kernel_Item_Set.Set.First).Dot  := 
Kernel_Item.Dot;
+
+            Closure := LR1_Items.Closure
+              (Kernel_Item_Set, Has_Empty_Production, First_Terminal_Sequence, 
Grammar, Descriptor);
+
+            for Closure_Item of Closure.Set loop
+               Generate_Lookahead_Info
+                 (Kernel_Item, Kernel, Closure_Item, Propagation_List, 
Descriptor, Grammar, Kernels);
+            end loop;
+         end loop;
+      end loop;
+
+      if Trace_Generate > Outline then
+         Ada.Text_IO.New_Line;
+         Ada.Text_IO.Put_Line ("Propagations:");
+         Put (Grammar, Descriptor, Propagation_List);
+         Ada.Text_IO.New_Line;
+      end if;
+
+      Propagate_Lookaheads (Propagation_List, Descriptor);
+   end Fill_In_Lookaheads;
+
+   --  Add actions for all Kernels to Table.
+   procedure Add_Actions
+     (Kernels                 : in     LR1_Items.Item_Set_List;
+      Grammar                 : in     
WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production    : in     Token_ID_Set;
+      First_Nonterm_Set       : in     Token_Array_Token_Set;
+      First_Terminal_Sequence : in     Token_Sequence_Arrays.Vector;
+      Conflict_Counts         :    out Conflict_Count_Lists.List;
+      Conflicts               :    out Conflict_Lists.List;
+      Table                   : in out Parse_Table;
+      Descriptor              : in     WisiToken.Descriptor)
+   is
+      Closure : LR1_Items.Item_Set;
+   begin
+      for Kernel of Kernels loop
+         --  IMPROVEME: there are three "closure" computations that could
+         --  probably be refactored to save computation; in
+         --  LALR_Goto_Transitions, Fill_In_Lookaheads, and here.
+         Closure := LR1_Items.Closure (Kernel, Has_Empty_Production, 
First_Terminal_Sequence, Grammar, Descriptor);
+
+         Add_Actions
+           (Closure, Table, Grammar, Has_Empty_Production, First_Nonterm_Set,
+            Conflict_Counts, Conflicts, Descriptor);
+      end loop;
+
+      if Trace_Generate > Detail then
+         Ada.Text_IO.New_Line;
+      end if;
+   end Add_Actions;
+
+   function Generate
+     (Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor        : in WisiToken.Descriptor;
+      Known_Conflicts   : in Conflict_Lists.List := Conflict_Lists.Empty_List;
+      McKenzie_Param    : in McKenzie_Param_Type := Default_McKenzie_Param;
+      Put_Parse_Table   : in Boolean             := False;
+      Include_Extra     : in Boolean             := False;
+      Ignore_Conflicts  : in Boolean             := False;
+      Partial_Recursion : in Boolean             := True)
+     return Parse_Table_Ptr
+   is
+      use all type Ada.Containers.Count_Type;
+
+      Ignore_Unused_Tokens     : constant Boolean := WisiToken.Trace_Generate 
> Detail;
+      Ignore_Unknown_Conflicts : constant Boolean := Ignore_Conflicts or 
WisiToken.Trace_Generate > Detail;
+      Unused_Tokens            : constant Boolean := 
WisiToken.Generate.Check_Unused_Tokens (Descriptor, Grammar);
+
+      Table : Parse_Table_Ptr;
+
+      Has_Empty_Production : constant Token_ID_Set := 
WisiToken.Generate.Has_Empty_Production (Grammar);
+
+      Recursions : constant WisiToken.Generate.Recursions :=
+        (if Partial_Recursion
+         then WisiToken.Generate.Compute_Partial_Recursion (Grammar)
+         else WisiToken.Generate.Compute_Full_Recursion (Grammar));
+      Minimal_Terminal_Sequences : constant Minimal_Sequence_Array :=
+        Compute_Minimal_Terminal_Sequences (Descriptor, Grammar, Recursions);
+
+      Minimal_Terminal_First : constant Token_Array_Token_ID :=
+        Compute_Minimal_Terminal_First (Descriptor, 
Minimal_Terminal_Sequences);
+
+      First_Nonterm_Set : constant Token_Array_Token_Set := 
WisiToken.Generate.First
+        (Grammar, Has_Empty_Production, Descriptor.First_Terminal);
+
+      First_Terminal_Sequence : constant Token_Sequence_Arrays.Vector :=
+        WisiToken.Generate.To_Terminal_Sequence_Array (First_Nonterm_Set, 
Descriptor);
+
+      Kernels : LR1_Items.Item_Set_List := LALR_Kernels (Grammar, 
First_Nonterm_Set, Descriptor);
+
+      Conflict_Counts      : Conflict_Count_Lists.List;
+      Unknown_Conflicts    : Conflict_Lists.List;
+      Known_Conflicts_Edit : Conflict_Lists.List := Known_Conflicts;
+
+   begin
+      WisiToken.Generate.Error := False; -- necessary in unit tests; some 
previous test might have encountered an error.
+
+      Fill_In_Lookaheads (Grammar, Has_Empty_Production, 
First_Terminal_Sequence, Kernels, Descriptor);
+
+      if Unused_Tokens then
+         WisiToken.Generate.Error := not Ignore_Unused_Tokens;
+         Ada.Text_IO.New_Line;
+      end if;
+
+      if Trace_Generate > Detail then
+         Ada.Text_IO.New_Line;
+         Ada.Text_IO.Put_Line ("LR(1) Kernels:");
+         LR1_Items.Put (Grammar, Descriptor, Kernels, Show_Lookaheads => True);
+      end if;
+
+      Table := new Parse_Table
+        (State_First       => Kernels.First_Index,
+         State_Last        => Kernels.Last_Index,
+         First_Terminal    => Descriptor.First_Terminal,
+         Last_Terminal     => Descriptor.Last_Terminal,
+         First_Nonterminal => Descriptor.First_Nonterminal,
+         Last_Nonterminal  => Descriptor.Last_Nonterminal);
+
+      if McKenzie_Param = Default_McKenzie_Param then
+         --  Descriminants in Default are wrong
+         Table.McKenzie_Param :=
+           (First_Terminal              => Descriptor.First_Terminal,
+            Last_Terminal               => Descriptor.Last_Terminal,
+            First_Nonterminal           => Descriptor.First_Nonterminal,
+            Last_Nonterminal            => Descriptor.Last_Nonterminal,
+            Insert                      => (others => 0),
+            Delete                      => (others => 0),
+            Push_Back                   => (others => 0),
+            Undo_Reduce                 => (others => 0),
+            Minimal_Complete_Cost_Delta => 
Default_McKenzie_Param.Minimal_Complete_Cost_Delta,
+            Fast_Forward                => Default_McKenzie_Param.Fast_Forward,
+            Matching_Begin              => 
Default_McKenzie_Param.Matching_Begin,
+            Ignore_Check_Fail           => 
Default_McKenzie_Param.Ignore_Check_Fail,
+            Task_Count                  => Default_McKenzie_Param.Task_Count,
+            Check_Limit                 => Default_McKenzie_Param.Check_Limit,
+            Check_Delta_Limit           => 
Default_McKenzie_Param.Check_Delta_Limit,
+            Enqueue_Limit               => 
Default_McKenzie_Param.Enqueue_Limit);
+      else
+         Table.McKenzie_Param := McKenzie_Param;
+      end if;
+
+      Add_Actions
+        (Kernels, Grammar, Has_Empty_Production, First_Nonterm_Set, 
First_Terminal_Sequence, Conflict_Counts,
+         Unknown_Conflicts, Table.all, Descriptor);
+
+      for State in Table.States'Range loop
+         if Trace_Generate > Extra then
+            Ada.Text_IO.Put_Line ("Set_Minimal_Complete_Actions:" & 
State_Index'Image (State));
+         end if;
+         WisiToken.Generate.LR.Set_Minimal_Complete_Actions
+           (Table.States (State), Kernels (State), Descriptor, Grammar, 
Minimal_Terminal_Sequences,
+            Minimal_Terminal_First);
+      end loop;
+
+      if Put_Parse_Table then
+         WisiToken.Generate.LR.Put_Parse_Table
+           (Table, "LALR", Grammar, Recursions, Minimal_Terminal_Sequences, 
Kernels, Conflict_Counts, Descriptor,
+            Include_Extra);
+      end if;
+
+      Delete_Known (Unknown_Conflicts, Known_Conflicts_Edit);
+
+      if Unknown_Conflicts.Length > 0 then
+         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "unknown 
conflicts:");
+         Put (Unknown_Conflicts, Ada.Text_IO.Current_Error, Descriptor);
+         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
+         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
+      end if;
+
+      if Known_Conflicts_Edit.Length > 0 then
+         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "excess known 
conflicts:");
+         Put (Known_Conflicts_Edit, Ada.Text_IO.Current_Error, Descriptor);
+         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
+         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
+      end if;
+
+      return Table;
+   end Generate;
+
+end WisiToken.Generate.LR.LALR_Generate;
diff --git a/wisitoken-generate-lr-lalr_generate.ads 
b/wisitoken-generate-lr-lalr_generate.ads
index b4a109f..9e33931 100644
--- a/wisitoken-generate-lr-lalr_generate.ads
+++ b/wisitoken-generate-lr-lalr_generate.ads
@@ -25,11 +25,14 @@ with WisiToken.Productions;
 package WisiToken.Generate.LR.LALR_Generate is
 
    function Generate
-     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor      : in WisiToken.Descriptor;
-      Known_Conflicts : in Conflict_Lists.List := Conflict_Lists.Empty_List;
-      McKenzie_Param  : in McKenzie_Param_Type := Default_McKenzie_Param;
-      Put_Parse_Table : in Boolean := False)
+     (Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor        : in WisiToken.Descriptor;
+      Known_Conflicts   : in Conflict_Lists.List := Conflict_Lists.Empty_List;
+      McKenzie_Param    : in McKenzie_Param_Type := Default_McKenzie_Param;
+      Put_Parse_Table   : in Boolean             := False;
+      Include_Extra     : in Boolean             := False;
+      Ignore_Conflicts  : in Boolean             := False;
+      Partial_Recursion : in Boolean             := True)
      return Parse_Table_Ptr
    with Pre =>
      Descriptor.Last_Lookahead = Descriptor.First_Nonterminal and
@@ -76,6 +79,7 @@ package WisiToken.Generate.LR.LALR_Generate is
       Has_Empty_Production    : in     Token_ID_Set;
       First_Nonterm_Set       : in     Token_Array_Token_Set;
       First_Terminal_Sequence : in     Token_Sequence_Arrays.Vector;
+      Conflict_Counts         :    out Conflict_Count_Lists.List;
       Conflicts               :    out Conflict_Lists.List;
       Table                   : in out Parse_Table;
       Descriptor              : in     WisiToken.Descriptor);
diff --git a/wisitoken-generate-lr-lr1_generate.adb 
b/wisitoken-generate-lr-lr1_generate.adb
index 62c20c1..0f177d9 100644
--- a/wisitoken-generate-lr-lr1_generate.adb
+++ b/wisitoken-generate-lr-lr1_generate.adb
@@ -1,321 +1,331 @@
---  Abstract :
---
---  See spec.
---
---  Copyright (C) 2017 - 2019 Free Software Foundation, Inc.
---
---  This file is part of the WisiToken package.
---
---  The WisiToken package is free software; you can redistribute it
---  and/or modify it under 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 library is distributed in
---  the hope that it will be useful, but WITHOUT ANY WARRANTY; without
---  even the implied warranty of MERCHAN- TABILITY or FITNESS FOR A
---  PARTICULAR PURPOSE.
---
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with Ada.Containers;
-with Ada.Text_IO;
-with WisiToken.Generate;
-package body WisiToken.Generate.LR.LR1_Generate is
-
-   function LR1_Goto_Transitions
-     (Set                     : in LR1_Items.Item_Set;
-      Symbol                  : in Token_ID;
-      Has_Empty_Production    : in Token_ID_Set;
-      First_Terminal_Sequence : in Token_Sequence_Arrays.Vector;
-      Grammar                 : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor              : in WisiToken.Descriptor)
-     return LR1_Items.Item_Set
-   is
-      use all type Ada.Containers.Count_Type;
-      use Token_ID_Arrays;
-      use LR1_Items;
-
-      Goto_Set : Item_Set;
-   begin
-      for Item of Set.Set loop
-         if Item.Dot /= No_Element then
-            if Element (Item.Dot) = Symbol and
-              --  We don't need a state with dot after EOI in the
-              --  accept production. EOI should only appear in the
-              --  accept production.
-              Symbol /= Descriptor.EOI_ID
-            then
-               Goto_Set.Set.Insert ((Item.Prod, Next (Item.Dot), new 
Token_ID_Set'(Item.Lookaheads.all)));
-            end if;
-         end if;
-      end loop;
-
-      if Goto_Set.Set.Length > 0 then
-         return Closure (Goto_Set, Has_Empty_Production, 
First_Terminal_Sequence, Grammar, Descriptor);
-      else
-         return Goto_Set;
-      end if;
-   end LR1_Goto_Transitions;
-
-   function LR1_Item_Sets
-     (Has_Empty_Production    : in Token_ID_Set;
-      First_Terminal_Sequence : in Token_Sequence_Arrays.Vector;
-      Grammar                 : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor              : in WisiToken.Descriptor)
-     return LR1_Items.Item_Set_List
-   is
-      use all type Ada.Containers.Count_Type;
-
-      --  [dragon] algorithm 4.9 pg 231; figure 4.38 pg 232; procedure
-      --  "items", with some optimizations.
-
-      use LR1_Items;
-
-      First_State_Index : constant State_Index := 0;
-
-      C               : LR1_Items.Item_Set_List;       -- result
-      C_Tree          : LR1_Items.Item_Set_Trees.Tree; -- for fast find
-      States_To_Check : State_Index_Queues.Queue;
-      --  [dragon] specifies 'until no more items can be added', but we use
-      --  a queue to avoid checking unecessary states. Ada LR1 has over
-      --  100,000 states, so this is a significant gain (reduced time from
-      --  600 seconds to 40).
-
-      I       : State_Index;
-      Dot_IDs : Token_ID_Arrays.Vector;
-
-      New_Item_Set : Item_Set := Closure
-        ((Set            => Item_Lists.To_List
-            ((Prod       => (Grammar.First_Index, 0),
-              Dot        => Grammar (Grammar.First_Index).RHSs 
(0).Tokens.First,
-              Lookaheads => new Token_ID_Set'(To_Lookahead (Descriptor.EOI_ID, 
Descriptor)))),
-          Goto_List      => <>,
-          Dot_IDs        => <>,
-          State          => First_State_Index),
-        Has_Empty_Production, First_Terminal_Sequence, Grammar, Descriptor);
-
-      Found_State  : Unknown_State_Index;
-
-   begin
-      C.Set_First (First_State_Index);
-
-      Add (New_Item_Set, C, C_Tree, Descriptor, Include_Lookaheads => True);
-
-      States_To_Check.Put (First_State_Index);
-      loop
-         exit when States_To_Check.Is_Empty;
-         I := States_To_Check.Get;
-
-         if Trace_Generate > Outline then
-            Ada.Text_IO.Put ("Checking ");
-            Put (Grammar, Descriptor, C (I), Show_Lookaheads => True, 
Show_Goto_List => True);
-         end if;
-
-         Dot_IDs := C (I).Dot_IDs;
-         --  We can't iterate on C (I).Dot_IDs when the loop adds items to C;
-         --  it might be reallocated to grow.
-
-         for Symbol of Dot_IDs loop
-            --  [dragon] has 'for each grammar symbol X', but 
LR1_Goto_Transitions
-            --  rejects Symbol that is not in Dot_IDs, so we iterate over that.
-
-            New_Item_Set := LR1_Goto_Transitions
-              (C (I), Symbol, Has_Empty_Production, First_Terminal_Sequence, 
Grammar, Descriptor);
-
-            if New_Item_Set.Set.Length > 0 then -- 'goto (I, X) not empty'
-
-               Found_State := Find (New_Item_Set, C_Tree, Match_Lookaheads => 
True); -- 'not in C'
-
-               if Found_State = Unknown_State then
-                  New_Item_Set.State := C.Last_Index + 1;
-
-                  States_To_Check.Put (New_Item_Set.State);
-
-                  Add (New_Item_Set, C, C_Tree, Descriptor, Include_Lookaheads 
=> True);
-
-                  if Trace_Generate > Outline then
-                     Ada.Text_IO.Put_Line
-                       ("  adding state" & Unknown_State_Index'Image 
(C.Last_Index) & ": from state" &
-                          Unknown_State_Index'Image (I) & " on " & Image 
(Symbol, Descriptor));
-                     Put (Grammar, Descriptor, New_Item_Set, Show_Lookaheads 
=> True);
-                  end if;
-
-                  C (I).Goto_List.Insert ((Symbol, C.Last_Index));
-               else
-
-                  --  If there's not already a goto entry between these two 
sets, create one.
-                  if not Is_In ((Symbol, Found_State), Goto_List => C 
(I).Goto_List) then
-                     if Trace_Generate > Outline then
-                        Ada.Text_IO.Put_Line
-                          ("  adding goto on " & Image (Symbol, Descriptor) & 
" to state" &
-                             Unknown_State_Index'Image (Found_State));
-
-                     end if;
-
-                     C (I).Goto_List.Insert ((Symbol, Found_State));
-                  end if;
-               end if;
-            end if;
-         end loop;
-      end loop;
-
-      if Trace_Generate > Outline then
-         Ada.Text_IO.New_Line;
-      end if;
-
-      return C;
-   end LR1_Item_Sets;
-
-   procedure Add_Actions
-     (Item_Sets            : in     LR1_Items.Item_Set_List;
-      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
-      Has_Empty_Production : in     Token_ID_Set;
-      First_Nonterm_Set    : in     Token_Array_Token_Set;
-      Conflicts            :    out Conflict_Lists.List;
-      Table                : in out Parse_Table;
-      Descriptor           : in     WisiToken.Descriptor)
-   is
-      --  Add actions for all Item_Sets to Table.
-   begin
-      for Item_Set of Item_Sets loop
-         Add_Actions (Item_Set, Table, Grammar, Has_Empty_Production, 
First_Nonterm_Set, Conflicts, Descriptor);
-      end loop;
-
-      if Trace_Generate > Outline then
-         Ada.Text_IO.New_Line;
-      end if;
-   end Add_Actions;
-
-   function Generate
-     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor      : in WisiToken.Descriptor;
-      Known_Conflicts : in Conflict_Lists.List := Conflict_Lists.Empty_List;
-      McKenzie_Param  : in McKenzie_Param_Type := Default_McKenzie_Param;
-      Put_Parse_Table : in Boolean := False)
-     return Parse_Table_Ptr
-   is
-      use type Ada.Containers.Count_Type;
-
-      Ignore_Unused_Tokens     : constant Boolean := WisiToken.Trace_Generate 
> Detail;
-      Ignore_Unknown_Conflicts : constant Boolean := WisiToken.Trace_Generate 
> Detail;
-      Unused_Tokens            : constant Boolean := 
WisiToken.Generate.Check_Unused_Tokens (Descriptor, Grammar);
-
-      Table : Parse_Table_Ptr;
-
-      Has_Empty_Production : constant Token_ID_Set := 
WisiToken.Generate.Has_Empty_Production (Grammar);
-
-      Minimal_Terminal_Sequences : constant Minimal_Sequence_Array :=
-        Compute_Minimal_Terminal_Sequences (Descriptor, Grammar);
-
-      Minimal_Terminal_First : constant Token_Array_Token_ID :=
-        Compute_Minimal_Terminal_First (Descriptor, 
Minimal_Terminal_Sequences);
-
-      First_Nonterm_Set : constant Token_Array_Token_Set := 
WisiToken.Generate.First
-        (Grammar, Has_Empty_Production, Descriptor.First_Terminal);
-
-      First_Terminal_Sequence : constant Token_Sequence_Arrays.Vector :=
-        WisiToken.Generate.To_Terminal_Sequence_Array (First_Nonterm_Set, 
Descriptor);
-
-      Item_Sets : constant LR1_Items.Item_Set_List := LR1_Item_Sets
-        (Has_Empty_Production, First_Terminal_Sequence, Grammar, Descriptor);
-
-      Unknown_Conflicts    : Conflict_Lists.List;
-      Known_Conflicts_Edit : Conflict_Lists.List := Known_Conflicts;
-   begin
-      if Trace_Generate > Outline then
-         Ada.Text_IO.New_Line;
-         Ada.Text_IO.Put_Line ("LR(1) Item_Sets:");
-         LR1_Items.Put (Grammar, Descriptor, Item_Sets);
-      end if;
-
-      Table := new Parse_Table
-        (State_First       => Item_Sets.First_Index,
-         State_Last        => Item_Sets.Last_Index,
-         First_Terminal    => Descriptor.First_Terminal,
-         Last_Terminal     => Descriptor.Last_Terminal,
-         First_Nonterminal => Descriptor.First_Nonterminal,
-         Last_Nonterminal  => Descriptor.Last_Nonterminal);
-
-      if McKenzie_Param = Default_McKenzie_Param then
-         --  Descriminants in Default are wrong
-         Table.McKenzie_Param :=
-           (First_Terminal    => Descriptor.First_Terminal,
-            Last_Terminal     => Descriptor.Last_Terminal,
-            First_Nonterminal => Descriptor.First_Nonterminal,
-            Last_Nonterminal  => Descriptor.Last_Nonterminal,
-            Insert            => (others => 0),
-            Delete            => (others => 0),
-            Push_Back         => (others => 0),
-            Ignore_Check_Fail => Default_McKenzie_Param.Ignore_Check_Fail,
-            Task_Count        => Default_McKenzie_Param.Task_Count,
-            Cost_Limit        => Default_McKenzie_Param.Cost_Limit,
-            Check_Limit       => Default_McKenzie_Param.Check_Limit,
-            Check_Delta_Limit => Default_McKenzie_Param.Check_Delta_Limit,
-            Enqueue_Limit     => Default_McKenzie_Param.Enqueue_Limit);
-      else
-         Table.McKenzie_Param := McKenzie_Param;
-      end if;
-
-      Add_Actions
-        (Item_Sets, Grammar, Has_Empty_Production, First_Nonterm_Set, 
Unknown_Conflicts, Table.all, Descriptor);
-
-      --  Set Table.States.Productions, Minimal_Complete_Actions for 
McKenzie_Recover
-      for State in Table.States'Range loop
-         Table.States (State).Productions := LR1_Items.Productions
-           (LR1_Items.Filter (Item_Sets (State), Grammar, Descriptor, 
LR1_Items.In_Kernel'Access));
-
-         if Trace_Generate > Detail then
-            Ada.Text_IO.Put_Line ("Set_Minimal_Complete_Actions:" & 
State_Index'Image (State));
-         end if;
-
-         WisiToken.Generate.LR.Set_Minimal_Complete_Actions
-           (Table.States (State),
-            LR1_Items.Filter (Item_Sets (State), Grammar, Descriptor, 
LR1_Items.In_Kernel'Access),
-            Descriptor, Grammar, Minimal_Terminal_Sequences, 
Minimal_Terminal_First);
-      end loop;
-
-      if Put_Parse_Table then
-         WisiToken.Generate.LR.Put_Parse_Table
-           (Table, "LR1", Grammar, Item_Sets, Unknown_Conflicts, Descriptor);
-      end if;
-
-      if Trace_Generate > Outline then
-         Ada.Text_IO.New_Line;
-         Ada.Text_IO.Put_Line ("Has_Empty_Production: " & Image 
(Has_Empty_Production, Descriptor));
-
-         Ada.Text_IO.New_Line;
-         Ada.Text_IO.Put_Line ("Minimal_Terminal_First:");
-         for ID in Minimal_Terminal_First'Range loop
-            Ada.Text_IO.Put_Line
-              (Image (ID, Descriptor) & " =>" &
-                 (if Minimal_Terminal_First (ID) = Invalid_Token_ID
-                  then ""
-                  else ' ' & Image (Minimal_Terminal_First (ID), Descriptor)));
-         end loop;
-      end if;
-
-      Delete_Known (Unknown_Conflicts, Known_Conflicts_Edit);
-
-      if Unknown_Conflicts.Length > 0 then
-         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "unknown 
conflicts:");
-         Put (Unknown_Conflicts, Ada.Text_IO.Current_Error, Descriptor);
-         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
-         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
-      end if;
-
-      if Known_Conflicts_Edit.Length > 0 then
-         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "excess known 
conflicts:");
-         Put (Known_Conflicts_Edit, Ada.Text_IO.Current_Error, Descriptor);
-         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
-         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
-      end if;
-
-      WisiToken.Generate.Error := WisiToken.Generate.Error or (Unused_Tokens 
and not Ignore_Unused_Tokens);
-
-      return Table;
-   end Generate;
-
-end WisiToken.Generate.LR.LR1_Generate;
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017 - 2019 Free Software Foundation, Inc.
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under 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 library is distributed in
+--  the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+--  even the implied warranty of MERCHAN- TABILITY or FITNESS FOR A
+--  PARTICULAR PURPOSE.
+--
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Containers;
+with Ada.Text_IO;
+with WisiToken.Generate;
+package body WisiToken.Generate.LR.LR1_Generate is
+
+   function LR1_Goto_Transitions
+     (Set                     : in LR1_Items.Item_Set;
+      Symbol                  : in Token_ID;
+      Has_Empty_Production    : in Token_ID_Set;
+      First_Terminal_Sequence : in Token_Sequence_Arrays.Vector;
+      Grammar                 : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor              : in WisiToken.Descriptor)
+     return LR1_Items.Item_Set
+   is
+      use all type Ada.Containers.Count_Type;
+      use Token_ID_Arrays;
+      use LR1_Items;
+
+      Goto_Set : Item_Set;
+   begin
+      for Item of Set.Set loop
+         if Item.Dot /= No_Element then
+            if Element (Item.Dot) = Symbol and
+              --  We don't need a state with dot after EOI in the
+              --  accept production. EOI should only appear in the
+              --  accept production.
+              Symbol /= Descriptor.EOI_ID
+            then
+               Goto_Set.Set.Insert ((Item.Prod, Next (Item.Dot), new 
Token_ID_Set'(Item.Lookaheads.all)));
+            end if;
+         end if;
+      end loop;
+
+      if Goto_Set.Set.Length > 0 then
+         return Closure (Goto_Set, Has_Empty_Production, 
First_Terminal_Sequence, Grammar, Descriptor);
+      else
+         return Goto_Set;
+      end if;
+   end LR1_Goto_Transitions;
+
+   function LR1_Item_Sets
+     (Has_Empty_Production    : in Token_ID_Set;
+      First_Terminal_Sequence : in Token_Sequence_Arrays.Vector;
+      Grammar                 : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor              : in WisiToken.Descriptor)
+     return LR1_Items.Item_Set_List
+   is
+      use all type Ada.Containers.Count_Type;
+
+      --  [dragon] algorithm 4.9 pg 231; figure 4.38 pg 232; procedure
+      --  "items", with some optimizations.
+
+      use LR1_Items;
+
+      First_State_Index : constant State_Index := 0;
+
+      C               : LR1_Items.Item_Set_List;       -- result
+      C_Tree          : LR1_Items.Item_Set_Trees.Tree; -- for fast find
+      States_To_Check : State_Index_Queues.Queue;
+      --  [dragon] specifies 'until no more items can be added', but we use
+      --  a queue to avoid checking unecessary states. Ada LR1 has over
+      --  100,000 states, so this is a significant gain (reduced time from
+      --  600 seconds to 40).
+
+      I       : State_Index;
+      Dot_IDs : Token_ID_Arrays.Vector;
+
+      New_Item_Set : Item_Set := Closure
+        ((Set            => Item_Lists.To_List
+            ((Prod       => (Grammar.First_Index, 0),
+              Dot        => Grammar (Grammar.First_Index).RHSs 
(0).Tokens.First,
+              Lookaheads => new Token_ID_Set'(To_Lookahead (Descriptor.EOI_ID, 
Descriptor)))),
+          Goto_List      => <>,
+          Dot_IDs        => <>,
+          State          => First_State_Index),
+        Has_Empty_Production, First_Terminal_Sequence, Grammar, Descriptor);
+
+      Found_State  : Unknown_State_Index;
+
+   begin
+      C.Set_First (First_State_Index);
+
+      Add (New_Item_Set, C, C_Tree, Descriptor, Include_Lookaheads => True);
+
+      States_To_Check.Put (First_State_Index);
+      loop
+         exit when States_To_Check.Is_Empty;
+         I := States_To_Check.Get;
+
+         if Trace_Generate > Outline then
+            Ada.Text_IO.Put ("Checking ");
+            Put (Grammar, Descriptor, C (I), Show_Lookaheads => True, 
Show_Goto_List => True);
+         end if;
+
+         Dot_IDs := C (I).Dot_IDs;
+         --  We can't iterate on C (I).Dot_IDs when the loop adds items to C;
+         --  it might be reallocated to grow.
+
+         for Symbol of Dot_IDs loop
+            --  [dragon] has 'for each grammar symbol X', but 
LR1_Goto_Transitions
+            --  rejects Symbol that is not in Dot_IDs, so we iterate over that.
+
+            New_Item_Set := LR1_Goto_Transitions
+              (C (I), Symbol, Has_Empty_Production, First_Terminal_Sequence, 
Grammar, Descriptor);
+
+            if New_Item_Set.Set.Length > 0 then -- 'goto (I, X) not empty'
+
+               Found_State := Find (New_Item_Set, C_Tree, Match_Lookaheads => 
True); -- 'not in C'
+
+               if Found_State = Unknown_State then
+                  New_Item_Set.State := C.Last_Index + 1;
+
+                  States_To_Check.Put (New_Item_Set.State);
+
+                  Add (New_Item_Set, C, C_Tree, Descriptor, Include_Lookaheads 
=> True);
+
+                  if Trace_Generate > Outline then
+                     Ada.Text_IO.Put_Line
+                       ("  adding state" & Unknown_State_Index'Image 
(C.Last_Index) & ": from state" &
+                          Unknown_State_Index'Image (I) & " on " & Image 
(Symbol, Descriptor));
+                     Put (Grammar, Descriptor, New_Item_Set, Show_Lookaheads 
=> True);
+                  end if;
+
+                  C (I).Goto_List.Insert ((Symbol, C.Last_Index));
+               else
+
+                  --  If there's not already a goto entry between these two 
sets, create one.
+                  if not Is_In ((Symbol, Found_State), Goto_List => C 
(I).Goto_List) then
+                     if Trace_Generate > Outline then
+                        Ada.Text_IO.Put_Line
+                          ("  adding goto on " & Image (Symbol, Descriptor) & 
" to state" &
+                             Unknown_State_Index'Image (Found_State));
+
+                     end if;
+
+                     C (I).Goto_List.Insert ((Symbol, Found_State));
+                  end if;
+               end if;
+            end if;
+         end loop;
+      end loop;
+
+      if Trace_Generate > Outline then
+         Ada.Text_IO.New_Line;
+      end if;
+
+      return C;
+   end LR1_Item_Sets;
+
+   procedure Add_Actions
+     (Item_Sets            : in     LR1_Items.Item_Set_List;
+      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in     Token_ID_Set;
+      First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      :    out Conflict_Count_Lists.List;
+      Conflicts            :    out Conflict_Lists.List;
+      Table                : in out Parse_Table;
+      Descriptor           : in     WisiToken.Descriptor)
+   is
+      --  Add actions for all Item_Sets to Table.
+   begin
+      for Item_Set of Item_Sets loop
+         Add_Actions
+           (Item_Set, Table, Grammar, Has_Empty_Production, First_Nonterm_Set, 
Conflict_Counts, Conflicts, Descriptor);
+      end loop;
+
+      if Trace_Generate > Outline then
+         Ada.Text_IO.New_Line;
+      end if;
+   end Add_Actions;
+
+   function Generate
+     (Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor        : in WisiToken.Descriptor;
+      Known_Conflicts   : in Conflict_Lists.List := Conflict_Lists.Empty_List;
+      McKenzie_Param    : in McKenzie_Param_Type := Default_McKenzie_Param;
+      Put_Parse_Table   : in Boolean             := False;
+      Include_Extra     : in Boolean             := False;
+      Ignore_Conflicts  : in Boolean             := False;
+      Partial_Recursion : in Boolean             := True)
+     return Parse_Table_Ptr
+   is
+      use type Ada.Containers.Count_Type;
+
+      Ignore_Unused_Tokens     : constant Boolean := WisiToken.Trace_Generate 
> Detail;
+      Ignore_Unknown_Conflicts : constant Boolean := Ignore_Conflicts or 
WisiToken.Trace_Generate > Detail;
+      Unused_Tokens            : constant Boolean := 
WisiToken.Generate.Check_Unused_Tokens (Descriptor, Grammar);
+
+      Table : Parse_Table_Ptr;
+
+      Has_Empty_Production : constant Token_ID_Set := 
WisiToken.Generate.Has_Empty_Production (Grammar);
+
+      Recursions : constant WisiToken.Generate.Recursions :=
+        (if Partial_Recursion
+         then WisiToken.Generate.Compute_Partial_Recursion (Grammar)
+         else WisiToken.Generate.Compute_Full_Recursion (Grammar));
+      Minimal_Terminal_Sequences : constant Minimal_Sequence_Array :=
+        Compute_Minimal_Terminal_Sequences (Descriptor, Grammar, Recursions);
+
+      Minimal_Terminal_First : constant Token_Array_Token_ID :=
+        Compute_Minimal_Terminal_First (Descriptor, 
Minimal_Terminal_Sequences);
+
+      First_Nonterm_Set : constant Token_Array_Token_Set := 
WisiToken.Generate.First
+        (Grammar, Has_Empty_Production, Descriptor.First_Terminal);
+
+      First_Terminal_Sequence : constant Token_Sequence_Arrays.Vector :=
+        WisiToken.Generate.To_Terminal_Sequence_Array (First_Nonterm_Set, 
Descriptor);
+
+      Item_Sets : constant LR1_Items.Item_Set_List := LR1_Item_Sets
+        (Has_Empty_Production, First_Terminal_Sequence, Grammar, Descriptor);
+
+      Conflict_Counts      : Conflict_Count_Lists.List;
+      Unknown_Conflicts    : Conflict_Lists.List;
+      Known_Conflicts_Edit : Conflict_Lists.List := Known_Conflicts;
+   begin
+      if Trace_Generate > Outline then
+         Ada.Text_IO.New_Line;
+         Ada.Text_IO.Put_Line ("LR(1) Item_Sets:");
+         LR1_Items.Put (Grammar, Descriptor, Item_Sets);
+      end if;
+
+      Table := new Parse_Table
+        (State_First       => Item_Sets.First_Index,
+         State_Last        => Item_Sets.Last_Index,
+         First_Terminal    => Descriptor.First_Terminal,
+         Last_Terminal     => Descriptor.Last_Terminal,
+         First_Nonterminal => Descriptor.First_Nonterminal,
+         Last_Nonterminal  => Descriptor.Last_Nonterminal);
+
+      if McKenzie_Param = Default_McKenzie_Param then
+         --  Descriminants in Default are wrong
+         Table.McKenzie_Param :=
+           (First_Terminal              => Descriptor.First_Terminal,
+            Last_Terminal               => Descriptor.Last_Terminal,
+            First_Nonterminal           => Descriptor.First_Nonterminal,
+            Last_Nonterminal            => Descriptor.Last_Nonterminal,
+            Insert                      => (others => 0),
+            Delete                      => (others => 0),
+            Push_Back                   => (others => 0),
+            Undo_Reduce                 => (others => 0),
+            Minimal_Complete_Cost_Delta => 
Default_McKenzie_Param.Minimal_Complete_Cost_Delta,
+            Fast_Forward                => Default_McKenzie_Param.Fast_Forward,
+            Matching_Begin              => 
Default_McKenzie_Param.Matching_Begin,
+            Ignore_Check_Fail           => 
Default_McKenzie_Param.Ignore_Check_Fail,
+            Task_Count                  => Default_McKenzie_Param.Task_Count,
+            Check_Limit                 => Default_McKenzie_Param.Check_Limit,
+            Check_Delta_Limit           => 
Default_McKenzie_Param.Check_Delta_Limit,
+            Enqueue_Limit               => 
Default_McKenzie_Param.Enqueue_Limit);
+      else
+         Table.McKenzie_Param := McKenzie_Param;
+      end if;
+
+      Add_Actions
+        (Item_Sets, Grammar, Has_Empty_Production, First_Nonterm_Set,
+         Conflict_Counts, Unknown_Conflicts, Table.all, Descriptor);
+
+      for State in Table.States'Range loop
+         if Trace_Generate > Extra then
+            Ada.Text_IO.Put_Line ("Set_Minimal_Complete_Actions:" & 
State_Index'Image (State));
+         end if;
+         WisiToken.Generate.LR.Set_Minimal_Complete_Actions
+           (Table.States (State),
+            LR1_Items.Filter (Item_Sets (State), Grammar, Descriptor, 
LR1_Items.In_Kernel'Access),
+            Descriptor, Grammar, Minimal_Terminal_Sequences, 
Minimal_Terminal_First);
+      end loop;
+
+      if Put_Parse_Table then
+         WisiToken.Generate.LR.Put_Parse_Table
+           (Table, "LR1", Grammar, Recursions, Minimal_Terminal_Sequences, 
Item_Sets, Conflict_Counts, Descriptor,
+            Include_Extra);
+      end if;
+
+      if Trace_Generate > Outline then
+         Ada.Text_IO.New_Line;
+         Ada.Text_IO.Put_Line ("Has_Empty_Production: " & Image 
(Has_Empty_Production, Descriptor));
+
+         Ada.Text_IO.New_Line;
+         Ada.Text_IO.Put_Line ("Minimal_Terminal_First:");
+         for ID in Minimal_Terminal_First'Range loop
+            Ada.Text_IO.Put_Line
+              (Image (ID, Descriptor) & " =>" &
+                 (if Minimal_Terminal_First (ID) = Invalid_Token_ID
+                  then ""
+                  else ' ' & Image (Minimal_Terminal_First (ID), Descriptor)));
+         end loop;
+      end if;
+
+      Delete_Known (Unknown_Conflicts, Known_Conflicts_Edit);
+
+      if Unknown_Conflicts.Length > 0 then
+         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "unknown 
conflicts:");
+         Put (Unknown_Conflicts, Ada.Text_IO.Current_Error, Descriptor);
+         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
+         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
+      end if;
+
+      if Known_Conflicts_Edit.Length > 0 then
+         Ada.Text_IO.Put_Line (Ada.Text_IO.Current_Error, "excess known 
conflicts:");
+         Put (Known_Conflicts_Edit, Ada.Text_IO.Current_Error, Descriptor);
+         Ada.Text_IO.New_Line (Ada.Text_IO.Current_Error);
+         WisiToken.Generate.Error := WisiToken.Generate.Error or not 
Ignore_Unknown_Conflicts;
+      end if;
+
+      WisiToken.Generate.Error := WisiToken.Generate.Error or (Unused_Tokens 
and not Ignore_Unused_Tokens);
+
+      return Table;
+   end Generate;
+
+end WisiToken.Generate.LR.LR1_Generate;
diff --git a/wisitoken-generate-lr-lr1_generate.ads 
b/wisitoken-generate-lr-lr1_generate.ads
index 92992cb..6e8ade6 100644
--- a/wisitoken-generate-lr-lr1_generate.ads
+++ b/wisitoken-generate-lr-lr1_generate.ads
@@ -30,11 +30,14 @@ with WisiToken.Productions;
 package WisiToken.Generate.LR.LR1_Generate is
 
    function Generate
-     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor      : in WisiToken.Descriptor;
-      Known_Conflicts : in Conflict_Lists.List := Conflict_Lists.Empty_List;
-      McKenzie_Param  : in McKenzie_Param_Type := Default_McKenzie_Param;
-      Put_Parse_Table : in Boolean := False)
+     (Grammar           : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor        : in WisiToken.Descriptor;
+      Known_Conflicts   : in Conflict_Lists.List := Conflict_Lists.Empty_List;
+      McKenzie_Param    : in McKenzie_Param_Type := Default_McKenzie_Param;
+      Put_Parse_Table   : in Boolean             := False;
+      Include_Extra     : in Boolean             := False;
+      Ignore_Conflicts  : in Boolean             := False;
+      Partial_Recursion : in Boolean             := True)
      return Parse_Table_Ptr
    with Pre => Descriptor.First_Nonterminal = Descriptor.Accept_ID;
    --  Generate a generalized LR1 parse table for Grammar. The
@@ -78,6 +81,7 @@ package WisiToken.Generate.LR.LR1_Generate is
       Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
       Has_Empty_Production : in     Token_ID_Set;
       First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      :    out Conflict_Count_Lists.List;
       Conflicts            :    out Conflict_Lists.List;
       Table                : in out Parse_Table;
       Descriptor           : in     WisiToken.Descriptor);
diff --git a/wisitoken-generate-lr.adb b/wisitoken-generate-lr.adb
index f909fde..b814885 100644
--- a/wisitoken-generate-lr.adb
+++ b/wisitoken-generate-lr.adb
@@ -24,30 +24,12 @@ with WisiToken.Generate;
 package body WisiToken.Generate.LR is
 
    package RHS_Set is new SAL.Gen_Unbounded_Definite_Vectors (Natural, 
Boolean, Default_Element => False);
-   type Token_ID_RHS_Set is array (Token_ID range <>) of RHS_Set.Vector;
 
-   type Token_ID_Array_Positive is array (Token_ID range <>) of Positive;
+   type LHS_RHS_Set is array (Token_ID range <>) of RHS_Set.Vector;
 
    ----------
    --  Body subprograms, alphabetical
 
-   function Find
-     (Symbol      : in Token_ID;
-      Action_List : in Action_Node_Ptr)
-     return Action_Node_Ptr
-   is
-      Action_Node : Action_Node_Ptr := Action_List;
-   begin
-      while Action_Node /= null loop
-         if Action_Node.Symbol = Symbol then
-            return Action_Node;
-         end if;
-         Action_Node := Action_Node.Next;
-      end loop;
-
-      return null;
-   end Find;
-
    function Min
      (Item    : in RHS_Sequence_Arrays.Vector;
       RHS_Set : in LR.RHS_Set.Vector)
@@ -70,15 +52,72 @@ package body WisiToken.Generate.LR is
       end if;
    end Min;
 
+   function Net_Recursion (Cycle : in Recursion_Cycle; RHS : in Natural) 
return Recursion
+   is
+      Result : Recursion := None;
+   begin
+      if Cycle'Length = 1 then
+         for E of Cycle (Cycle'First).Edges loop
+            if E.Data.RHS = RHS then
+               Result := Net_Recursion (Result, E.Data.Recursive);
+            end if;
+         end loop;
+      else
+         for Item of Cycle loop
+            for E of Item.Edges loop
+               Result := Net_Recursion (Result, E.Data.Recursive);
+            end loop;
+         end loop;
+      end if;
+      return Result;
+   end Net_Recursion;
+
+   function Worst_Recursion (Cycle : in Recursion_Cycle; RHS : in Natural) 
return Recursion
+   is
+      Result : Recursion := None;
+   begin
+      if Cycle'Length = 1 then
+         for E of Cycle (Cycle'First).Edges loop
+            if E.Data.RHS = RHS then
+               Result := Worst_Recursion (Result, E.Data.Recursive);
+            end if;
+         end loop;
+      else
+         for Item of Cycle loop
+            for E of Item.Edges loop
+               Result := Worst_Recursion (Result, E.Data.Recursive);
+            end loop;
+         end loop;
+      end if;
+      return Result;
+   end Worst_Recursion;
+
+   function Worst_Recursion
+     (Recursion_IDs : in Recursion_Lists.List;
+      Recursions    : in Generate.Recursions;
+      RHS           : in Natural)
+     return Recursion
+   is
+      Result : Recursion := None;
+   begin
+      for ID of Recursion_IDs loop
+         Result := Worst_Recursion
+           (Result,
+            (if Recursions.Full
+             then Net_Recursion (Recursions.Recursions (ID), RHS)
+             else Worst_Recursion (Recursions.Recursions (ID), RHS)));
+      end loop;
+      return Result;
+   end Worst_Recursion;
+
    procedure Terminal_Sequence
-     (Grammar         : in     WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor      : in     WisiToken.Descriptor;
-      All_Sequences   : in out Minimal_Sequence_Array;
-      All_Set         : in out Token_ID_Set;
-      RHS_Set         : in out Token_ID_RHS_Set;
-      Recursing       : in out Token_ID_Set;
-      Recursing_Index : in out Token_ID_Array_Positive;
-      Nonterm         : in     Token_ID)
+     (Grammar       : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor    : in     WisiToken.Descriptor;
+      All_Sequences : in out Minimal_Sequence_Array;
+      All_Seq_Set   : in out Token_ID_Set;
+      RHS_Seq_Set   : in out LHS_RHS_Set;
+      Recursing     : in out Token_ID_Set;
+      Nonterm       : in     Token_ID)
    is
       use Ada.Containers;
       use Token_ID_Arrays;
@@ -88,35 +127,45 @@ package body WisiToken.Generate.LR is
       Prod : Productions.Instance renames Grammar (Nonterm);
 
       Skipped_Recursive : Boolean := False;
+
+      procedure Init_All_Sequences (LHS : in Token_ID)
+      is
+         Prod : Productions.Instance renames Grammar (LHS);
+      begin
+         if All_Sequences (LHS).Length = 0 then
+            All_Sequences (LHS).Set_First_Last (Prod.RHSs.First_Index, 
Prod.RHSs.Last_Index);
+         end if;
+         if RHS_Seq_Set (LHS).Length = 0 then
+            RHS_Seq_Set (LHS).Set_First_Last (Prod.RHSs.First_Index, 
Prod.RHSs.Last_Index);
+         end if;
+      end Init_All_Sequences;
+
    begin
-      --  We get here because All_Sequences (Nonterm) has not been comptued
-      --  yet. Attempt to compute All_Sequences (Nonterm); it may not
-      --  succeed due to recursion. If successful, set All_Set (Nonterm)
-      --  True.
+      --  We get here because All_Sequences (Nonterm) has not been fully
+      --  computed yet (All_Seq_Set (Nonterm) is False). Attempt to
+      --  compute All_Sequences (Nonterm); it may not succeed due to
+      --  recursion. If successful, set All_Seq_Set (Nonterm).
       --
       --  In a useful grammar, all direct and indirect recursive nonterms
       --  have a non-recursive minimal terminal sequence; finding it will
       --  break the recursion, allowing this algorithm to complete. This is
       --  checked in Compute_Minimal_Terminal_Sequences.
 
-      --  Fill All_Sequences (Nonterm) with terminals from each production
-      --  for Nonterm. We set partial results so recursion can be resolved.
-      if All_Sequences (Nonterm).Length = 0 then
-         All_Sequences (Nonterm).Set_First_Last (Prod.RHSs.First_Index, 
Prod.RHSs.Last_Index); -- defaults empty
-         RHS_Set (Nonterm).Set_First_Last (Prod.RHSs.First_Index, 
Prod.RHSs.Last_Index); -- defaults False
-      end if;
+      Init_All_Sequences (Nonterm);
 
       for RHS in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
-         if not RHS_Set (Nonterm)(RHS) then
+         if not RHS_Seq_Set (Nonterm)(RHS) then
+            if Trace_Generate > Extra then
+               Ada.Text_IO.Put_Line (Trimmed_Image ((Nonterm, RHS)) & " " & 
Image (Nonterm, Descriptor) & " compute");
+            end if;
             if Prod.RHSs (RHS).Tokens.Length = 0 then
-               RHS_Set (Nonterm)(RHS) := True;
+               RHS_Seq_Set (Nonterm)(RHS) := True;
                if Trace_Generate > Extra then
                   Ada.Text_IO.Put_Line (Trimmed_Image (Production_ID'(Nonterm, 
RHS)) & " => () empty");
                end if;
 
             else
                for I in Prod.RHSs (RHS).Tokens.First_Index .. Prod.RHSs 
(RHS).Tokens.Last_Index loop
-                  Recursing_Index (Nonterm) := I;
                   declare
                      ID : Token_ID renames Prod.RHSs (RHS).Tokens (I);
                   begin
@@ -124,40 +173,22 @@ package body WisiToken.Generate.LR is
                         All_Sequences (Nonterm) (RHS).Sequence.Append (ID);
 
                      else
-                        if not All_Set (ID) then
-                           --  Need to compute some RHSs of ID
-
+                        if (for some RHS of RHS_Seq_Set (ID) => RHS) then
+                           --  There is a minimal sequence for ID; use it
+                           null;
+                        else
                            if ID = Nonterm or Recursing (ID) then
-                              --  Nonterm is mutually recursive with itself or 
some other.
-                              All_Sequences (Nonterm)(RHS).Left_Recursive := I 
= Positive'First and
-                                (ID = Nonterm or Recursing_Index (ID) = 
Positive'First);
+                              --  Clear partial minimal sequence; we are 
starting over.
+                              All_Sequences (Nonterm)(RHS).Sequence.Clear;
+                              goto Skip;
 
-                              if (for some RHS of RHS_Set (ID) => RHS) then
-                                 --  There is a minimal sequence for ID; use it
-                                 null;
-                              else
-                                 if Trace_Generate > Extra then
-                                    Ada.Text_IO.Put_Line
-                                      (Trimmed_Image (Production_ID'(Nonterm, 
RHS)) & "." & Trimmed_Image (I) & " => " &
-                                         (if ID = Nonterm
-                                          then "direct recursive"
-                                          else "indirect recursive " & Image 
(ID, Descriptor)));
-                                 end if;
-
-                                 All_Sequences (Nonterm)(RHS).Left_Recursive 
:= False;
-                                 All_Sequences (Nonterm)(RHS).Sequence.Clear;
-                                 goto Skip;
-                              end if;
                            else
                               Recursing (ID) := True;
-                              if Trace_Generate > Extra then
-                                 Ada.Text_IO.Put_Line (Trimmed_Image (ID) & " 
" & Image (ID, Descriptor) & " compute");
-                              end if;
                               Terminal_Sequence
-                                (Grammar, Descriptor, All_Sequences, All_Set, 
RHS_Set, Recursing, Recursing_Index, ID);
+                                (Grammar, Descriptor, All_Sequences, 
All_Seq_Set, RHS_Seq_Set, Recursing, ID);
                               Recursing (ID) := False;
 
-                              if All_Set (ID) or else (for some RHS of RHS_Set 
(ID) => RHS) then
+                              if All_Seq_Set (ID) or else (for some RHS of 
RHS_Seq_Set (ID) => RHS) then
                                  --  Found a minimal sequence for ID; use it
                                  null;
                               else
@@ -167,14 +198,14 @@ package body WisiToken.Generate.LR is
                            end if;
                         end if;
                         declare
-                           Min_RHS : constant Integer := Min (All_Sequences 
(ID), RHS_Set (ID));
+                           Min_RHS : constant Integer := Min (All_Sequences 
(ID), RHS_Seq_Set (ID));
                         begin
                            All_Sequences (Nonterm)(RHS).Sequence.Append 
(All_Sequences (ID)(Min_RHS).Sequence);
                         end;
                      end if;
                   end;
                end loop;
-               RHS_Set (Nonterm)(RHS) := True;
+               RHS_Seq_Set (Nonterm)(RHS) := True;
                if Trace_Generate > Extra then
                   Ada.Text_IO.Put_Line
                     (Trimmed_Image (Production_ID'(Nonterm, RHS)) & " => " &
@@ -187,7 +218,7 @@ package body WisiToken.Generate.LR is
       end loop;
 
       if Skipped_Recursive then
-         if (for some RHS of RHS_Set (Nonterm) => not RHS) then
+         if (for some RHS of RHS_Seq_Set (Nonterm) => not RHS) then
             --  Some RHSs are have unresolved recursion; we will
             --  eventually try again when the recursion is resolved.
             if Trace_Generate > Extra then
@@ -198,7 +229,7 @@ package body WisiToken.Generate.LR is
          end if;
       end if;
 
-      All_Set (Nonterm) := True;
+      All_Seq_Set (Nonterm) := True;
 
       if Trace_Generate > Extra then
          Ada.Text_IO.Put_Line
@@ -228,31 +259,33 @@ package body WisiToken.Generate.LR is
       Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
       Has_Empty_Production : in     Token_ID_Set;
       First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      : in out Conflict_Count_Lists.List;
       Conflicts            : in out Conflict_Lists.List;
       Descriptor           : in     WisiToken.Descriptor)
    is
       Matching_Action : constant Action_Node_Ptr := Find (Symbol, Action_List);
    begin
-      if Trace_Generate > Outline then
+      if Trace_Generate > Detail then
          Ada.Text_IO.Put (Image (Symbol, Descriptor) & " => ");
          Put (Descriptor, Action);
          Ada.Text_IO.New_Line;
       end if;
 
       if Matching_Action /= null then
-         if Equal (Matching_Action.Action.Item, Action) then
-            --  Matching_Action is identical to Action, so there is no
-            --  conflict; just don't add it again.
-            if Trace_Generate > Outline then
+         if Is_In (Action, Matching_Action.Action) then
+            --  Action is already in the list.
+            if Trace_Generate > Detail then
                Ada.Text_IO.Put_Line (" - already present");
             end if;
             return;
          else
             --  There is a conflict. Report it and add it, so the
-            --  generalized parser can follow both paths
+            --  generalized parser can follow all paths
             declare
-               --  Enforce canonical Shift/Reduce or Accept/Reduce
-               --  order, to simplify searching and code generation.
+               --  Enforce canonical Shift/Reduce or Accept/Reduce order, to 
simplify
+               --  searching and code generation. There can be only one Shift 
in the
+               --  list of conflicting actions, so we keep it the first item 
in the
+               --  list; no order in the rest of the list.
                Action_A : constant Parse_Action_Rec :=
                  (if Action.Verb in Shift | Accept_It then Action else 
Matching_Action.Action.Item);
 
@@ -268,37 +301,66 @@ package body WisiToken.Generate.LR is
                     (Closure, Action_B, Symbol, Grammar, Has_Empty_Production, 
First_Nonterm_Set, Descriptor),
                   State_Index => Closure.State,
                   On          => Symbol);
+
+               Counts : Conflict_Count_Lists.Cursor;
             begin
+               for Cur in Conflict_Counts.Iterate loop
+                  if Conflict_Counts (Cur).State = Closure.State then
+                     Counts := Cur;
+                     exit;
+                  end if;
+               end loop;
+
+               if not Conflict_Count_Lists.Has_Element (Counts) then
+                  Conflict_Counts.Append ((Closure.State, others => 0));
+                  Counts := Conflict_Counts.Last;
+               end if;
+
+               declare
+                  use Conflict_Count_Lists;
+                  Counts_Ref : constant Reference_Type := Reference 
(Conflict_Counts, Counts);
+               begin
+                  case Action_A.Verb is
+                  when Shift =>
+                     case Action_B.Verb is
+                     when Shift | Accept_It | WisiToken.Parse.LR.Error =>
+                        raise SAL.Programmer_Error;
+                     when Reduce =>
+                        Counts_Ref.Shift_Reduce := Counts_Ref.Shift_Reduce + 1;
+                     end case;
+                  when Reduce =>
+                     case Action_B.Verb is
+                     when Shift | Accept_It | WisiToken.Parse.LR.Error =>
+                        raise SAL.Programmer_Error;
+                     when Reduce =>
+                        Counts_Ref.Reduce_Reduce := Counts_Ref.Reduce_Reduce + 
1;
+                     end case;
+                  when Accept_It =>
+                     case Action_B.Verb is
+                     when Shift | Accept_It | WisiToken.Parse.LR.Error =>
+                        raise SAL.Programmer_Error;
+                     when Reduce =>
+                        Counts_Ref.Accept_Reduce := Counts_Ref.Accept_Reduce + 
1;
+                     end case;
+                  when WisiToken.Parse.LR.Error =>
+                     raise SAL.Programmer_Error;
+                  end case;
+               end;
+
                if not Is_Present (New_Conflict, Conflicts) then
                   --  The same conflict may occur in a different
                   --  item set. Only add it to conflicts once.
                   Conflicts.Append (New_Conflict);
 
-                  if Trace_Generate > Outline then
+                  if Trace_Generate > Detail then
                      Ada.Text_IO.Put_Line (" - conflict added: " & Image 
(New_Conflict, Descriptor));
                   end if;
                else
-                  if Trace_Generate > Outline then
+                  if Trace_Generate > Detail then
                      Ada.Text_IO.Put_Line (" - conflict duplicate: " & Image 
(New_Conflict, Descriptor));
                   end if;
                end if;
 
-               --  More than two actions can occur; see triple_conflict.wy. We 
make
-               --  that an error, since the grammar will be better off without 
them.
-               --  But keep going; the full parse table output will be needed 
to fix
-               --  the excess conflict.
-               if Matching_Action.Action.Next /= null then
-                  if Matching_Action.Action.Item = Action or 
Matching_Action.Action.Next.Item = Action then
-                     if Trace_Generate > Outline then
-                        Ada.Text_IO.Put_Line (" - conflict duplicate");
-                     end if;
-                  else
-                     WisiToken.Generate.Put_Error
-                       ("More than two actions on " & Image (Symbol, 
Descriptor) &
-                          " in state" & State_Index'Image (Closure.State));
-                  end if;
-               end if;
-
                if Action.Verb = Shift then
                   Matching_Action.Action := new Parse_Action_Node'(Action, 
Matching_Action.Action);
                else
@@ -317,6 +379,7 @@ package body WisiToken.Generate.LR is
       Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
       Has_Empty_Production : in     Token_ID_Set;
       First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      : in out Conflict_Count_Lists.List;
       Conflicts            : in out Conflict_Lists.List;
       Descriptor           : in     WisiToken.Descriptor)
    is
@@ -324,7 +387,7 @@ package body WisiToken.Generate.LR is
 
       State : constant State_Index := Closure.State;
    begin
-      if Trace_Generate > Outline then
+      if Trace_Generate > Detail then
          Ada.Text_IO.Put_Line ("adding actions for state" & State_Index'Image 
(State));
       end if;
 
@@ -334,7 +397,7 @@ package body WisiToken.Generate.LR is
 
             Add_Lookahead_Actions
               (Item, Table.States (State).Action_List, Grammar, 
Has_Empty_Production, First_Nonterm_Set,
-               Conflicts, Closure, Descriptor);
+               Conflict_Counts, Conflicts, Closure, Descriptor);
 
          elsif Element (Item.Dot) in Descriptor.First_Terminal .. 
Descriptor.Last_Terminal then
             --  Dot is before a terminal token.
@@ -358,7 +421,7 @@ package body WisiToken.Generate.LR is
                         --  EOF is not pushed on stack in parser, because the 
action for EOF
                         --  is Accept, not Shift.
                         Table.States (State).Action_List, Closure,
-                        Grammar, Has_Empty_Production, First_Nonterm_Set, 
Conflicts, Descriptor);
+                        Grammar, Has_Empty_Production, First_Nonterm_Set, 
Conflict_Counts, Conflicts, Descriptor);
                   end;
                else
                   if Goto_State /= Unknown_State then
@@ -366,13 +429,14 @@ package body WisiToken.Generate.LR is
                        (Dot_ID,
                         (Shift, Goto_State),
                         Table.States (State).Action_List,
-                        Closure, Grammar, Has_Empty_Production, 
First_Nonterm_Set, Conflicts, Descriptor);
+                        Closure, Grammar, Has_Empty_Production, 
First_Nonterm_Set,
+                        Conflict_Counts, Conflicts, Descriptor);
                   end if;
                end if;
             end;
          else
             --  Dot is before a non-terminal token; no action.
-            if Trace_Generate > Outline then
+            if Trace_Generate > Detail then
                Ada.Text_IO.Put_Line (Image (Element (Item.Dot), Descriptor) & 
" => no action");
             end if;
          end if;
@@ -437,6 +501,7 @@ package body WisiToken.Generate.LR is
       Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
       Has_Empty_Production : in     Token_ID_Set;
       First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      : in out Conflict_Count_Lists.List;
       Conflicts            : in out Conflict_Lists.List;
       Closure              : in     LR1_Items.Item_Set;
       Descriptor           : in     WisiToken.Descriptor)
@@ -445,7 +510,7 @@ package body WisiToken.Generate.LR is
       RHS    : Productions.Right_Hand_Side renames Prod.RHSs (Item.Prod.RHS);
       Action : constant Parse_Action_Rec := (Reduce, Item.Prod, RHS.Action, 
RHS.Check, RHS.Tokens.Length);
    begin
-      if Trace_Generate > Outline then
+      if Trace_Generate > Detail then
          Ada.Text_IO.Put_Line ("processing lookaheads");
       end if;
 
@@ -457,7 +522,7 @@ package body WisiToken.Generate.LR is
             else
                Add_Action
                  (Lookahead, Action, Action_List, Closure, Grammar,
-                  Has_Empty_Production, First_Nonterm_Set, Conflicts, 
Descriptor);
+                  Has_Empty_Production, First_Nonterm_Set, Conflict_Counts, 
Conflicts, Descriptor);
             end if;
          end if;
       end loop;
@@ -635,9 +700,13 @@ package body WisiToken.Generate.LR is
         Known.On = Item.On;
    end Match;
 
+   ----------
+   --  Minimal terminal sequences.
+
    function Image (Item : in RHS_Sequence; Descriptor : in 
WisiToken.Descriptor) return String
    is begin
-      return "(" & Boolean'Image (Item.Left_Recursive) & ", " & Image 
(Item.Sequence, Descriptor) & ")";
+      return "(" & Image (Item.Recursion) & ", " & Recursion'Image 
(Item.Worst_Recursion) & ", " &
+        Image (Item.Sequence, Descriptor) & ")";
    end Image;
 
    function Min (Item : in RHS_Sequence_Arrays.Vector) return RHS_Sequence
@@ -661,43 +730,87 @@ package body WisiToken.Generate.LR is
    end Min;
 
    function Compute_Minimal_Terminal_Sequences
-     (Descriptor : in     WisiToken.Descriptor;
-      Grammar    : in     WisiToken.Productions.Prod_Arrays.Vector)
+     (Descriptor : in WisiToken.Descriptor;
+      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Recursions : in Generate.Recursions)
      return Minimal_Sequence_Array
    is
-      --  Result (ID).Length = 0 is a valid sequence (ie the nonterminal can
-      --  be empty), so we use an auxilliary array to track whether Result
-      --  (ID) has been computed.
-      --
-      --  We also need to detect mutual recursion, and incomplete grammars.
-
-      All_Set   : Token_ID_Set := (Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal  => False);
-      Recursing : Token_ID_Set := (Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal  => False);
+      --  Result (ID).Sequence.Length = 0 is a valid result (ie the
+      --  nonterminal can be empty), so we use an auxilliary array to track
+      --  whether Result (ID) has been computed.
 
-      Recursing_Index : Token_ID_Array_Positive :=
-        (Descriptor.First_Nonterminal .. Descriptor.Last_Nonterminal  => 
Positive'Last);
+      All_Seq_Set : Token_ID_Set := (Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal => False);
+      Recursing   : Token_ID_Set := (Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal => False);
 
-      RHS_Set : Token_ID_RHS_Set :=
-        (Descriptor.First_Nonterminal .. Descriptor.Last_Nonterminal => 
LR.RHS_Set.Empty_Vector);
+      RHS_Seq_Set : LHS_RHS_Set :=
+        (Descriptor.First_Nonterminal .. Descriptor.Last_Nonterminal => 
RHS_Set.Empty_Vector);
 
-      Last_Count : Integer := 0;
-      This_Count : Integer;
+      Last_Seq_Count : Integer := 0;
+      This_Count     : Integer;
+      Pass_Count     : Integer := 0;
    begin
       return Result : Minimal_Sequence_Array (Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal) do
          loop
-            exit when (for all B of All_Set => B);
-            for P of Grammar loop
-               if not All_Set (P.LHS) then
-                  Terminal_Sequence (Grammar, Descriptor, Result, All_Set, 
RHS_Set, Recursing, Recursing_Index, P.LHS);
+            exit when (for all B of All_Seq_Set => B);
+            Pass_Count := Pass_Count + 1;
+            if Trace_Generate > Detail then
+               if Trace_Generate > Extra then
+                  Ada.Text_IO.New_Line;
                end if;
+               Ada.Text_IO.Put_Line ("Compute_Minimal_Terminal_Sequences pass" 
& Integer'Image (Pass_Count));
+            end if;
+            for P of Grammar loop
+               Terminal_Sequence (Grammar, Descriptor, Result, All_Seq_Set, 
RHS_Seq_Set, Recursing, P.LHS);
             end loop;
-            This_Count := Count (All_Set);
-            if This_Count = Last_Count then
-               Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, Image 
(All_Set, Descriptor, Inverted => True));
-               raise Grammar_Error with "recursion not resolved";
+            This_Count := Count (All_Seq_Set);
+            if This_Count = Last_Seq_Count then
+               Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, Image 
(All_Seq_Set, Descriptor, Inverted => True));
+               raise Grammar_Error with "sequences not resolved";
             end if;
-            Last_Count := This_Count;
+            Last_Seq_Count := This_Count;
          end loop;
+
+         --  Set Result.Recursions
+         for Recursion_ID in Recursions.Recursions.First_Index .. 
Recursions.Recursions.Last_Index loop
+            declare
+               Cycle : Recursion_Cycle renames Recursions.Recursions 
(Recursion_ID);
+            begin
+               for I in Cycle'Range loop
+                  declare
+                     Edges : constant Grammar_Graphs.Edge_Lists.List :=
+                       (if Recursions.Full then
+                          (if I = Cycle'Last
+                           then Cycle (Cycle'First).Edges
+                           else Cycle (I + 1).Edges)
+                        else Cycle (I).Edges);
+                  begin
+                     for E of Edges loop
+                        Result (Cycle (I).Vertex)(E.Data.RHS).Recursion.Append 
(Recursion_ID);
+                     end loop;
+                  end;
+               end loop;
+            end;
+         end loop;
+
+         --  Set Result.Worst_Recursions
+         for Nonterm in Result'Range loop
+            for RHS in Result (Nonterm).First_Index .. Result 
(Nonterm).Last_Index loop
+               declare
+                  RHS_Seq : RHS_Sequence renames Result (Nonterm)(RHS);
+               begin
+                  RHS_Seq.Worst_Recursion := Worst_Recursion 
(RHS_Seq.Recursion, Recursions, RHS);
+               end;
+            end loop;
+         end loop;
+
+         if Trace_Generate > Detail then
+            Ada.Text_IO.Put_Line ("Minimal_Terminal_Sequences:");
+            for LHS in Result'Range loop
+               Ada.Text_IO.Put_Line
+                 (Trimmed_Image (LHS) & " " & Image (LHS, Descriptor) & " ==> 
" &
+                    Image (Result (LHS), Descriptor));
+            end loop;
+         end if;
       end return;
    end Compute_Minimal_Terminal_Sequences;
 
@@ -739,7 +852,7 @@ package body WisiToken.Generate.LR is
       subtype Terminals is Token_ID range Descriptor.First_Terminal .. 
Descriptor.Last_Terminal;
 
       Working_Set : LR1_Items.Item_Lists.List := Kernel.Set;
-      Del         : LR1_Items.Item_Lists.Cursor;
+      Recursive   : Boolean := False;
 
       function Find_Action (List : in Action_Node_Ptr; ID : in Token_ID) 
return Minimal_Action
       is
@@ -764,100 +877,237 @@ package body WisiToken.Generate.LR is
          raise SAL.Programmer_Error;
       end Find_Action;
 
+      function Min_Length (Item : in RHS_Sequence_Arrays.Vector) return 
Ada.Containers.Count_Type
+      is
+         use Ada.Containers;
+         Min : Count_Type := Count_Type'Last;
+      begin
+         for RHS of Item loop
+            if RHS.Sequence.Length < Min then
+               Min := RHS.Sequence.Length;
+            end if;
+         end loop;
+         return Min;
+      end Min_Length;
+
+      function After_Dot_Length (Item : in LR1_Items.Item) return 
Ada.Containers.Count_Type
+      is
+         use Ada.Containers;
+         Prod   : constant Production_ID := Item.Prod;
+         I      : Token_ID_Arrays.Cursor := Item.Dot;
+         Result : Count_Type             := 0;
+         Tokens : Vector renames Grammar (Prod.LHS).RHSs (Prod.RHS).Tokens;
+      begin
+         loop
+            exit when I = Token_ID_Arrays.No_Element;
+
+            if Tokens (I) in Terminals then
+               Result := Result + 1;
+            else
+               Result := Result + Min_Length (Minimal_Terminal_Sequences 
(Tokens (I)));
+            end if;
+            Next (I);
+         end loop;
+         return Result;
+      end After_Dot_Length;
+
       procedure Delete_Non_Minimal
       is
-         Min_Length : Ada.Containers.Count_Type := 
Ada.Containers.Count_Type'Last;
-         I          : LR1_Items.Item_Lists.Cursor;
-         Min_I      : LR1_Items.Item_Lists.Cursor;
+         use Ada.Containers;
+
+         Min_Length       : Count_Type := Count_Type'Last;
+         I                : LR1_Items.Item_Lists.Cursor;
+         Recursive_Count  : Count_Type := 0;
+         Delete_Recursive : Boolean;
+
+         function Immediate_Recursive return Boolean
+         is
+            --  Direct left recursion is never minimal; for example, consider
+            --  ada_lite LALR state 149:
+            --
+            --  61.0:association_list <= association_list ^ COMMA 
association_opt
+            --
+            --  If we already have an association_list, adding a COMMA to it
+            --  cannot be minimal.
+            --
+            --  Similarly, indirect left recursion is not minimal; consider
+            --  ada_lite LALR states 29 and 60:
+            --
+            --  State 29:
+            --  103.3:name <= selected_component ^,
+            --
+            --  State 60:
+            --   94.0:function_specification <= FUNCTION name ^ 
parameter_and_result_profile
+            --  103.0:name <= name ^ LEFT_PAREN range_list
+            --  103.1:name <= name ^ actual_parameter_part
+            --  123.0:selected_component <= name ^ DOT IDENTIFIER
+            --
+            --  If we already have a name, adding actual_parameter_part or DOT 
IDENTIFIER cannot be
+            --  minimal.
+
+            --  There is a trade off here between error recovery power and 
risk of
+            --  recursive loops. Consider ada_lite state 152:
+            --
+            --  103.0:name <= name LEFT_PAREN range_list ^ RIGHT_PAREN
+            --  117.0:range_list <= range_list ^ COMMA range_g
+            --
+            --  Both productions are Left_Recursive, but in the first item, 
dot is past
+            --  the recursion, and can be usefully completed.
+            --
+            --  However, that might allow loops; see java_enum_ch19.wy.
+            --
+            --  A similar argument applies to right recursive items; from
+            --  java_expressions_ch19.wy:
+            --
+            --  State 7:
+            --  27.0:Assignment <= LeftHandSide ^ EQUAL Expression
+            --
+            --  State 22:
+            --  28.0:LeftHandSide <= Identifier ^
+            --  34.0:ClassType <= Identifier ^
+            --
+            --  State 25:
+            --  24.1:Expression <= AssignmentExpression ^
+            --
+            --  State 26:
+            --  26.1:AssignmentExpression <= Assignment ^
+            --
+            --  Choosing LeftHandSide for the minimal action in state 22 will 
lead
+            --  to a loop thru state 7. However, Assignment can also occur in
+            --  Statement, where it is not recursive:
+            --
+            --  State 1:
+            --  23.0:Statement <= LEFT_CURLY_BRACKET ^ Assignment 
RIGHT_CURLY_BRACKET
+            --
+            --  This is not easy to check for.
+            --
+            --  It is not expensive to check for loops in 
Minimal_Complete_Action
+            --  at run-time, so given all the above we allow items that are 
"past
+            --  the recursion" here.
+
+            Item : LR1_Items.Item renames Constant_Ref (I).Element.all;
+            Prod : constant WisiToken.Production_ID := Item.Prod;
+            Min_Seq : RHS_Sequence renames Minimal_Terminal_Sequences 
(Prod.LHS)(Prod.RHS);
+         begin
+            return Min_Seq.Worst_Recursion in Right | Left and then
+              (Has_Element (Item.Dot) and then
+                 Item.Dot = To_Cursor (Grammar (Prod.LHS).RHSs 
(Prod.RHS).Tokens, 2));
+         end Immediate_Recursive;
+
       begin
-         --  The absolute minimal production for an LHS may not be in this 
state.
-         --  For example, for an Ada aggregate, the minimal terminal sequence 
is:
+         --  The absolute minimal production for an LHS may not be in this
+         --  state. For example, for an Ada aggregate, the absolute minimal
+         --  terminal sequence is:
+         --
          --  aggregate <= LEFT_PAREN RIGHT_PAREN
-         --  but one state has:
+         --
+         --  but one state has only:
+         --
          --  aggregate <= LEFT_PAREN expression_opt WITH ^ NULL RECORD 
RIGHT_PAREN
          --  aggregate <= LEFT_PAREN expression_opt WITH ^ association_list 
RIGHT_PAREN
          --
-         --  Find the minimum of the productions that are present
+         --  Find the minimum tokens after dot of the productions that are 
present
 
+         --  First see if all are recursive
          I := Working_Set.First;
          loop
             exit when not Has_Element (I);
-            declare
-               Prod : constant WisiToken.Production_ID := Constant_Ref 
(I).Prod;
-            begin
-               --  IMPROVEME: If Dot is near the end of a production, this is 
not the
-               --  best metric; the best metric is count of remaining tokens to
-               --  insert. But changing to that won't make much difference; it 
only
-               --  matters when Dot is not near the beginning, but then we 
don't have
-               --  multiple productions (they all must have same prefix).
-               --
-               --  We must eliminate direct left recursion; otherwise it can 
appear
-               --  to have a minimum length. For example, consider ada_lite 
state
-               --  149:
-               --
-               --  57.0:actual_parameter_part <= LEFT_PAREN association_list ^ 
RIGHT_PAREN
-               --  61.0:association_list <= association_list ^ COMMA 
association_opt
-               --
-               --  Both have length 2, but the association_list requires a
-               --  RIGHT_PAREN eventually.
-               --
-               --  We also have to eliminate indirect left recursion; consider 
ada_lite state 60:
-               --
-               --   94.0:function_specification <= FUNCTION name ^ 
parameter_and_result_profile
-               --  103.0:name <= name ^ LEFT_PAREN range_list
-               --  103.1:name <= name ^ actual_parameter_part
-               --  123.0:selected_component <= name ^ DOT IDENTIFIER
-               --
-               --  'selected' component has length 3, which two others also 
have, but
-               --  it requires more eventually.
-               --
-               --  An item production is left recursive only if Dot is after 
the
-               --  first token (ie, before the second token); consider 
conflict_name
-               --  state 5:
-               --
-               --   8.0:attribute_reference <= name TICK ^ 
attribute_designator, RIGHT_PAREN/TICK/Wisi_EOI
-               --  11.0:qualified_expression <= name TICK ^ aggregate, 
RIGHT_PAREN/TICK/Wisi_EOI
-               --
-               --  Both are indirect left recursive with "name", but both are 
past
-               --  the recursion, and can be completed.
-
-               if Min_Length > Minimal_Terminal_Sequences 
(Prod.LHS)(Prod.RHS).Sequence.Length and
-                 not (Minimal_Terminal_Sequences 
(Prod.LHS)(Prod.RHS).Left_Recursive and then
-                        (Has_Element (Constant_Ref (I).Dot) and then
-                           Constant_Ref (I).Dot = To_Cursor (Grammar 
(Prod.LHS).RHSs (Prod.RHS).Tokens, 2)))
-               then
-                  Min_Length := Minimal_Terminal_Sequences 
(Prod.LHS)(Prod.RHS).Sequence.Length;
-                  Min_I      := I;
-               end if;
-            end;
+
+            if Immediate_Recursive then
+               Recursive_Count := Recursive_Count + 1;
+            end if;
+
             Next (I);
          end loop;
 
-         if not Has_Element (Min_I) then
-            Working_Set.Clear;
-         else
-            I := Working_Set.First;
-            loop
-               exit when not Has_Element (I);
+         Delete_Recursive := Recursive_Count < Working_Set.Length;
+
+         I := Working_Set.First;
+         loop
+            exit when not Has_Element (I);
+
+            if Delete_Recursive and Immediate_Recursive then
+               if Trace_Generate > Extra then
+                  Ada.Text_IO.Put_Line ("delete " & Image (Constant_Ref 
(I).Prod) & " recursive");
+               end if;
                declare
-                  Prod : constant WisiToken.Production_ID := Constant_Ref 
(I).Prod;
+                  Del : LR1_Items.Item_Lists.Cursor := I;
                begin
-                  if I /= Min_I then
-                     if Trace_Generate > Extra then
-                        Ada.Text_IO.Put_Line ("delete " & Image (Prod) & " not 
minimal");
-                     end if;
-                     Del := I;
-                     Next (I);
-                     Working_Set.Delete (Del);
-                  else
-                     Next (I);
+                  Next (I);
+                  Working_Set.Delete (Del);
+               end;
+
+            else
+               Recursive := Recursive or Minimal_Terminal_Sequences
+                 (Constant_Ref (I).Prod.LHS)(Constant_Ref 
(I).Prod.RHS).Worst_Recursion in
+                 Left | Right;
+
+               declare
+                  Prod_Length : constant Count_Type := After_Dot_Length 
(Constant_Ref (I));
+               begin
+                  if Min_Length > Prod_Length then
+                     Min_Length := Prod_Length;
                   end if;
                end;
-            end loop;
-         end if;
+
+               Next (I);
+            end if;
+         end loop;
+
+         --  Now we have the minimum length; check remaining items against that
+         I := Working_Set.First;
+         loop
+            exit when not Has_Element (I);
+            if Min_Length < After_Dot_Length (Constant_Ref (I)) then
+               declare
+                  Del : LR1_Items.Item_Lists.Cursor := I;
+               begin
+                  if Trace_Generate > Extra then
+                     Ada.Text_IO.Put_Line ("delete " & Image (Constant_Ref 
(I).Prod));
+                  end if;
+                  Next (I);
+                  Working_Set.Delete (Del);
+               end;
+            else
+               if Trace_Generate > Extra then
+                  Ada.Text_IO.Put_Line ("keep " & Image (Constant_Ref 
(I).Prod));
+               end if;
+               Next (I);
+            end if;
+         end loop;
       end Delete_Non_Minimal;
 
    begin
+      if Kernel.State > 0 then
+         declare
+            use Ada.Containers;
+            I : Count_Type := 1;
+
+            function Before_Dot (Item : in LR1_Items.Item) return Token_ID
+            is
+               Tokens : Token_ID_Arrays.Vector renames Grammar 
(Item.Prod.LHS).RHSs (Item.Prod.RHS).Tokens;
+            begin
+               if Item.Dot = Token_ID_Arrays.No_Element then
+                  return Tokens (Tokens.Last_Index);
+               else
+                  return Tokens (Prev (Item.Dot));
+               end if;
+            end Before_Dot;
+         begin
+            State.Kernel.Set_First_Last (1, Kernel.Set.Length);
+            for Item of Kernel.Set loop
+               State.Kernel (I) :=
+                 (LHS              => Item.Prod.LHS,
+                  Before_Dot       => Before_Dot (Item),
+                  Length_After_Dot => After_Dot_Length (Item),
+                  Recursive        => Minimal_Terminal_Sequences
+                    (Item.Prod.LHS)(Item.Prod.RHS).Worst_Recursion in Right | 
Left);
+
+               I := I + 1;
+            end loop;
+         end;
+      end if;
+
       --  The actions computed here are used in the error recovery
       --  algorithm, to decide what terminals to insert in the input stream
       --  in order to correct an error. The strategy is to complete a high
@@ -867,8 +1117,7 @@ package body WisiToken.Generate.LR is
       --  production.
       --
       --  The actions are empty in a state that includes the accept
-      --  production, or where the only not left recursive productions can
-      --  be empty. That tells the error recovery algorithm to stop using
+      --  production. That tells the error recovery algorithm to stop using
       --  the minimal complete actions strategy.
 
       if (for some Item of Working_Set =>
@@ -879,53 +1128,86 @@ package body WisiToken.Generate.LR is
          return;
       end if;
 
-      if Working_Set.Length > 1 then
-         --  There are multiple productions in this state, all equally valid;
-         --  the choice is determined by what input error recovery inserts.
-         Delete_Non_Minimal;
-      end if;
+      Delete_Non_Minimal;
 
-      if Trace_Generate > Extra then
-         Ada.Text_IO.Put_Line ("after deletions:");
-         LR1_Items.Put (Grammar, Descriptor, Working_Set, Show_Lookaheads => 
False);
-      end if;
+      State.Minimal_Complete_Actions_Recursive := Recursive;
 
-      --  Find the actions for the remaining productions.
-
-      for Item of Working_Set loop
-         if not Has_Element (Item.Dot) then
-            --  Item has no next terminal. Include a reduce action; the
-            --  Minimal_Terminal_First for the resulting state will be used.
-            if Trace_Generate > Extra then
-               Ada.Text_IO.Put_Line ("reduce " & Image (Item.Prod));
-            end if;
-            State.Minimal_Complete_Action :=
-              (Reduce, Item.Prod.LHS,
-                Token_Count => Grammar (Item.Prod.LHS).RHSs 
(Item.Prod.RHS).Tokens.Length);
-         else
-            declare
-               ID : constant Token_ID := Element (Item.Dot);
-            begin
-               if ID /= Descriptor.EOI_ID then
+      if Working_Set.Length > 0 then
+         --  There are one or more productions with equal after-dot length in
+         --  this state, all equally valid; the choice is determined by what
+         --  input error recovery inserts.
+         --
+         --  We could simply choose one arbitrarily, but that can lead to loops
+         --  (see discussion above in Immediate_Recursive). So we consider the
+         --  higher level production. However, in general we cannot precompute
+         --  what higher-level productions might be completed from each state;
+         --  we must use the parse stack during error recovery. In that case,
+         --  we store multiple minimal actions in the state (see
+         --  Insert_Minimal_Complete_Actions in
+         --  wisitoken-parse-lr-mckenzie_recover-explore.adb).
 
-                  if ID in Terminals then
-                     State.Minimal_Complete_Action := Find_Action 
(State.Action_List, ID);
+         declare
+            Actions : Minimal_Action_Array (1 .. Working_Set.Length) := 
(others => (others => <>));
 
-                  else
-                     if Minimal_Terminal_First (ID) = Invalid_Token_ID then
-                        --  Item.Dot is a nullable nonterm, include a reduce 
of the null
-                        --  nonterm, rather than a shift of the following 
terminal; recover
-                        --  must do the reduce first.
-                        State.Minimal_Complete_Action := (Reduce, ID, 
Token_Count => 0);
+            I    : Ada.Containers.Count_Type := 1;
+            Skip : Boolean;
+         begin
+            for Item of Working_Set loop
+
+               if not Has_Element (Item.Dot) then
+                  --  Item has no next terminal. Include a reduce action; the
+                  --  Minimal_Terminal_First for the resulting state will be 
used.
+                  Actions (I) :=
+                    (Reduce, Item.Prod.LHS,
+                     Token_Count => Grammar (Item.Prod.LHS).RHSs 
(Item.Prod.RHS).Tokens.Length);
+               else
+                  declare
+                     ID : constant Token_ID := Element (Item.Dot);
+                  begin
+                     if ID in Terminals then
+                        Actions (I) := Find_Action (State.Action_List, ID);
 
                      else
-                        State.Minimal_Complete_Action := Find_Action 
(State.Action_List, Minimal_Terminal_First (ID));
+                        if Minimal_Terminal_First (ID) = Invalid_Token_ID then
+                           --  Item.Dot is a nullable nonterm; include a 
reduce to the null
+                           --  nonterm, rather than a shift of the following 
terminal; recover
+                           --  must do the reduce first.
+                           Actions (I) := (Reduce, ID, Token_Count => 0);
+
+                        else
+                           Actions (I) := Find_Action (State.Action_List, 
Minimal_Terminal_First (ID));
+                        end if;
                      end if;
-                  end if;
+                  end;
                end if;
-            end;
-         end if;
-      end loop;
+               I := I + 1;
+            end loop;
+
+            if Actions'Length = 1 then
+               State.Minimal_Complete_Actions := 
Minimal_Action_Arrays.To_Vector (Actions (Actions'First));
+            else
+               --  Check for duplicates; see 
three_action_conflict_lalr.parse_table
+               --  state 3 or lalr_generator_bug_01_lalr.parse_table state 28
+               for I in Actions'Range loop
+                  Skip := False;
+                  for J in Actions'First .. I - 1 loop
+                     if Actions (I) = Actions (J) then
+                        Skip := True;
+                        exit;
+                     end if;
+                  end loop;
+                  if not Skip then
+                     State.Minimal_Complete_Actions.Append (Actions (I));
+                  end if;
+               end loop;
+            end if;
+
+            if Trace_Generate > Extra then
+               Ada.Text_IO.Put_Line
+                 (Image (State.Minimal_Complete_Actions, Descriptor) & (if 
Recursive then " recursive" else ""));
+            end if;
+         end;
+      end if;
    end Set_Minimal_Complete_Actions;
 
    ----------
@@ -937,14 +1219,14 @@ package body WisiToken.Generate.LR is
       Action_Names : in Names_Array_Array;
       Check_Names  : in Names_Array_Array)
    is
+      use Ada.Containers;
       use Ada.Text_IO;
       File : File_Type;
    begin
       --  Only space, semicolon, newline delimit object values. Bounds of
       --  arrays output before each array, unless known from discriminants.
       --  End of lists indicated by semicolon. Action, Check subprograms are
-      --  represented by True if present, False if not; look up the actual
-      --  address Table.Productions.
+      --  represented by True if present, False if not.
 
       Create (File, Out_File, File_Name);
 
@@ -956,13 +1238,6 @@ package body WisiToken.Generate.LR is
       New_Line (File);
 
       for State of Table.States loop
-         Put (File, Integer'Image (State.Productions.First_Index));
-         Put (File, Integer'Image (State.Productions.Last_Index));
-         for Prod of State.Productions loop
-            Put (File, Token_ID'Image (Prod.LHS) & Integer'Image (Prod.RHS));
-         end loop;
-         New_Line (File);
-
          declare
             Node_I : Action_Node_Ptr := State.Action_List;
          begin
@@ -1033,22 +1308,38 @@ package body WisiToken.Generate.LR is
             New_Line (File);
          end;
 
-         declare
-            Action  : Minimal_Action renames State.Minimal_Complete_Action;
-         begin
-            case State.Minimal_Complete_Action.Verb is
-            when Pause =>
-               null;
-            when Shift =>
-               Put (File, Minimal_Verbs'Image (Action.Verb));
-               Put (File, Token_ID'Image (Action.ID) & State_Index'Image 
(Action.State));
-            when Reduce =>
-               Put (File, Minimal_Verbs'Image (Action.Verb));
-               Put (File, Token_ID'Image (Action.Nonterm) & 
Ada.Containers.Count_Type'Image (Action.Token_Count));
-            end case;
-         end;
-         Put (File, ';');
-         New_Line (File);
+         if State.Kernel.Length = 0 then
+            --  Not set for state 0
+            Put_Line (File, "0 -1");
+
+         else
+            Put (File, Count_Type'Image (State.Kernel.First_Index));
+            Put (File, Count_Type'Image (State.Kernel.Last_Index));
+            for Item of State.Kernel loop
+               Put (File, Token_ID'Image (Item.LHS) & Token_ID'Image 
(Item.Before_Dot) &
+                      Count_Type'Image (Item.Length_After_Dot));
+            end loop;
+            New_Line (File);
+         end if;
+
+         if State.Minimal_Complete_Actions.Length = 0 then
+            null;
+         else
+            Put (File, Count_Type'Image 
(State.Minimal_Complete_Actions.First_Index));
+            Put (File, Count_Type'Image 
(State.Minimal_Complete_Actions.Last_Index));
+            for Action of State.Minimal_Complete_Actions loop
+               Put (File, " ");
+               case Action.Verb is
+               when Shift =>
+                  Put (File, Minimal_Verbs'Image (Action.Verb));
+                  Put (File, Token_ID'Image (Action.ID) & State_Index'Image 
(Action.State));
+               when Reduce =>
+                  Put (File, Minimal_Verbs'Image (Action.Verb));
+                  Put (File, Token_ID'Image (Action.Nonterm) & 
Ada.Containers.Count_Type'Image (Action.Token_Count));
+               end case;
+            end loop;
+         end if;
+         Put_Line (File, ";");
       end loop;
       Close (File);
    end Put_Text_Rep;
@@ -1096,17 +1387,28 @@ package body WisiToken.Generate.LR is
          end if;
       end loop;
       Put_Line ("(Push_Back =>");
-      for I in Item.Delete'Range loop
-         Put (" " & Padded_Image (I, Descriptor) & " =>" & Natural'Image 
(Item.Delete (I)));
-         if I = Item.Delete'Last then
+      for I in Item.Push_Back'Range loop
+         Put (" " & Padded_Image (I, Descriptor) & " =>" & Natural'Image 
(Item.Push_Back (I)));
+         if I = Item.Push_Back'Last then
             Put_Line (")");
          else
             Put_Line (",");
          end if;
       end loop;
+      Put_Line ("(Undo_Reduce =>");
+      for I in Item.Undo_Reduce'Range loop
+         Put (" " & Padded_Image (I, Descriptor) & " =>" & Natural'Image 
(Item.Undo_Reduce (I)));
+         if I = Item.Undo_Reduce'Last then
+            Put_Line (")");
+         else
+            Put_Line (",");
+         end if;
+      end loop;
+      Put_Line ("Minimal_Complete_Cost_Delta => " & Integer'Image 
(Item.Minimal_Complete_Cost_Delta));
+      Put_Line ("Fast_Forward      => " & Integer'Image (Item.Fast_Forward));
+      Put_Line ("Matching_Begin    => " & Integer'Image (Item.Matching_Begin));
       Put_Line ("Ignore_Check_Fail =>" & Integer'Image 
(Item.Ignore_Check_Fail));
       Put_Line ("Task_Count        =>" & 
System.Multiprocessors.CPU_Range'Image (Item.Task_Count));
-      Put_Line ("Cost_Limit        =>" & Integer'Image (Item.Cost_Limit));
       Put_Line ("Check_Limit       =>" & Token_Index'Image (Item.Check_Limit));
       Put_Line ("Check_Delta_Limit =>" & Integer'Image 
(Item.Check_Delta_Limit));
       Put_Line ("Enqueue_Limit     =>" & Integer'Image (Item.Enqueue_Limit));
@@ -1150,6 +1452,7 @@ package body WisiToken.Generate.LR is
 
    procedure Put (Descriptor : in WisiToken.Descriptor; State : in Parse_State)
    is
+      use all type Ada.Containers.Count_Type;
       use Ada.Text_IO;
       use Ada.Strings.Fixed;
       Action_Ptr : Action_Node_Ptr := State.Action_List;
@@ -1183,25 +1486,54 @@ package body WisiToken.Generate.LR is
       end loop;
 
       New_Line;
-      Put ("   Minimal_Complete_Action => (");
-      case State.Minimal_Complete_Action.Verb is
-      when Pause =>
+      Put ("   Minimal_Complete_Action => "); --  No trailing 's' for 
compatibility with previous good parse tables.
+      case State.Minimal_Complete_Actions.Length is
+      when 0 =>
          null;
-      when Shift =>
-         Put (Image (State.Minimal_Complete_Action.ID, Descriptor));
-      when Reduce =>
-         Put (Image (State.Minimal_Complete_Action.Nonterm, Descriptor));
+      when 1 =>
+         --  No () here for compatibity with previous known good parse tables.
+         declare
+            Action : Minimal_Action renames State.Minimal_Complete_Actions 
(State.Minimal_Complete_Actions.First_Index);
+         begin
+            case Action.Verb is
+            when Shift =>
+               Put (Image (Action.ID, Descriptor));
+            when Reduce =>
+               Put (Image (Action.Nonterm, Descriptor));
+            end case;
+         end;
+      when others =>
+         Put ("(");
+         for I in State.Minimal_Complete_Actions.First_Index .. 
State.Minimal_Complete_Actions.Last_Index loop
+            case State.Minimal_Complete_Actions (I).Verb is
+            when Shift =>
+               Put (Image (State.Minimal_Complete_Actions (I).ID, Descriptor));
+            when Reduce =>
+               Put (Image (State.Minimal_Complete_Actions (I).Nonterm, 
Descriptor));
+            end case;
+            if I < State.Minimal_Complete_Actions.Last_Index then
+               Put (", ");
+            end if;
+         end loop;
+         Put (")");
       end case;
-      Put_Line (")");
+      if State.Minimal_Complete_Actions_Recursive then
+         Put_Line (" recursive");
+      else
+         New_Line;
+      end if;
    end Put;
 
    procedure Put_Parse_Table
-     (Table      : in Parse_Table_Ptr;
-      Title      : in String;
-      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
-      Kernels    : in LR1_Items.Item_Set_List;
-      Conflicts  : in Conflict_Lists.List;
-      Descriptor : in WisiToken.Descriptor)
+     (Table                      : in Parse_Table_Ptr;
+      Title                      : in String;
+      Grammar                    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Recursions                 : in Generate.Recursions;
+      Minimal_Terminal_Sequences : in Minimal_Sequence_Array;
+      Kernels                    : in LR1_Items.Item_Set_List;
+      Conflicts                  : in Conflict_Count_Lists.List;
+      Descriptor                 : in WisiToken.Descriptor;
+      Include_Extra              : in Boolean := False)
    is
       use all type Ada.Containers.Count_Type;
       use Ada.Text_IO;
@@ -1211,10 +1543,32 @@ package body WisiToken.Generate.LR is
 
       New_Line;
       Put_Line ("Productions:");
-      WisiToken.Productions.Put (Grammar, Descriptor);
+      for LHS in Grammar.First_Index .. Grammar.Last_Index loop
+         declare
+            Prod : WisiToken.Productions.Instance renames Grammar (LHS);
+         begin
+            for RHS in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
+               Put (WisiToken.Productions.Image (Prod.LHS, RHS, Prod.RHSs 
(RHS).Tokens, Descriptor));
+               if not Include_Extra or Minimal_Terminal_Sequences 
(LHS)(RHS).Recursion.Length = 0 then
+                  New_Line;
+               else
+                  Put_Line
+                    (" ; " & Image (Minimal_Terminal_Sequences 
(LHS)(RHS).Recursion) & " " &
+                       Recursion'Image (Minimal_Terminal_Sequences 
(LHS)(RHS).Worst_Recursion));
+               end if;
+            end loop;
+         end;
+      end loop;
+
+      if Include_Extra then
+         New_Line;
+         Put_Line ((if Recursions.Full then "Recursions:" else "Partial 
recursions:"));
+         for I in Recursions.Recursions.First_Index .. 
Recursions.Recursions.Last_Index loop
+            Put_Line (Trimmed_Image (I) & " => " & Grammar_Graphs.Image 
(Recursions.Recursions (I)));
+         end loop;
+      end if;
 
-      if Table.McKenzie_Param.Cost_Limit /= Default_McKenzie_Param.Cost_Limit 
or
-          Table.McKenzie_Param.Check_Limit /= 
Default_McKenzie_Param.Check_Limit or
+      if Table.McKenzie_Param.Check_Limit /= 
Default_McKenzie_Param.Check_Limit or
           Table.McKenzie_Param.Check_Delta_Limit /= 
Default_McKenzie_Param.Check_Delta_Limit or
           Table.McKenzie_Param.Enqueue_Limit /= 
Default_McKenzie_Param.Enqueue_Limit
       then
@@ -1227,7 +1581,8 @@ package body WisiToken.Generate.LR is
       Put_Line (Title & " Parse Table:");
 
       for State_Index in Table.States'Range loop
-         LR1_Items.Put (Grammar, Descriptor, Kernels (State_Index), 
Kernel_Only => True, Show_Lookaheads => True);
+         LR1_Items.Put
+           (Grammar, Descriptor, Kernels (State_Index), Kernel_Only => True, 
Show_Lookaheads => Include_Extra);
          New_Line;
          Put (Descriptor, Table.States (State_Index));
 
@@ -1239,17 +1594,30 @@ package body WisiToken.Generate.LR is
       if Conflicts.Length > 0 then
          declare
             use Ada.Strings.Unbounded;
-            Last_State : Unknown_State_Index := Unknown_State;
-            Line : Unbounded_String := +"States with conflicts:";
+            Line          : Unbounded_String := +"States with conflicts:";
+            Accept_Reduce : Integer          := 0;
+            Shift_Reduce  : Integer          := 0;
+            Reduce_Reduce : Integer          := 0;
          begin
-            for Conflict of Conflicts loop
-               if Conflict.State_Index /= Last_State then
-                  Append (Line, State_Index'Image (Conflict.State_Index));
-                  Last_State := Conflict.State_Index;
-               end if;
+            for Count of Conflicts loop
+               Line          := Line & State_Index'Image (Count.State);
+               Accept_Reduce := Accept_Reduce + Count.Accept_Reduce;
+               Shift_Reduce  := Shift_Reduce + Count.Shift_Reduce;
+               Reduce_Reduce := Reduce_Reduce + Count.Reduce_Reduce;
             end loop;
+
+            New_Line;
             Indent_Wrap (-Line);
+
+            New_Line;
+            Put_Line
+              (Integer'Image (Accept_Reduce) & " accept/reduce conflicts," &
+                 Integer'Image (Shift_Reduce) & " shift/reduce conflicts," &
+                 Integer'Image (Reduce_Reduce) & " reduce/reduce conflicts");
          end;
+      else
+         New_Line;
+         Put_Line (" 0 accept/reduce conflicts, 0 shift/reduce conflicts, 0 
reduce/reduce conflicts");
       end if;
    end Put_Parse_Table;
 
diff --git a/wisitoken-generate-lr.ads b/wisitoken-generate-lr.ads
index 9cb22f2..297689b 100644
--- a/wisitoken-generate-lr.ads
+++ b/wisitoken-generate-lr.ads
@@ -46,6 +46,15 @@ package WisiToken.Generate.LR is
 
    package Conflict_Lists is new Ada.Containers.Doubly_Linked_Lists (Conflict);
 
+   type Conflict_Count is record
+      State : State_Index;
+      Accept_Reduce : Integer             := 0;
+      Shift_Reduce  : Integer             := 0;
+      Reduce_Reduce : Integer             := 0;
+   end record;
+
+   package Conflict_Count_Lists is new Ada.Containers.Doubly_Linked_Lists 
(Conflict_Count);
+
    procedure Put
      (Item       : in Conflict_Lists.List;
       File       : in Ada.Text_IO.File_Type;
@@ -59,6 +68,7 @@ package WisiToken.Generate.LR is
       Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
       Has_Empty_Production : in     Token_ID_Set;
       First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      : in out Conflict_Count_Lists.List;
       Conflicts            : in out Conflict_Lists.List;
       Descriptor           : in     WisiToken.Descriptor);
    --  Add (Symbol, Action) to Action_List; check for conflicts
@@ -71,6 +81,7 @@ package WisiToken.Generate.LR is
       Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
       Has_Empty_Production : in     Token_ID_Set;
       First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      : in out Conflict_Count_Lists.List;
       Conflicts            : in out Conflict_Lists.List;
       Descriptor           : in     WisiToken.Descriptor);
    --  Add actions for Closure to Table. Has_Empty_Production, First,
@@ -82,6 +93,7 @@ package WisiToken.Generate.LR is
       Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
       Has_Empty_Production : in     Token_ID_Set;
       First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflict_Counts      : in out Conflict_Count_Lists.List;
       Conflicts            : in out Conflict_Lists.List;
       Closure              : in     LR1_Items.Item_Set;
       Descriptor           : in     WisiToken.Descriptor);
@@ -95,11 +107,6 @@ package WisiToken.Generate.LR is
    --  Delete Known_Conflicts from Conflicts.
 
    function Find
-     (Symbol      : in Token_ID;
-      Action_List : in Action_Node_Ptr)
-     return Action_Node_Ptr;
-
-   function Find
      (Closure              : in LR1_Items.Item_Set;
       Action               : in Parse_Action_Rec;
       Lookahead            : in Token_ID;
@@ -117,11 +124,15 @@ package WisiToken.Generate.LR is
 
    function Match (Known : in Conflict; Item : in 
Conflict_Lists.Constant_Reference_Type) return Boolean;
 
+   ----------
+   --  Minimal terminal sequences.
+
    type RHS_Sequence is
    record
-      Left_Recursive : Boolean := False;
-      --  Direct or indirect; see comment in
-      --  Set_Minimal_Complete_Actions.Delete_Non_Minimal.
+      Recursion : Recursion_Lists.List;
+      --  All recursion cycles involving this RHS.
+
+      Worst_Recursion : WisiToken.Recursion := None; --  worst case of all 
Recursion.
 
       Sequence : Token_ID_Arrays.Vector;
    end record;
@@ -132,7 +143,7 @@ package WisiToken.Generate.LR is
    function Image (Item : in RHS_Sequence; Descriptor : in 
WisiToken.Descriptor) return String;
    --  Positional Ada aggregate syntax.
 
-   function Image is new RHS_Sequence_Arrays.Gen_Image_Aux (Descriptor, Image);
+   function Image is new RHS_Sequence_Arrays.Gen_Image_Aux (Descriptor, 
Trimmed_Image, Image);
 
    function Min (Item : in RHS_Sequence_Arrays.Vector) return RHS_Sequence;
    --  Return element of Item with minimum length;
@@ -140,13 +151,13 @@ package WisiToken.Generate.LR is
    type Minimal_Sequence_Array is array (Token_ID range <>) of 
RHS_Sequence_Arrays.Vector;
 
    function Compute_Minimal_Terminal_Sequences
-     (Descriptor : in     WisiToken.Descriptor;
-      Grammar    : in     WisiToken.Productions.Prod_Arrays.Vector)
+     (Descriptor : in WisiToken.Descriptor;
+      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Recursions : in Generate.Recursions)
      return Minimal_Sequence_Array;
    --  For each production in Grammar, compute the minimal sequence of
    --  terminals that will complete it. Result is an empty sequence if
-   --  the production may be empty, or Invalid_Token_ID if it is
-   --  recursive.
+   --  the production may be empty.
 
    function Compute_Minimal_Terminal_First
      (Descriptor                 : in WisiToken.Descriptor;
@@ -169,7 +180,11 @@ package WisiToken.Generate.LR is
    --  terminal is a block ending or statement start.
    --
    --  The Minimal_Complete_Actions will be empty in a state where there
-   --  is nothing useful to do.
+   --  is nothing useful to do; the accept state, or one where all
+   --  productions are recursive.
+   --
+   --  Also set State.Kernels; used to resolve multiple reduce actions at
+   --  runtime.
 
    ----------
    --  Parse table output
@@ -191,11 +206,15 @@ package WisiToken.Generate.LR is
    --  Put Item to Ada.Text_IO.Current_Output in parse table format.
 
    procedure Put_Parse_Table
-     (Table      : in Parse_Table_Ptr;
-      Title      : in String;
-      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
-      Kernels    : in LR1_Items.Item_Set_List;
-      Conflicts  : in Conflict_Lists.List;
-      Descriptor : in WisiToken.Descriptor);
+     (Table                      : in Parse_Table_Ptr;
+      Title                      : in String;
+      Grammar                    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Recursions                 : in Generate.Recursions;
+      Minimal_Terminal_Sequences : in Minimal_Sequence_Array;
+      Kernels                    : in LR1_Items.Item_Set_List;
+      Conflicts                  : in Conflict_Count_Lists.List;
+      Descriptor                 : in WisiToken.Descriptor;
+      Include_Extra              : in Boolean := False);
+   --  "Extra" is recursions, lookaheads.
 
 end WisiToken.Generate.LR;
diff --git a/wisitoken-generate-lr1_items.adb b/wisitoken-generate-lr1_items.adb
index b6d14ca..86cd8d9 100644
--- a/wisitoken-generate-lr1_items.adb
+++ b/wisitoken-generate-lr1_items.adb
@@ -451,9 +451,8 @@ package body WisiToken.Generate.LR1_Items is
             Added_Item := False;
 
             if Trace_Generate > Extra then
-               Ada.Text_IO.Put_Line ("I:");
+               Ada.Text_IO.Put_Line ("  closure:");
                Put (Grammar, Descriptor, I);
-               Ada.Text_IO.New_Line;
             end if;
          else
             Item_I := Item_Lists.Next (Item_I);
diff --git a/wisitoken-generate-lr1_items.ads b/wisitoken-generate-lr1_items.ads
index 3ee55d8..0fd8c55 100644
--- a/wisitoken-generate-lr1_items.ads
+++ b/wisitoken-generate-lr1_items.ads
@@ -96,7 +96,7 @@ package WisiToken.Generate.LR1_Items is
    type Item is record
       Prod       : Production_ID;
       Dot        : Token_ID_Arrays.Cursor; -- token after item Dot
-      Lookaheads : access Lookahead := null;
+      Lookaheads : Token_ID_Set_Access := null;
       --  Programmer must remember to copy Item.Lookaheads.all, not
       --  Item.Lookaheads. Wrapping this in Ada.Finalization.Controlled
       --  would just slow it down.
diff --git a/wisitoken-generate-packrat.adb b/wisitoken-generate-packrat.adb
index 80a4c10..c50b1ed 100644
--- a/wisitoken-generate-packrat.adb
+++ b/wisitoken-generate-packrat.adb
@@ -1,247 +1,247 @@
---  Abstract :
---
---  See spec.
---
---  Copyright (C) 2018 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-package body WisiToken.Generate.Packrat is
-
-   function Potential_Direct_Right_Recursive
-     (Grammar : in WisiToken.Productions.Prod_Arrays.Vector;
-      Empty   : in Token_ID_Set)
-     return Token_ID_Set
-   is
-      subtype Nonterminal is Token_ID range Grammar.First_Index .. 
Grammar.Last_Index;
-   begin
-      return Result : Token_ID_Set (Nonterminal) := (others => False) do
-         for Prod of Grammar loop
-            RHS_Loop :
-            for RHS of Prod.RHSs loop
-               ID_Loop :
-               for I in reverse RHS.Tokens.First_Index + 1 .. 
RHS.Tokens.Last_Index loop
-                  declare
-                     ID : constant Token_ID := RHS.Tokens (I);
-                  begin
-                     if ID = Prod.LHS then
-                        Result (ID) := True;
-                        exit RHS_Loop;
-                     elsif not (ID in Nonterminal) then
-                        exit ID_Loop;
-                     elsif not Empty (ID) then
-                        exit ID_Loop;
-                     end if;
-                  end;
-               end loop ID_Loop;
-            end loop RHS_Loop;
-         end loop;
-      end return;
-   end Potential_Direct_Right_Recursive;
-
-   procedure Indirect_Left_Recursive (Data : in out Packrat.Data)
-   is
-   begin
-      for Prod_I of Data.Grammar loop
-         for Prod_J of Data.Grammar loop
-            Data.Involved (Prod_I.LHS, Prod_J.LHS) :=
-              Data.First (Prod_I.LHS, Prod_J.LHS) and
-              Data.First (Prod_J.LHS, Prod_I.LHS);
-         end loop;
-      end loop;
-   end Indirect_Left_Recursive;
-
-   ----------
-   --  Public subprograms
-
-   function Initialize
-     (Source_File_Name : in String;
-      Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
-      Source_Line_Map  : in Productions.Source_Line_Maps.Vector;
-      First_Terminal   : in Token_ID)
-     return Packrat.Data
-   is
-      Empty : constant Token_ID_Set := WisiToken.Generate.Has_Empty_Production 
(Grammar);
-   begin
-      return Result : Packrat.Data :=
-        (First_Terminal        => First_Terminal,
-         First_Nonterminal     => Grammar.First_Index,
-         Last_Nonterminal      => Grammar.Last_Index,
-         Source_File_Name      => +Source_File_Name,
-         Grammar               => Grammar,
-         Source_Line_Map       => Source_Line_Map,
-         Empty                 => Empty,
-         Direct_Left_Recursive => Potential_Direct_Left_Recursive (Grammar, 
Empty),
-         First                 => WisiToken.Generate.First (Grammar, Empty, 
First_Terminal => First_Terminal),
-         Involved              => (others => (others => False)))
-      do
-         Indirect_Left_Recursive (Result);
-      end return;
-   end Initialize;
-
-   procedure Check_Recursion (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor)
-   is
-      Right_Recursive : constant Token_ID_Set := 
Potential_Direct_Right_Recursive (Data.Grammar, Data.Empty);
-   begin
-      for Prod of Data.Grammar loop
-         if Data.Direct_Left_Recursive (Prod.LHS) and Right_Recursive 
(Prod.LHS) then
-            --  We only implement the simplest left recursion solution ([warth
-            --  2008] figure 3); [tratt 2010] section 6.3 gives this condition 
for
-            --  that to be valid.
-            --  FIXME: not quite? definite direct right recursive ok?
-            --  FIXME: for indirect left recursion, need potential indirect 
right recursive check?
-            Put_Error
-              (Error_Message
-                 (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).Line, "'" & Image (Prod.LHS, Descriptor) &
-                    "' is both left and right recursive; not supported."));
-         end if;
-
-         for I in Data.Involved'Range (2) loop
-            if Prod.LHS /= I and then Data.Involved (Prod.LHS, I) then
-               Put_Error
-                 (Error_Message
-                    (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).Line, "'" & Image (Prod.LHS, Descriptor) &
-                       "' is indirect recursive with " & Image (I, Descriptor) 
& ", not supported"));
-            end if;
-         end loop;
-      end loop;
-   end Check_Recursion;
-
-   procedure Check_RHS_Order (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor)
-   is
-      use all type Ada.Containers.Count_Type;
-   begin
-      for Prod of Data.Grammar loop
-         --  Empty must be last
-         for I in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index - 1 loop
-            if Prod.RHSs (I).Tokens.Length = 0 then
-               Put_Error
-                 (Error_Message
-                    (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).RHS_Map (I),
-                     "right hand side" & Integer'Image (I) & " in " & Image 
(Prod.LHS, Descriptor) &
-                       " is empty, but not last; no later right hand side will 
match."));
-               WisiToken.Generate.Error := True;
-            end if;
-         end loop;
-
-         for I in Prod.RHSs.First_Index + 1 .. Prod.RHSs.Last_Index loop
-            declare
-               Cur : Token_ID_Arrays.Vector renames Prod.RHSs (I).Tokens;
-            begin
-               --  Shared prefix; longer must be first
-               for J in Prod.RHSs.First_Index .. I - 1 loop
-                  declare
-                     Prev : Token_ID_Arrays.Vector renames Prod.RHSs 
(J).Tokens;
-                     K    : constant Natural := Shared_Prefix (Prev, Cur);
-                  begin
-                     if K > 0 and Prev.Length < Cur.Length then
-                        Put_Error
-                          (Error_Message
-                             (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).RHS_Map (I),
-                              "right hand side" & Integer'Image (I) & " in " & 
Image (Prod.LHS, Descriptor) &
-                                " may never match; it shares a prefix with a 
shorter previous rhs" &
-                                Integer'Image (J) & "."));
-                     end if;
-                  end;
-               end loop;
-
-               --  recursion; typical LALR list is written:
-               --
-               --  statement_list
-               --    : statement
-               --    | statement_list statement
-               --    ;
-               --  association_list
-               --    : association
-               --    | association_list COMMA association
-               --    ;
-               --
-               --  a different recursive definition:
-               --
-               --  name
-               --    : IDENTIFIER
-               --    | name LEFT_PAREN range_list RIGHT_PAREN
-               --    | name actual_parameter_part
-               --    ...
-               --    ;
-               --
-               --  For packrat, the recursive RHSs must come before others:
-               --
-               --  statement_list
-               --    : statement_list statement
-               --    | statement
-               --    ;
-               --  association_list
-               --    : association_list COMMA association
-               --    | association
-               --    ;
-               --  name
-               --    : name LEFT_PAREN range_list RIGHT_PAREN
-               --    | name actual_parameter_part
-               --    | IDENTIFIER
-               --    ...
-               --    ;
-               declare
-                  Prev : Token_ID_Arrays.Vector renames Prod.RHSs (I - 
1).Tokens;
-               begin
-                  if Cur.Length > 0 and then Prev.Length > 0 and then
-                    Cur (1) = Prod.LHS and then Prev (1) /= Prod.LHS
-                  then
-                     Put_Error
-                       (Error_Message
-                          (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).Line,
-                           "recursive right hand sides must be before 
others."));
-                  end if;
-               end;
-            end;
-         end loop;
-      end loop;
-   end Check_RHS_Order;
-
-   procedure Check_All (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor)
-   is begin
-      Check_Recursion (Data, Descriptor);
-      Check_RHS_Order (Data, Descriptor);
-   end Check_All;
-
-   function Potential_Direct_Left_Recursive
-     (Grammar : in WisiToken.Productions.Prod_Arrays.Vector;
-      Empty   : in Token_ID_Set)
-     return Token_ID_Set
-   is
-      subtype Nonterminal is Token_ID range Grammar.First_Index .. 
Grammar.Last_Index;
-   begin
-      --  FIXME: this duplicates the computation of First; if keep First,
-      --  change this to use it.
-      return Result : Token_ID_Set (Nonterminal) := (others => False) do
-         for Prod of Grammar loop
-            RHS_Loop :
-            for RHS of Prod.RHSs loop
-               ID_Loop :
-               for ID of RHS.Tokens loop
-                  if ID = Prod.LHS then
-                     Result (ID) := True;
-                     exit RHS_Loop;
-                  elsif not (ID in Nonterminal) then
-                     exit ID_Loop;
-                  elsif not Empty (ID) then
-                     exit ID_Loop;
-                  end if;
-               end loop ID_Loop;
-            end loop RHS_Loop;
-         end loop;
-      end return;
-   end Potential_Direct_Left_Recursive;
-
-end WisiToken.Generate.Packrat;
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2018 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+package body WisiToken.Generate.Packrat is
+
+   function Potential_Direct_Right_Recursive
+     (Grammar : in WisiToken.Productions.Prod_Arrays.Vector;
+      Empty   : in Token_ID_Set)
+     return Token_ID_Set
+   is
+      subtype Nonterminal is Token_ID range Grammar.First_Index .. 
Grammar.Last_Index;
+   begin
+      return Result : Token_ID_Set (Nonterminal) := (others => False) do
+         for Prod of Grammar loop
+            RHS_Loop :
+            for RHS of Prod.RHSs loop
+               ID_Loop :
+               for I in reverse RHS.Tokens.First_Index + 1 .. 
RHS.Tokens.Last_Index loop
+                  declare
+                     ID : constant Token_ID := RHS.Tokens (I);
+                  begin
+                     if ID = Prod.LHS then
+                        Result (ID) := True;
+                        exit RHS_Loop;
+                     elsif not (ID in Nonterminal) then
+                        exit ID_Loop;
+                     elsif not Empty (ID) then
+                        exit ID_Loop;
+                     end if;
+                  end;
+               end loop ID_Loop;
+            end loop RHS_Loop;
+         end loop;
+      end return;
+   end Potential_Direct_Right_Recursive;
+
+   procedure Indirect_Left_Recursive (Data : in out Packrat.Data)
+   is
+   begin
+      for Prod_I of Data.Grammar loop
+         for Prod_J of Data.Grammar loop
+            Data.Involved (Prod_I.LHS, Prod_J.LHS) :=
+              Data.First (Prod_I.LHS, Prod_J.LHS) and
+              Data.First (Prod_J.LHS, Prod_I.LHS);
+         end loop;
+      end loop;
+   end Indirect_Left_Recursive;
+
+   ----------
+   --  Public subprograms
+
+   function Initialize
+     (Source_File_Name : in String;
+      Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
+      Source_Line_Map  : in Productions.Source_Line_Maps.Vector;
+      First_Terminal   : in Token_ID)
+     return Packrat.Data
+   is
+      Empty : constant Token_ID_Set := WisiToken.Generate.Has_Empty_Production 
(Grammar);
+   begin
+      return Result : Packrat.Data :=
+        (First_Terminal        => First_Terminal,
+         First_Nonterminal     => Grammar.First_Index,
+         Last_Nonterminal      => Grammar.Last_Index,
+         Source_File_Name      => +Source_File_Name,
+         Grammar               => Grammar,
+         Source_Line_Map       => Source_Line_Map,
+         Empty                 => Empty,
+         Direct_Left_Recursive => Potential_Direct_Left_Recursive (Grammar, 
Empty),
+         First                 => WisiToken.Generate.First (Grammar, Empty, 
First_Terminal => First_Terminal),
+         Involved              => (others => (others => False)))
+      do
+         Indirect_Left_Recursive (Result);
+      end return;
+   end Initialize;
+
+   procedure Check_Recursion (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor)
+   is
+      Right_Recursive : constant Token_ID_Set := 
Potential_Direct_Right_Recursive (Data.Grammar, Data.Empty);
+   begin
+      for Prod of Data.Grammar loop
+         if Data.Direct_Left_Recursive (Prod.LHS) and Right_Recursive 
(Prod.LHS) then
+            --  We only implement the simplest left recursion solution ([warth
+            --  2008] figure 3); [tratt 2010] section 6.3 gives this condition 
for
+            --  that to be valid.
+            --  FIXME: not quite? definite direct right recursive ok?
+            --  FIXME: for indirect left recursion, need potential indirect 
right recursive check?
+            Put_Error
+              (Error_Message
+                 (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).Line, "'" & Image (Prod.LHS, Descriptor) &
+                    "' is both left and right recursive; not supported."));
+         end if;
+
+         for I in Data.Involved'Range (2) loop
+            if Prod.LHS /= I and then Data.Involved (Prod.LHS, I) then
+               Put_Error
+                 (Error_Message
+                    (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).Line, "'" & Image (Prod.LHS, Descriptor) &
+                       "' is indirect recursive with " & Image (I, Descriptor) 
& ", not supported"));
+            end if;
+         end loop;
+      end loop;
+   end Check_Recursion;
+
+   procedure Check_RHS_Order (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor)
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      for Prod of Data.Grammar loop
+         --  Empty must be last
+         for I in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index - 1 loop
+            if Prod.RHSs (I).Tokens.Length = 0 then
+               Put_Error
+                 (Error_Message
+                    (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).RHS_Map (I),
+                     "right hand side" & Integer'Image (I) & " in " & Image 
(Prod.LHS, Descriptor) &
+                       " is empty, but not last; no later right hand side will 
match."));
+               WisiToken.Generate.Error := True;
+            end if;
+         end loop;
+
+         for I in Prod.RHSs.First_Index + 1 .. Prod.RHSs.Last_Index loop
+            declare
+               Cur : Token_ID_Arrays.Vector renames Prod.RHSs (I).Tokens;
+            begin
+               --  Shared prefix; longer must be first
+               for J in Prod.RHSs.First_Index .. I - 1 loop
+                  declare
+                     Prev : Token_ID_Arrays.Vector renames Prod.RHSs 
(J).Tokens;
+                     K    : constant Natural := Shared_Prefix (Prev, Cur);
+                  begin
+                     if K > 0 and Prev.Length < Cur.Length then
+                        Put_Error
+                          (Error_Message
+                             (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).RHS_Map (I),
+                              "right hand side" & Integer'Image (I) & " in " & 
Image (Prod.LHS, Descriptor) &
+                                " may never match; it shares a prefix with a 
shorter previous rhs" &
+                                Integer'Image (J) & "."));
+                     end if;
+                  end;
+               end loop;
+
+               --  recursion; typical LALR list is written:
+               --
+               --  statement_list
+               --    : statement
+               --    | statement_list statement
+               --    ;
+               --  association_list
+               --    : association
+               --    | association_list COMMA association
+               --    ;
+               --
+               --  a different recursive definition:
+               --
+               --  name
+               --    : IDENTIFIER
+               --    | name LEFT_PAREN range_list RIGHT_PAREN
+               --    | name actual_parameter_part
+               --    ...
+               --    ;
+               --
+               --  For packrat, the recursive RHSs must come before others:
+               --
+               --  statement_list
+               --    : statement_list statement
+               --    | statement
+               --    ;
+               --  association_list
+               --    : association_list COMMA association
+               --    | association
+               --    ;
+               --  name
+               --    : name LEFT_PAREN range_list RIGHT_PAREN
+               --    | name actual_parameter_part
+               --    | IDENTIFIER
+               --    ...
+               --    ;
+               declare
+                  Prev : Token_ID_Arrays.Vector renames Prod.RHSs (I - 
1).Tokens;
+               begin
+                  if Cur.Length > 0 and then Prev.Length > 0 and then
+                    Cur (1) = Prod.LHS and then Prev (1) /= Prod.LHS
+                  then
+                     Put_Error
+                       (Error_Message
+                          (-Data.Source_File_Name, Data.Source_Line_Map 
(Prod.LHS).Line,
+                           "recursive right hand sides must be before 
others."));
+                  end if;
+               end;
+            end;
+         end loop;
+      end loop;
+   end Check_RHS_Order;
+
+   procedure Check_All (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor)
+   is begin
+      Check_Recursion (Data, Descriptor);
+      Check_RHS_Order (Data, Descriptor);
+   end Check_All;
+
+   function Potential_Direct_Left_Recursive
+     (Grammar : in WisiToken.Productions.Prod_Arrays.Vector;
+      Empty   : in Token_ID_Set)
+     return Token_ID_Set
+   is
+      subtype Nonterminal is Token_ID range Grammar.First_Index .. 
Grammar.Last_Index;
+   begin
+      --  FIXME: this duplicates the computation of First; if keep First,
+      --  change this to use it.
+      return Result : Token_ID_Set (Nonterminal) := (others => False) do
+         for Prod of Grammar loop
+            RHS_Loop :
+            for RHS of Prod.RHSs loop
+               ID_Loop :
+               for ID of RHS.Tokens loop
+                  if ID = Prod.LHS then
+                     Result (ID) := True;
+                     exit RHS_Loop;
+                  elsif not (ID in Nonterminal) then
+                     exit ID_Loop;
+                  elsif not Empty (ID) then
+                     exit ID_Loop;
+                  end if;
+               end loop ID_Loop;
+            end loop RHS_Loop;
+         end loop;
+      end return;
+   end Potential_Direct_Left_Recursive;
+
+end WisiToken.Generate.Packrat;
diff --git a/wisitoken-generate-packrat.ads b/wisitoken-generate-packrat.ads
index ec9a4cb..17bf03e 100644
--- a/wisitoken-generate-packrat.ads
+++ b/wisitoken-generate-packrat.ads
@@ -1,75 +1,75 @@
---  Abstract :
---
---  Types and operations for computing grammar properties used in
---  generating a packrat parser.
---
---  We use the terminology in [tratt 2010] for recursion in
---  productions.
---
---  References :
---
---  See wisitoken-parse-packrat.ads.
---
---  Copyright (C) 2018 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-package WisiToken.Generate.Packrat is
-
-   type Data (First_Terminal, First_Nonterminal, Last_Nonterminal : Token_ID) 
is tagged
-   record
-      --  Data needed to check a grammar and generate code. Tagged to allow
-      --  Object.Method syntax. Descriptor not included to avoid duplicating
-      --  lots of discriminants.
-      Source_File_Name      : Ada.Strings.Unbounded.Unbounded_String;
-      Grammar               : WisiToken.Productions.Prod_Arrays.Vector;
-      Source_Line_Map       : Productions.Source_Line_Maps.Vector;
-      Empty                 : Token_ID_Set (First_Nonterminal .. 
Last_Nonterminal);
-      Direct_Left_Recursive : Token_ID_Set (First_Nonterminal .. 
Last_Nonterminal);
-      First                 : Token_Array_Token_Set
-        (First_Nonterminal .. Last_Nonterminal, First_Terminal .. 
Last_Nonterminal);
-      Involved              : Token_Array_Token_Set
-        (First_Nonterminal .. Last_Nonterminal, First_Nonterminal .. 
Last_Nonterminal);
-   end record;
-
-   function Initialize
-     (Source_File_Name : in String;
-      Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
-      Source_Line_Map  : in Productions.Source_Line_Maps.Vector;
-      First_Terminal   : in Token_ID)
-     return Packrat.Data;
-
-   procedure Check_Recursion (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor);
-   --  Check that any rule recursion present is supported.
-
-   procedure Check_RHS_Order (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor);
-   --  For each production, check that right hand sides that share
-   --  prefixes have the longest right hand side first, and that any
-   --  empty right hand side is last.
-   --
-   --  Violations output a message to Ada.Text_IO.Standard_Error, and
-   --  set WisiToken.Generate.Error True.
-
-   procedure Check_All  (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor);
-   --  Run all the above checks.
-   --
-   --  Note that WisiToken.Generate.Check_Consistent is run in
-   --  wisi-gen_generate_utils.To_Grammar.
-
-   function Potential_Direct_Left_Recursive
-     (Grammar : in WisiToken.Productions.Prod_Arrays.Vector;
-      Empty   : in Token_ID_Set)
-     return Token_ID_Set;
-
-end WisiToken.Generate.Packrat;
+--  Abstract :
+--
+--  Types and operations for computing grammar properties used in
+--  generating a packrat parser.
+--
+--  We use the terminology in [tratt 2010] for recursion in
+--  productions.
+--
+--  References :
+--
+--  See wisitoken-parse-packrat.ads.
+--
+--  Copyright (C) 2018 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+package WisiToken.Generate.Packrat is
+
+   type Data (First_Terminal, First_Nonterminal, Last_Nonterminal : Token_ID) 
is tagged
+   record
+      --  Data needed to check a grammar and generate code. Tagged to allow
+      --  Object.Method syntax. Descriptor not included to avoid duplicating
+      --  lots of discriminants.
+      Source_File_Name      : Ada.Strings.Unbounded.Unbounded_String;
+      Grammar               : WisiToken.Productions.Prod_Arrays.Vector;
+      Source_Line_Map       : Productions.Source_Line_Maps.Vector;
+      Empty                 : Token_ID_Set (First_Nonterminal .. 
Last_Nonterminal);
+      Direct_Left_Recursive : Token_ID_Set (First_Nonterminal .. 
Last_Nonterminal);
+      First                 : Token_Array_Token_Set
+        (First_Nonterminal .. Last_Nonterminal, First_Terminal .. 
Last_Nonterminal);
+      Involved              : Token_Array_Token_Set
+        (First_Nonterminal .. Last_Nonterminal, First_Nonterminal .. 
Last_Nonterminal);
+   end record;
+
+   function Initialize
+     (Source_File_Name : in String;
+      Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
+      Source_Line_Map  : in Productions.Source_Line_Maps.Vector;
+      First_Terminal   : in Token_ID)
+     return Packrat.Data;
+
+   procedure Check_Recursion (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor);
+   --  Check that any rule recursion present is supported.
+
+   procedure Check_RHS_Order (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor);
+   --  For each production, check that right hand sides that share
+   --  prefixes have the longest right hand side first, and that any
+   --  empty right hand side is last.
+   --
+   --  Violations output a message to Ada.Text_IO.Standard_Error, and
+   --  set WisiToken.Generate.Error True.
+
+   procedure Check_All  (Data : in Packrat.Data; Descriptor : in 
WisiToken.Descriptor);
+   --  Run all the above checks.
+   --
+   --  Note that WisiToken.Generate.Check_Consistent is run in
+   --  wisi-gen_generate_utils.To_Grammar.
+
+   function Potential_Direct_Left_Recursive
+     (Grammar : in WisiToken.Productions.Prod_Arrays.Vector;
+      Empty   : in Token_ID_Set)
+     return Token_ID_Set;
+
+end WisiToken.Generate.Packrat;
diff --git a/wisitoken-generate.adb b/wisitoken-generate.adb
index 9796b87..769a581 100644
--- a/wisitoken-generate.adb
+++ b/wisitoken-generate.adb
@@ -1,434 +1,543 @@
---  Abstract :
---
---  See spec.
---
---  Copyright (C) 2018, 2019 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with Ada.Directories;
-with Ada.Text_IO;
-with Ada.Strings.Fixed;
-package body WisiToken.Generate is
-
-   function Error_Message
-     (File_Name : in String;
-      File_Line : in Line_Number_Type;
-      Message   : in String)
-     return String
-   is
-      use Ada.Directories;
-      use Ada.Strings.Fixed;
-      use Ada.Strings;
-   begin
-      return Simple_Name (File_Name) & ":" &
-        Trim (Line_Number_Type'Image (File_Line), Left) & ":0: " & Message;
-   end Error_Message;
-
-   procedure Put_Error (Message : in String)
-   is begin
-      Error := True;
-      Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, Message);
-   end Put_Error;
-
-   procedure Check_Consistent
-     (Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor       : in WisiToken.Descriptor;
-      Source_File_Name : in String)
-   is begin
-      if Descriptor.Accept_ID /= Descriptor.First_Nonterminal then
-         Put_Error
-           (Error_Message
-              (Source_File_Name, Line_Number_Type'First,
-               "Descriptor.Accept_ID /= Descriptor.First_Nonterminal"));
-      end if;
-      if Grammar.First_Index /= Descriptor.First_Nonterminal then
-         Put_Error
-           (Error_Message
-              (Source_File_Name, Line_Number_Type'First,
-               "Grammar.First_Index /= Descriptor.First_Nonterminal"));
-      end if;
-      if Grammar.Last_Index /= Descriptor.Last_Nonterminal then
-         Put_Error
-           (Error_Message
-              (Source_File_Name, Line_Number_Type'First,
-               "Grammar.Last_Index /= Descriptor.Last_Nonterminal"));
-      end if;
-
-      for Nonterm in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal loop
-         if Grammar (Nonterm).LHS /= Nonterm then
-            Put_Error
-              (Error_Message
-                 (Source_File_Name, Line_Number_Type'First,
-                  "Grammar (" & Image (Nonterm, Descriptor) & ").LHS /= " &
-                    Image (Nonterm, Descriptor)));
-         end if;
-      end loop;
-   end Check_Consistent;
-
-   function Check_Unused_Tokens
-     (Descriptor : in WisiToken.Descriptor;
-      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector)
-     return Boolean
-   is
-      subtype Terminals    is Token_ID range Descriptor.First_Terminal    .. 
Descriptor.Last_Terminal;
-      subtype Nonterminals is Token_ID range Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal;
-
-      Used_Tokens : Token_ID_Set := (Descriptor.First_Terminal .. 
Descriptor.Last_Nonterminal => False);
-
-      Changed        : Boolean := False;
-      Abort_Generate : Boolean := False;
-      Unused_Tokens  : Boolean := False;
-   begin
-      Used_Tokens (Descriptor.Accept_ID) := True;
-
-      --  First mark all nonterminals that occur in used nonterminals as
-      --  used.
-      loop
-         for Prod of Grammar loop
-            if Used_Tokens (Prod.LHS) then
-               for RHS of Prod.RHSs loop
-                  for J of RHS.Tokens loop
-                     if J in Nonterminals then
-                        Changed         := Changed or else not Used_Tokens (J);
-                        Used_Tokens (J) := True;
-                     end if;
-                  end loop;
-               end loop;
-            end if;
-         end loop;
-         exit when not Changed;
-         Changed := False;
-      end loop;
-
-      --  Now mark terminals used in used nonterminals
-      for Prod of Grammar loop
-         if Used_Tokens (Prod.LHS) then
-            for RHS of Prod.RHSs loop
-               for J of RHS.Tokens loop
-                  if not (J in Used_Tokens'Range) then
-                     WisiToken.Generate.Put_Error
-                       ("non-grammar token " & Image (J, Descriptor) & " used 
in grammar");
-
-                     --  This causes lots of problems with token_id not in 
terminal or
-                     --  nonterminal range, so abort early.
-                     Abort_Generate := True;
-                  end if;
-
-                  if J in Terminals then
-                     Used_Tokens (J) := True;
-                  end if;
-               end loop;
-            end loop;
-         end if;
-      end loop;
-
-      for I in Used_Tokens'Range loop
-         if not Used_Tokens (I) then
-            if not Unused_Tokens then
-               WisiToken.Generate.Put_Error ("Unused tokens:");
-               Unused_Tokens := True;
-            end if;
-            WisiToken.Generate.Put_Error (Image (I, Descriptor));
-         end if;
-      end loop;
-
-      if Abort_Generate then
-         raise Grammar_Error;
-      end if;
-
-      return Unused_Tokens;
-   end Check_Unused_Tokens;
-
-   function Has_Empty_Production (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Token_ID_Set
-   is
-      use all type Ada.Containers.Count_Type;
-
-      subtype Nonterminal is Token_ID range Grammar.First_Index .. 
Grammar.Last_Index;
-
-      Result  : Token_ID_Set := (Nonterminal => False);
-      Changed : Boolean      := True;
-   begin
-      loop
-         exit when not Changed;
-         Changed := False;
-
-         for Prod of Grammar loop
-            for RHS of Prod.RHSs loop
-               if (RHS.Tokens.Length = 0 or else
-                     (RHS.Tokens (1) in Nonterminal and then Result 
(RHS.Tokens (1)))) and
-                 not Result (Prod.LHS)
-               then
-                  Result (Prod.LHS) := True;
-                  Changed := True;
-               end if;
-            end loop;
-         end loop;
-      end loop;
-      return Result;
-   end Has_Empty_Production;
-
-   function First
-     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
-      Has_Empty_Production : in Token_ID_Set;
-      First_Terminal       : in Token_ID;
-      Non_Terminal         : in Token_ID)
-     return Token_ID_Set
-   is
-      Derivations   : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
-      Added_Tokens  : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
-      Search_Tokens : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
-
-      function Compute_Non_Terminals return Token_ID_Set
-      is
-         Result : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
-      begin
-         --  Can't use a simple aggregate for this; bounds are non-static.
-         Result (First_Terminal .. Grammar.First_Index - 1) := (others => 
False);
-         Result (Grammar.First_Index .. Grammar.Last_Index) := (others => 
True);
-         return Result;
-      end Compute_Non_Terminals;
-
-      Non_Terminals : constant Token_ID_Set := Compute_Non_Terminals;
-
-   begin
-      Search_Tokens (Non_Terminal) := True;
-
-      while Any (Search_Tokens) loop
-
-         Added_Tokens := (others => False);
-
-         for Prod of Grammar loop
-            if Search_Tokens (Prod.LHS) then
-               for RHS of Prod.RHSs loop
-                  for Derived_Token of RHS.Tokens loop
-                     if not Derivations (Derived_Token) then
-                        Added_Tokens (Derived_Token) := True;
-                     end if;
-
-                     if Non_Terminals (Derived_Token) and then 
Has_Empty_Production (Derived_Token) then
-                        null;
-                     else
-                        exit;
-                     end if;
-                  end loop;
-               end loop;
-            end if;
-         end loop;
-
-         Derivations   := Derivations or Added_Tokens;
-         Search_Tokens := Added_Tokens and Non_Terminals;
-      end loop;
-
-      return Derivations;
-   end First;
-
-   function First
-     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
-      Has_Empty_Production : in Token_ID_Set;
-      First_Terminal       : in Token_ID)
-     return Token_Array_Token_Set
-   is
-      Matrix : Token_Array_Token_Set :=
-        (Grammar.First_Index .. Grammar.Last_Index =>
-           (First_Terminal .. Grammar.Last_Index => False));
-
-      procedure Set_Slice (Matrix : in out Token_Array_Token_Set; I : 
Token_ID; Value : in Token_ID_Set)
-      is begin
-         for J in Matrix'Range (2) loop
-            Matrix (I, J) := Value (J);
-         end loop;
-      end Set_Slice;
-
-   begin
-      for NT_Index in Matrix'Range loop
-         Set_Slice (Matrix, NT_Index, First (Grammar, Has_Empty_Production, 
First_Terminal, NT_Index));
-      end loop;
-
-      return Matrix;
-   end First;
-
-   function To_Terminal_Sequence_Array
-     (First      : in Token_Array_Token_Set;
-      Descriptor : in WisiToken.Descriptor)
-     return Token_Sequence_Arrays.Vector
-   is
-      subtype Terminal is Token_ID range Descriptor.First_Terminal .. 
Descriptor.Last_Terminal;
-   begin
-      return Result : Token_Sequence_Arrays.Vector do
-         Result.Set_First (First'First (1));
-         Result.Set_Last (First'Last (1));
-
-         for I in First'Range (1) loop
-            declare
-               Row : Token_ID_Arrays.Vector renames Result (I);
-            begin
-               for J in First'Range (2) loop
-                  if First (I, J) and then J in Terminal then
-                     Row.Append (J);
-                  end if;
-               end loop;
-            end;
-         end loop;
-      end return;
-   end To_Terminal_Sequence_Array;
-
-   function Follow
-     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor           : in WisiToken.Descriptor;
-      First                : in Token_Array_Token_Set;
-      Has_Empty_Production : in Token_ID_Set)
-     return Token_Array_Token_Set
-   is
-      subtype Terminal    is Token_ID range Descriptor.First_Terminal    .. 
Descriptor.Last_Terminal;
-      subtype Nonterminal is Token_ID range Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal;
-
-      Prev_Result : Token_Array_Token_Set := (Nonterminal => (Terminal => 
False));
-      Result      : Token_Array_Token_Set := (Nonterminal => (Terminal => 
False));
-
-      ID : Token_ID;
-   begin
-      --  [dragon] pgp 189:
-      --
-      --  Rule 1 Follow (S, EOF) = True; EOF is explicit in the
-      --  start symbol production, so this is covered by Rule 2.
-      --
-      --  Rule 2: If A => alpha B Beta, add First (Beta) to Follow (B)
-      --
-      --  Rule 3; if A => alpha B, or A -> alpha B Beta and Beta
-      --  can be null, add Follow (A) to Follow (B)
-      --
-      --  We don't assume any order in the productions list, so we
-      --  have to keep applying rule 3 until nothing changes.
-
-      for B in Nonterminal loop
-         for Prod of Grammar loop
-            for A of Prod.RHSs loop
-               for I in A.Tokens.First_Index .. A.Tokens.Last_Index loop
-                  if A.Tokens (I) = B then
-                     if I < A.Tokens.Last_Index then
-                        --  Rule 1
-                        ID := A.Tokens (1 + I);
-                        if ID in Terminal then
-                           Result (B, ID) := True;
-                        else
-                           Or_Slice (Result, B, Slice (First, ID));
-                        end if;
-                     end if;
-                  end if;
-               end loop;
-            end loop;
-         end loop;
-      end loop;
-
-      Prev_Result := Result;
-      loop
-         for B in Nonterminal loop
-            for Prod of Grammar loop
-               for A of Prod.RHSs loop
-                  for I in A.Tokens.First_Index .. A.Tokens.Last_Index loop
-                     if A.Tokens (I) = B then
-                        if I = A.Tokens.Last_Index or else
-                          (A.Tokens (1 + I) in Nonterminal and then
-                             Has_Empty_Production (A.Tokens (1 + I)))
-                        then
-                           --  rule 3
-                           Or_Slice (Result, B, Slice (Result, Prod.LHS));
-                        end if;
-                     end if;
-                  end loop;
-               end loop;
-            end loop;
-         end loop;
-
-         exit when Prev_Result = Result;
-         Prev_Result := Result;
-      end loop;
-      return Result;
-   end Follow;
-
-   ----------
-   --  Indented text output
-
-   procedure Indent_Line (Text : in String)
-   is
-      use Ada.Text_IO;
-   begin
-      Set_Col (Indent);
-      Put_Line (Text);
-      Line_Count := Line_Count + 1;
-   end Indent_Line;
-
-   procedure Indent_Start (Text : in String)
-   is
-      use Ada.Text_IO;
-   begin
-      Set_Col (Indent);
-      Put (Text);
-   end Indent_Start;
-
-   procedure Indent_Wrap (Text : in String)
-   is
-      use all type Ada.Text_IO.Count;
-      use Ada.Strings;
-      use Ada.Strings.Fixed;
-      I     : Natural;
-      First : Integer := Text'First;
-   begin
-      if Text'Length + Indent <= Max_Line_Length then
-         Indent_Line (Text);
-      else
-         loop
-            I := Text'Last;
-            loop
-               I := Index (Text (First .. Text'Last), " ", From => I, Going => 
Backward);
-               exit when I - First + Integer (Indent) <= Max_Line_Length;
-               I := I - 1;
-            end loop;
-            Indent_Line (Text (First .. I - 1));
-            First := I + 1;
-            exit when Text'Last - First + Integer (Indent) <= Max_Line_Length;
-         end loop;
-         Indent_Line (Text (First .. Text'Last));
-      end if;
-   end Indent_Wrap;
-
-   procedure Indent_Wrap_Comment (Text : in String; Comment_Syntax : in String)
-   is
-      use all type Ada.Text_IO.Count;
-      use Ada.Strings;
-      use Ada.Strings.Fixed;
-      Prefix : constant String := Comment_Syntax & "  ";
-      I      : Natural;
-      First  : Integer         := Text'First;
-   begin
-      if Text'Length + Indent <= Max_Line_Length - 4 then
-         Indent_Line (Prefix & Text);
-      else
-         loop
-            I := Text'Last;
-            loop
-               I := Index (Text (First .. Text'Last), " ", From => I, Going => 
Backward);
-               exit when I - First + Integer (Indent) <= Max_Line_Length - 4;
-               I := I - 1;
-            end loop;
-            Indent_Line (Prefix & Text (First .. I - 1));
-            First := I + 1;
-            exit when Text'Last - First + Integer (Indent) <= Max_Line_Length 
- 4;
-         end loop;
-         Indent_Line (Prefix & Text (First .. Text'Last));
-      end if;
-   end Indent_Wrap_Comment;
-
-end WisiToken.Generate;
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2018, 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Directories;
+with Ada.Text_IO;
+with Ada.Strings.Fixed;
+package body WisiToken.Generate is
+
+   function Error_Message
+     (File_Name : in String;
+      File_Line : in Line_Number_Type;
+      Message   : in String)
+     return String
+   is
+      use Ada.Directories;
+      use Ada.Strings.Fixed;
+      use Ada.Strings;
+   begin
+      return Simple_Name (File_Name) & ":" &
+        Trim (Line_Number_Type'Image (File_Line), Left) & ":0: " & Message;
+   end Error_Message;
+
+   procedure Put_Error (Message : in String)
+   is begin
+      Error := True;
+      Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, Message);
+   end Put_Error;
+
+   procedure Check_Consistent
+     (Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor       : in WisiToken.Descriptor;
+      Source_File_Name : in String)
+   is begin
+      if Descriptor.Accept_ID /= Descriptor.First_Nonterminal then
+         Put_Error
+           (Error_Message
+              (Source_File_Name, Line_Number_Type'First,
+               "Descriptor.Accept_ID /= Descriptor.First_Nonterminal"));
+      end if;
+      if Grammar.First_Index /= Descriptor.First_Nonterminal then
+         Put_Error
+           (Error_Message
+              (Source_File_Name, Line_Number_Type'First,
+               "Grammar.First_Index /= Descriptor.First_Nonterminal"));
+      end if;
+      if Grammar.Last_Index /= Descriptor.Last_Nonterminal then
+         Put_Error
+           (Error_Message
+              (Source_File_Name, Line_Number_Type'First,
+               "Grammar.Last_Index /= Descriptor.Last_Nonterminal"));
+      end if;
+
+      for Nonterm in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal loop
+         if Grammar (Nonterm).LHS /= Nonterm then
+            Put_Error
+              (Error_Message
+                 (Source_File_Name, Line_Number_Type'First,
+                  "Grammar (" & Image (Nonterm, Descriptor) & ").LHS = " &
+                    Image (Grammar (Nonterm).LHS, Descriptor) & " /= " &
+                    Image (Nonterm, Descriptor)));
+         end if;
+      end loop;
+   end Check_Consistent;
+
+   function Check_Unused_Tokens
+     (Descriptor : in WisiToken.Descriptor;
+      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector)
+     return Boolean
+   is
+      subtype Terminals    is Token_ID range Descriptor.First_Terminal    .. 
Descriptor.Last_Terminal;
+      subtype Nonterminals is Token_ID range Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal;
+
+      Used_Tokens : Token_ID_Set := (Descriptor.First_Terminal .. 
Descriptor.Last_Nonterminal => False);
+
+      Changed        : Boolean := False;
+      Abort_Generate : Boolean := False;
+      Unused_Tokens  : Boolean := False;
+   begin
+      Used_Tokens (Descriptor.Accept_ID) := True;
+
+      --  First mark all nonterminals that occur in used nonterminals as
+      --  used.
+      loop
+         for Prod of Grammar loop
+            if Used_Tokens (Prod.LHS) then
+               for RHS of Prod.RHSs loop
+                  for J of RHS.Tokens loop
+                     if J in Nonterminals then
+                        Changed         := Changed or else not Used_Tokens (J);
+                        Used_Tokens (J) := True;
+                     end if;
+                  end loop;
+               end loop;
+            end if;
+         end loop;
+         exit when not Changed;
+         Changed := False;
+      end loop;
+
+      --  Now mark terminals used in used nonterminals
+      for Prod of Grammar loop
+         if Used_Tokens (Prod.LHS) then
+            for RHS of Prod.RHSs loop
+               for J of RHS.Tokens loop
+                  if not (J in Used_Tokens'Range) then
+                     WisiToken.Generate.Put_Error
+                       ("non-grammar token " & Image (J, Descriptor) & " used 
in grammar");
+
+                     --  This causes lots of problems with token_id not in 
terminal or
+                     --  nonterminal range, so abort early.
+                     Abort_Generate := True;
+                  end if;
+
+                  if J in Terminals then
+                     Used_Tokens (J) := True;
+                  end if;
+               end loop;
+            end loop;
+         end if;
+      end loop;
+
+      for I in Used_Tokens'Range loop
+         if not Used_Tokens (I) then
+            if not Unused_Tokens then
+               WisiToken.Generate.Put_Error ("Unused tokens:");
+               Unused_Tokens := True;
+            end if;
+            WisiToken.Generate.Put_Error (Image (I, Descriptor));
+         end if;
+      end loop;
+
+      if Abort_Generate then
+         raise Grammar_Error;
+      end if;
+
+      return Unused_Tokens;
+   end Check_Unused_Tokens;
+
+   function Has_Empty_Production (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Token_ID_Set
+   is
+      use all type Ada.Containers.Count_Type;
+
+      subtype Nonterminal is Token_ID range Grammar.First_Index .. 
Grammar.Last_Index;
+
+      Result  : Token_ID_Set := (Nonterminal => False);
+      Changed : Boolean      := True;
+   begin
+      loop
+         exit when not Changed;
+         Changed := False;
+
+         for Prod of Grammar loop
+            for RHS of Prod.RHSs loop
+               if (RHS.Tokens.Length = 0 or else
+                     (RHS.Tokens (1) in Nonterminal and then Result 
(RHS.Tokens (1)))) and
+                 not Result (Prod.LHS)
+               then
+                  Result (Prod.LHS) := True;
+                  Changed := True;
+               end if;
+            end loop;
+         end loop;
+      end loop;
+      return Result;
+   end Has_Empty_Production;
+
+   function First
+     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in Token_ID_Set;
+      First_Terminal       : in Token_ID;
+      Non_Terminal         : in Token_ID)
+     return Token_ID_Set
+   is
+      Derivations   : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
+      Added_Tokens  : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
+      Search_Tokens : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
+
+      function Compute_Non_Terminals return Token_ID_Set
+      is
+         Result : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => 
False);
+      begin
+         --  Can't use a simple aggregate for this; bounds are non-static.
+         Result (First_Terminal .. Grammar.First_Index - 1) := (others => 
False);
+         Result (Grammar.First_Index .. Grammar.Last_Index) := (others => 
True);
+         return Result;
+      end Compute_Non_Terminals;
+
+      Non_Terminals : constant Token_ID_Set := Compute_Non_Terminals;
+
+   begin
+      Search_Tokens (Non_Terminal) := True;
+
+      while Any (Search_Tokens) loop
+
+         Added_Tokens := (others => False);
+
+         for Prod of Grammar loop
+            if Search_Tokens (Prod.LHS) then
+               for RHS of Prod.RHSs loop
+                  for Derived_Token of RHS.Tokens loop
+                     if not Derivations (Derived_Token) then
+                        Added_Tokens (Derived_Token) := True;
+                     end if;
+
+                     if Non_Terminals (Derived_Token) and then 
Has_Empty_Production (Derived_Token) then
+                        null;
+                     else
+                        exit;
+                     end if;
+                  end loop;
+               end loop;
+            end if;
+         end loop;
+
+         Derivations   := Derivations or Added_Tokens;
+         Search_Tokens := Added_Tokens and Non_Terminals;
+      end loop;
+
+      return Derivations;
+   end First;
+
+   function First
+     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in Token_ID_Set;
+      First_Terminal       : in Token_ID)
+     return Token_Array_Token_Set
+   is
+      Matrix : Token_Array_Token_Set :=
+        (Grammar.First_Index .. Grammar.Last_Index =>
+           (First_Terminal .. Grammar.Last_Index => False));
+
+      procedure Set_Slice (Matrix : in out Token_Array_Token_Set; I : 
Token_ID; Value : in Token_ID_Set)
+      is begin
+         for J in Matrix'Range (2) loop
+            Matrix (I, J) := Value (J);
+         end loop;
+      end Set_Slice;
+
+   begin
+      for NT_Index in Matrix'Range loop
+         Set_Slice (Matrix, NT_Index, First (Grammar, Has_Empty_Production, 
First_Terminal, NT_Index));
+      end loop;
+
+      return Matrix;
+   end First;
+
+   function To_Terminal_Sequence_Array
+     (First      : in Token_Array_Token_Set;
+      Descriptor : in WisiToken.Descriptor)
+     return Token_Sequence_Arrays.Vector
+   is
+      subtype Terminal is Token_ID range Descriptor.First_Terminal .. 
Descriptor.Last_Terminal;
+   begin
+      return Result : Token_Sequence_Arrays.Vector do
+         Result.Set_First (First'First (1));
+         Result.Set_Last (First'Last (1));
+
+         for I in First'Range (1) loop
+            declare
+               Row : Token_ID_Arrays.Vector renames Result (I);
+            begin
+               for J in First'Range (2) loop
+                  if First (I, J) and then J in Terminal then
+                     Row.Append (J);
+                  end if;
+               end loop;
+            end;
+         end loop;
+      end return;
+   end To_Terminal_Sequence_Array;
+
+   function Follow
+     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor           : in WisiToken.Descriptor;
+      First                : in Token_Array_Token_Set;
+      Has_Empty_Production : in Token_ID_Set)
+     return Token_Array_Token_Set
+   is
+      subtype Terminal    is Token_ID range Descriptor.First_Terminal    .. 
Descriptor.Last_Terminal;
+      subtype Nonterminal is Token_ID range Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal;
+
+      Prev_Result : Token_Array_Token_Set := (Nonterminal => (Terminal => 
False));
+      Result      : Token_Array_Token_Set := (Nonterminal => (Terminal => 
False));
+
+      ID : Token_ID;
+   begin
+      --  [dragon] pgp 189:
+      --
+      --  Rule 1 Follow (S, EOF) = True; EOF is explicit in the
+      --  start symbol production, so this is covered by Rule 2.
+      --
+      --  Rule 2: If A => alpha B Beta, add First (Beta) to Follow (B)
+      --
+      --  Rule 3; if A => alpha B, or A -> alpha B Beta and Beta
+      --  can be null, add Follow (A) to Follow (B)
+      --
+      --  We don't assume any order in the productions list, so we
+      --  have to keep applying rule 3 until nothing changes.
+
+      for B in Nonterminal loop
+         for Prod of Grammar loop
+            for A of Prod.RHSs loop
+               for I in A.Tokens.First_Index .. A.Tokens.Last_Index loop
+                  if A.Tokens (I) = B then
+                     if I < A.Tokens.Last_Index then
+                        --  Rule 1
+                        ID := A.Tokens (1 + I);
+                        if ID in Terminal then
+                           Result (B, ID) := True;
+                        else
+                           Or_Slice (Result, B, Slice (First, ID));
+                        end if;
+                     end if;
+                  end if;
+               end loop;
+            end loop;
+         end loop;
+      end loop;
+
+      Prev_Result := Result;
+      loop
+         for B in Nonterminal loop
+            for Prod of Grammar loop
+               for A of Prod.RHSs loop
+                  for I in A.Tokens.First_Index .. A.Tokens.Last_Index loop
+                     if A.Tokens (I) = B then
+                        if I = A.Tokens.Last_Index or else
+                          (A.Tokens (1 + I) in Nonterminal and then
+                             Has_Empty_Production (A.Tokens (1 + I)))
+                        then
+                           --  rule 3
+                           Or_Slice (Result, B, Slice (Result, Prod.LHS));
+                        end if;
+                     end if;
+                  end loop;
+               end loop;
+            end loop;
+         end loop;
+
+         exit when Prev_Result = Result;
+         Prev_Result := Result;
+      end loop;
+      return Result;
+   end Follow;
+
+   function To_Graph (Grammar : in WisiToken.Productions.Prod_Arrays.Vector) 
return Grammar_Graphs.Graph
+   is
+      use all type Ada.Containers.Count_Type;
+      subtype Nonterminals is Token_ID range Grammar.First_Index .. 
Grammar.Last_Index;
+      Graph : Grammar_Graphs.Graph;
+      J     : Integer := 1;
+   begin
+      if Trace_Generate > Outline then
+         Ada.Text_IO.Put_Line ("grammar graph:");
+      end if;
+
+      for LHS in Grammar.First_Index .. Grammar.Last_Index loop
+         declare
+            Prod : WisiToken.Productions.Instance renames Grammar (LHS);
+         begin
+            for RHS in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
+               declare
+                  Tokens : Token_ID_Arrays.Vector renames Prod.RHSs 
(RHS).Tokens;
+               begin
+                  for I in Tokens.First_Index .. Tokens.Last_Index loop
+                     if Tokens (I) in Nonterminals then
+                        if Trace_Generate > Detail then
+                           Ada.Text_IO.Put_Line
+                             ("(" & Trimmed_Image (LHS) & ", " & Trimmed_Image 
(Tokens (I)) & ","  & J'Image & ")");
+                           J := J + 1;
+                        end if;
+                        Graph.Add_Edge
+                          (LHS, Tokens (I),
+                           (RHS,
+                            Recursive =>
+                              (if Tokens.Length = 1 then Single
+                               elsif I = Tokens.First_Index then Left
+                               elsif I = Tokens.Last_Index then Right
+                               else Middle)));
+                     end if;
+                  end loop;
+               end;
+            end loop;
+         end;
+      end loop;
+
+      if Trace_Generate > Outline then
+         Ada.Text_IO.Put_Line ("..." & Graph.Count_Nodes'Image & " nodes" & 
Graph.Count_Edges'Image & " edges.");
+      end if;
+      return Graph;
+   end To_Graph;
+
+   function Compute_Full_Recursion (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Recursions
+   is
+      Graph : constant Grammar_Graphs.Graph := To_Graph (Grammar);
+   begin
+      return Result : Recursions :=
+        (Full       => True,
+         Recursions => Graph.Find_Cycles)
+      do
+         Grammar_Graphs.Sort_Paths.Sort (Result.Recursions);
+         if Trace_Generate > Extra then
+            Ada.Text_IO.Put_Line ("full recursions:");
+            for I in Result.Recursions.First_Index .. 
Result.Recursions.Last_Index loop
+               Ada.Text_IO.Put_Line (Trimmed_Image (I) & " => " & 
Grammar_Graphs.Image (Result.Recursions (I)));
+            end loop;
+         end if;
+      end return;
+   end Compute_Full_Recursion;
+
+   function Compute_Partial_Recursion (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Recursions
+   is
+      use Grammar_Graphs;
+      Graph      : constant Grammar_Graphs.Graph := To_Graph (Grammar);
+      Components : constant Component_Lists.List := 
Strongly_Connected_Components
+        (To_Adjancency (Graph), Non_Trivial_Only => True);
+      Loops      : constant Vertex_Lists.List    := Graph.Loops;
+   begin
+      return Result : Recursions do
+         Result.Full := False;
+         for Comp of Components loop
+            declare
+               Path : Recursion_Cycle (1 .. Integer (Comp.Length));
+               Last : Integer := Path'First - 1;
+            begin
+               for V of Comp loop
+                  Last := Last + 1;
+                  Path (Last) := (V, Graph.Edges (V));
+               end loop;
+               Result.Recursions.Append (Path);
+            end;
+         end loop;
+
+         declare
+            Path : Recursion_Cycle (1 .. Integer (Loops.Length));
+            Last : Integer := Path'First - 1;
+         begin
+            for V of Loops loop
+               Last := Last + 1;
+               Path (Last) := (V, Graph.Edges (V));
+            end loop;
+            Result.Recursions.Append (Path);
+         end;
+
+         if Trace_Generate > Extra then
+            Ada.Text_IO.Put_Line ("partial recursions:");
+            for I in Result.Recursions.First_Index .. 
Result.Recursions.Last_Index loop
+               Ada.Text_IO.Put_Line (Trimmed_Image (I) & " => " & 
Grammar_Graphs.Image (Result.Recursions (I)));
+            end loop;
+         end if;
+      end return;
+   end Compute_Partial_Recursion;
+
+   ----------
+   --  Indented text output
+
+   procedure Indent_Line (Text : in String)
+   is
+      use Ada.Text_IO;
+   begin
+      Set_Col (Indent);
+      Put_Line (Text);
+      Line_Count := Line_Count + 1;
+   end Indent_Line;
+
+   procedure Indent_Start (Text : in String)
+   is
+      use Ada.Text_IO;
+   begin
+      Set_Col (Indent);
+      Put (Text);
+   end Indent_Start;
+
+   procedure Indent_Wrap (Text : in String)
+   is
+      use all type Ada.Text_IO.Count;
+      use Ada.Strings;
+      use Ada.Strings.Fixed;
+      I     : Natural;
+      First : Integer := Text'First;
+   begin
+      if Text'Length + Indent <= Max_Line_Length then
+         Indent_Line (Text);
+      else
+         loop
+            I := Text'Last;
+            loop
+               I := Index (Text (First .. Text'Last), " ", From => I, Going => 
Backward);
+               exit when I - First + Integer (Indent) <= Max_Line_Length;
+               I := I - 1;
+            end loop;
+            Indent_Line (Text (First .. I - 1));
+            First := I + 1;
+            exit when Text'Last - First + Integer (Indent) <= Max_Line_Length;
+         end loop;
+         Indent_Line (Text (First .. Text'Last));
+      end if;
+   end Indent_Wrap;
+
+   procedure Indent_Wrap_Comment (Text : in String; Comment_Syntax : in String)
+   is
+      use all type Ada.Text_IO.Count;
+      use Ada.Strings;
+      use Ada.Strings.Fixed;
+      Prefix : constant String := Comment_Syntax & "  ";
+      I      : Natural;
+      First  : Integer         := Text'First;
+   begin
+      if Text'Length + Indent <= Max_Line_Length - 4 then
+         Indent_Line (Prefix & Text);
+      else
+         loop
+            I := Text'Last;
+            loop
+               I := Index (Text (First .. Text'Last), " ", From => I, Going => 
Backward);
+               exit when I - First + Integer (Indent) <= Max_Line_Length - 4;
+               I := I - 1;
+            end loop;
+            Indent_Line (Prefix & Text (First .. I - 1));
+            First := I + 1;
+            exit when Text'Last - First + Integer (Indent) <= Max_Line_Length 
- 4;
+         end loop;
+         Indent_Line (Prefix & Text (First .. Text'Last));
+      end if;
+   end Indent_Wrap_Comment;
+
+end WisiToken.Generate;
diff --git a/wisitoken-generate.ads b/wisitoken-generate.ads
index 01340eb..45d8c7c 100644
--- a/wisitoken-generate.ads
+++ b/wisitoken-generate.ads
@@ -1,121 +1,189 @@
---  Abstract :
---
---  Types and operations for generating parsers, common to all parser
---  types.
---
---  The wisi* packages deal with reading *.wy files and generating
---  source code files. The wisitoken-generate* packages deal with
---  computing parser properties from the grammar. (For historical
---  reasons, not all packages follow this naming convention yet).
---
---  References :
---
---  See wisitoken.ads
---
---  Copyright (C) 2018, 2019 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with WisiToken.Productions;
-package WisiToken.Generate is
-
-   Error : Boolean := False;
-   --  Set True by errors during grammar generation
-
-   function Error_Message
-     (File_Name : in String;
-      File_Line : in WisiToken.Line_Number_Type;
-      Message   : in String)
-     return String;
-
-   procedure Put_Error (Message : in String);
-   --  Set Error True, output Message to Standard_Error
-
-   procedure Check_Consistent
-     (Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor       : in WisiToken.Descriptor;
-      Source_File_Name : in String);
-   --  Check requirements on Descriptor values.
-
-   function Check_Unused_Tokens
-     (Descriptor : in WisiToken.Descriptor;
-      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector)
-     return Boolean;
-   --  Return False if there is a terminal or nonterminal that is not
-   --  used in the grammar.
-   --
-   --  Raises Grammar_Error if there is a non-grammar token used in the
-   --  grammar.
-
-   function Has_Empty_Production (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Token_ID_Set;
-   --  Result (ID) is True if any production for ID can be an empty
-   --  production, recursively.
-
-   function First
-     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
-      Has_Empty_Production : in Token_ID_Set;
-      First_Terminal       : in Token_ID)
-     return Token_Array_Token_Set;
-   --  For each nonterminal in Grammar, find the set of tokens
-   --  (terminal or nonterminal) that any string derived from it can
-   --  start with. Together with Has_Empty_Production, implements
-   --  algorithm FIRST from [dragon], augmented with nonterminals.
-   --
-   --  LALR, LR1 generate want First as both Token_Sequence_Arrays.Vector
-   --  and Token_Array_Token_Set, Packrat wants Token_Array_Token_Set,
-   --  existing tests all use Token_Array_Token_Set. So for LR1 we use
-   --  To_Terminal_Sequence_Array.
-
-   function To_Terminal_Sequence_Array
-     (First      : in Token_Array_Token_Set;
-      Descriptor : in WisiToken.Descriptor)
-     return Token_Sequence_Arrays.Vector;
-   --  Only includes terminals.
-
-   function Follow
-     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
-      Descriptor           : in WisiToken.Descriptor;
-      First                : in Token_Array_Token_Set;
-      Has_Empty_Production : in Token_ID_Set)
-     return Token_Array_Token_Set;
-   --  For each nonterminal in Grammar, find the set of terminal
-   --  tokens that can follow it. Implements algorithm FOLLOW from
-   --  [dragon] pg 189.
-
-   ----------
-   --  Indented text output. Mostly used for code generation in wisi,
-   --  also used in outputing the parse_table and other debug stuff.
-
-   Max_Line_Length : constant := 120;
-
-   Indent     : Standard.Ada.Text_IO.Positive_Count := 1;
-   Line_Count : Integer;
-
-   procedure Indent_Line (Text : in String);
-   --  Put Text, indented to Indent, to Current_Output, with newline.
-
-   procedure Indent_Start (Text : in String);
-   --  Put Text indented to Indent to Current_Output, without newline.
-   --  Should be followed by Put_Line, not Indent_Line.
-
-   procedure Indent_Wrap (Text : in String);
-   --  Put Text, indented to Indent, wrapped at Max_Line_Length, to
-   --  Current_Output, ending with newline.
-
-   procedure Indent_Wrap_Comment (Text : in String; Comment_Syntax : in 
String);
-   --  Put Text, prefixed by Comment_Syntax and two spaces, indented to
-   --  Indent, wrapped at Max_Line_Length, to Current_Output, ending with
-   --  newline.
-
-end WisiToken.Generate;
+--  Abstract :
+--
+--  Types and operations for generating parsers, common to all parser
+--  types.
+--
+--  The wisi* packages deal with reading *.wy files and generating
+--  source code files. The wisitoken-generate* packages deal with
+--  computing parser properties from the grammar. (For historical
+--  reasons, not all packages follow this naming convention yet).
+--
+--  References :
+--
+--  See wisitoken.ads
+--
+--  Copyright (C) 2018, 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Containers.Doubly_Linked_Lists;
+with SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image;
+with SAL.Gen_Graphs;
+with WisiToken.Productions;
+package WisiToken.Generate is
+
+   Error : Boolean := False;
+   --  Set True by errors during grammar generation
+
+   function Error_Message
+     (File_Name : in String;
+      File_Line : in WisiToken.Line_Number_Type;
+      Message   : in String)
+     return String;
+
+   procedure Put_Error (Message : in String);
+   --  Set Error True, output Message to Standard_Error
+
+   procedure Check_Consistent
+     (Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor       : in WisiToken.Descriptor;
+      Source_File_Name : in String);
+   --  Check requirements on Descriptor values.
+
+   function Check_Unused_Tokens
+     (Descriptor : in WisiToken.Descriptor;
+      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector)
+     return Boolean;
+   --  Return False if there is a terminal or nonterminal that is not
+   --  used in the grammar.
+   --
+   --  Raises Grammar_Error if there is a non-grammar token used in the
+   --  grammar.
+
+   function Has_Empty_Production (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Token_ID_Set;
+   --  Result (ID) is True if any production for ID can be an empty
+   --  production, recursively.
+
+   function First
+     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in Token_ID_Set;
+      First_Terminal       : in Token_ID)
+     return Token_Array_Token_Set;
+   --  For each nonterminal in Grammar, find the set of tokens
+   --  (terminal or nonterminal) that any string derived from it can
+   --  start with. Together with Has_Empty_Production, implements
+   --  algorithm FIRST from [dragon], augmented with nonterminals.
+   --
+   --  LALR, LR1 generate want First as both Token_Sequence_Arrays.Vector
+   --  and Token_Array_Token_Set, Packrat wants Token_Array_Token_Set,
+   --  existing tests all use Token_Array_Token_Set. So for LR1 we use
+   --  To_Terminal_Sequence_Array.
+
+   function To_Terminal_Sequence_Array
+     (First      : in Token_Array_Token_Set;
+      Descriptor : in WisiToken.Descriptor)
+     return Token_Sequence_Arrays.Vector;
+   --  Only includes terminals.
+
+   function Follow
+     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor           : in WisiToken.Descriptor;
+      First                : in Token_Array_Token_Set;
+      Has_Empty_Production : in Token_ID_Set)
+     return Token_Array_Token_Set;
+   --  For each nonterminal in Grammar, find the set of terminal
+   --  tokens that can follow it. Implements algorithm FOLLOW from
+   --  [dragon] pg 189.
+
+   ----------
+   --  Recursion
+
+   --  Recursion is the result of a cycle in the grammar. We can form a
+   --  graph representing the grammar by taking the nonterminals as the
+   --  graph vertices, and the occurence of a nonterminal in a production
+   --  right hand side as a directed edge connecting two nonterminals
+   --  (the other is the left hand side of that production. Then
+   --  recursion is represented by a cycle in the graph.
+
+   type Recursion_Item is record
+      RHS : Natural := 0;
+      --  The edge leading to this node. We don't need the actual token
+      --  number.
+
+      Recursive : Recursion := None;
+      --  Position of the token in the RHS.
+   end record;
+
+   function Edge_Image (Edge : in Recursion_Item) return String is
+     (Trimmed_Image (Edge.RHS) & " " & Recursion'Image (Edge.Recursive));
+
+   type Base_Recursion_Index is range 0 .. Integer'Last;
+   subtype Recursion_Index is Base_Recursion_Index range 1 .. 
Base_Recursion_Index'Last;
+   Invalid_Recursion_Index : constant Base_Recursion_Index := 0;
+   function Trimmed_Image is new SAL.Gen_Trimmed_Image (Base_Recursion_Index);
+
+   package Grammar_Graphs is new SAL.Gen_Graphs
+     (Edge_Data         => Recursion_Item,
+      Default_Edge_Data => (others => <>),
+      Vertex_Index      => Token_ID,
+      Invalid_Vertex    => Invalid_Token_ID,
+      Path_Index        => Recursion_Index,
+      Edge_Image        => Edge_Image);
+
+   subtype Recursion_Cycle is Grammar_Graphs.Path;
+   --  A recursion, with lowest numbered production first. If there is
+   --  only one element, the recursion is direct; otherwise indirect.
+
+   subtype Recursion_Array is Grammar_Graphs.Path_Arrays.Vector;
+   --  For the collection of all cycles.
+
+   type Recursions is record
+      Full       : Boolean;
+      Recursions : Recursion_Array;
+      --  If Full, elements are paths; edges at path (I) are to path (I). If
+      --  not Full, elements are strongly connected components; edges at
+      --  path (I) are from path (I).
+   end record;
+
+   package Recursion_Lists is new Ada.Containers.Doubly_Linked_Lists 
(Recursion_Index);
+   function Image is new SAL.Ada_Containers.Gen_Doubly_Linked_Lists_Image
+     (Recursion_Index, "=", Recursion_Lists, Trimmed_Image);
+
+   function To_Graph (Grammar : in WisiToken.Productions.Prod_Arrays.Vector) 
return Grammar_Graphs.Graph;
+
+   function Compute_Full_Recursion (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Recursions;
+   --  Each element of result is a cycle in the grammar.
+
+   function Compute_Partial_Recursion (Grammar : in 
WisiToken.Productions.Prod_Arrays.Vector) return Recursions;
+   --  Each element of the result contains all members of a non-trivial
+   --  strongly connected component in the grammar, in arbitrary order.
+   --  This is an approximation to the full recursion, when that is too
+   --  hard to compute (ie for Java).
+
+   ----------
+   --  Indented text output. Mostly used for code generation in wisi,
+   --  also used in outputing the parse_table and other debug stuff.
+
+   Max_Line_Length : constant := 120;
+
+   Indent     : Standard.Ada.Text_IO.Positive_Count := 1;
+   Line_Count : Integer;
+
+   procedure Indent_Line (Text : in String);
+   --  Put Text, indented to Indent, to Current_Output, with newline.
+
+   procedure Indent_Start (Text : in String);
+   --  Put Text indented to Indent to Current_Output, without newline.
+   --  Should be followed by Put_Line, not Indent_Line.
+
+   procedure Indent_Wrap (Text : in String);
+   --  Put Text, indented to Indent, wrapped at Max_Line_Length, to
+   --  Current_Output, ending with newline.
+
+   procedure Indent_Wrap_Comment (Text : in String; Comment_Syntax : in 
String);
+   --  Put Text, prefixed by Comment_Syntax and two spaces, indented to
+   --  Indent, wrapped at Max_Line_Length, to Current_Output, ending with
+   --  newline.
+
+end WisiToken.Generate;
diff --git a/wisitoken-lexer-re2c.adb b/wisitoken-lexer-re2c.adb
index 10bbd16..d66088d 100644
--- a/wisitoken-lexer-re2c.adb
+++ b/wisitoken-lexer-re2c.adb
@@ -81,7 +81,7 @@ package body WisiToken.Lexer.re2c is
 
    overriding procedure Reset_With_String_Access
      (Lexer      : in out Instance;
-      Input      : access String;
+      Input      : in     Ada.Strings.Unbounded.String_Access;
       File_Name  : in     Ada.Strings.Unbounded.Unbounded_String;
       Begin_Char : in     Buffer_Pos       := Buffer_Pos'First;
       Begin_Line : in     Line_Number_Type := Line_Number_Type'First)
diff --git a/wisitoken-lexer-re2c.ads b/wisitoken-lexer-re2c.ads
index b58c4ea..b871e9c 100644
--- a/wisitoken-lexer-re2c.ads
+++ b/wisitoken-lexer-re2c.ads
@@ -92,7 +92,7 @@ package WisiToken.Lexer.re2c is
 
    overriding procedure Reset_With_String_Access
      (Lexer      : in out Instance;
-      Input      : access String;
+      Input      : in     Ada.Strings.Unbounded.String_Access;
       File_Name  : in     Ada.Strings.Unbounded.Unbounded_String;
       Begin_Char : in     Buffer_Pos       := Buffer_Pos'First;
       Begin_Line : in     Line_Number_Type := Line_Number_Type'First);
diff --git a/wisitoken-lexer-regexp.adb b/wisitoken-lexer-regexp.adb
index 1790b74..3e5a421 100644
--- a/wisitoken-lexer-regexp.adb
+++ b/wisitoken-lexer-regexp.adb
@@ -178,9 +178,9 @@ package body WisiToken.Lexer.Regexp is
    end Reset_With_String;
 
    overriding procedure Reset_With_String_Access
-     (Lexer     : in out Instance;
-      Input     : access String;
-      File_Name : in     Ada.Strings.Unbounded.Unbounded_String;
+     (Lexer      : in out Instance;
+      Input      : in     Ada.Strings.Unbounded.String_Access;
+      File_Name  : in     Ada.Strings.Unbounded.Unbounded_String;
       Begin_Char : in     Buffer_Pos       := Buffer_Pos'First;
       Begin_Line : in     Line_Number_Type := Line_Number_Type'First)
    is begin
diff --git a/wisitoken-lexer-regexp.ads b/wisitoken-lexer-regexp.ads
index 3a3d5f4..da55448 100644
--- a/wisitoken-lexer-regexp.ads
+++ b/wisitoken-lexer-regexp.ads
@@ -67,7 +67,7 @@ package WisiToken.Lexer.Regexp is
       Begin_Line : in     Line_Number_Type := Line_Number_Type'First);
    overriding procedure Reset_With_String_Access
      (Lexer      : in out Instance;
-      Input      : access String;
+      Input      : in     Ada.Strings.Unbounded.String_Access;
       File_Name  : in     Ada.Strings.Unbounded.Unbounded_String;
       Begin_Char : in     Buffer_Pos       := Buffer_Pos'First;
       Begin_Line : in     Line_Number_Type := Line_Number_Type'First);
diff --git a/wisitoken-lexer.ads b/wisitoken-lexer.ads
index 7be10ed..3de31c7 100644
--- a/wisitoken-lexer.ads
+++ b/wisitoken-lexer.ads
@@ -70,7 +70,7 @@ package WisiToken.Lexer is
 
    procedure Reset_With_String_Access
      (Lexer      : in out Instance;
-      Input      : access String;
+      Input      : in     Ada.Strings.Unbounded.String_Access;
       File_Name  : in     Ada.Strings.Unbounded.Unbounded_String;
       Begin_Char : in     Buffer_Pos       := Buffer_Pos'First;
       Begin_Line : in     Line_Number_Type := Line_Number_Type'First)
@@ -154,7 +154,7 @@ private
 
       case Label is
       when String_Label =>
-         Buffer      : access String;
+         Buffer      : Ada.Strings.Unbounded.String_Access;
          User_Buffer : Boolean := False;
          --  If User_Buffer is True, user provided buffer and will deallocate
          --  it. Otherwise we must deallocate it.
diff --git a/wisitoken-parse-lr-mckenzie_recover-base.adb 
b/wisitoken-parse-lr-mckenzie_recover-base.adb
index 17301b2..ae60955 100644
--- a/wisitoken-parse-lr-mckenzie_recover-base.adb
+++ b/wisitoken-parse-lr-mckenzie_recover-base.adb
@@ -1,433 +1,442 @@
---  Abstract :
---
---  Base utilities for McKenzie_Recover
---
---  Copyright (C) 2018 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with Ada.Task_Identification;
-with GNAT.Traceback.Symbolic;
-package body WisiToken.Parse.LR.McKenzie_Recover.Base is
-
-   function Get_Barrier
-     (Parsers                 : not null access Parser_Lists.List;
-      Parser_Status           : in              Parser_Status_Array;
-      Cost_Limit              : in              Natural;
-      Min_Success_Check_Count : in              Natural;
-      Check_Delta_Limit       : in              Natural;
-      Enqueue_Limit           : in              Natural)
-     return Boolean
-   is
-      use all type SAL.Base_Peek_Type;
-      Done_Count : SAL.Base_Peek_Type := 0;
-   begin
-      --  Return True if all parsers are done, or if any parser has a config
-      --  available to check.
-      for P_Status of Parser_Status loop
-         case P_Status.Recover_State is
-         when Active =>
-            if P_Status.Parser_State.Recover.Check_Count - Check_Delta_Limit 
>= Min_Success_Check_Count then
-               --  fail; another parser succeeded, this one taking too long.
-               Done_Count := Done_Count + 1;
-
-            elsif P_Status.Parser_State.Recover.Enqueue_Count >= Enqueue_Limit 
then
-               --  fail
-               Done_Count := Done_Count + 1;
-
-            elsif P_Status.Parser_State.Recover.Config_Heap.Count > 0 then
-               if P_Status.Parser_State.Recover.Config_Heap.Min_Key <= 
Cost_Limit then
-                  return True;
-               else
-                  if P_Status.Active_Workers = 0 then
-                     --  fail; remaining configs exceed cost limit
-                     Done_Count := Done_Count + 1;
-                  end if;
-               end if;
-
-            else
-               if P_Status.Active_Workers = 0 then
-                  --  fail; no configs left to check (rarely happens with real
-                  --  languages).
-                  Done_Count := Done_Count + 1;
-               end if;
-            end if;
-
-         when Ready =>
-            --  We don't check Enqueue_Limit here; there will only be a few 
more
-            --  to find all the same-cost solutions.
-
-            if P_Status.Parser_State.Recover.Config_Heap.Count > 0 and then
-              P_Status.Parser_State.Recover.Config_Heap.Min_Key <= 
P_Status.Parser_State.Recover.Results.Min_Key
-            then
-               --  Still more to check.
-               return True;
-
-            elsif P_Status.Active_Workers = 0 then
-               Done_Count := Done_Count + 1;
-            end if;
-
-         when Success | Fail =>
-            Done_Count := Done_Count + 1;
-         end case;
-      end loop;
-
-      return Done_Count = Parsers.Count;
-   end Get_Barrier;
-
-   protected body Supervisor is
-
-      procedure Initialize
-        (Parsers   : not null access Parser_Lists.List;
-         Terminals : not null access constant Base_Token_Arrays.Vector)
-      is
-         use all type SAL.Base_Peek_Type;
-         Index : SAL.Peek_Type := 1;
-      begin
-         Supervisor.Parsers      := Parsers;
-         Supervisor.Terminals    := Terminals;
-         All_Parsers_Done        := False;
-         Success_Counter         := 0;
-         Min_Success_Check_Count := Natural'Last;
-         Fatal_Called            := False;
-         Result                  := Recover_Status'First;
-         Error_ID                := Ada.Exceptions.Null_Id;
-
-         for I in Parsers.Iterate loop
-            if Parsers.Reference (I).Recover_Insert_Delete.Length > 0 then
-               --  Previous error recovery resume not finished; this is 
supposed to
-               --  be checked in Parser.
-               raise SAL.Programmer_Error;
-            end if;
-
-            Parser_Status (Index) :=
-              (Recover_State  => Active,
-               Parser_State   => Parser_Lists.Persistent_State_Ref (I),
-               Fail_Mode      => Success,
-               Active_Workers => 0);
-
-            declare
-               Data : McKenzie_Data renames Parsers.Reference (I).Recover;
-            begin
-               Data.Config_Heap.Clear;
-               Data.Results.Clear;
-               Data.Enqueue_Count := 0;
-               Data.Check_Count   := 0;
-               Data.Success       := False;
-            end;
-
-            Index := Index + 1;
-         end loop;
-      end Initialize;
-
-      entry Get
-        (Parser_Index : out SAL.Base_Peek_Type;
-         Config       : out Configuration;
-         Status       : out Config_Status)
-        when (Fatal_Called or All_Parsers_Done) or else
-          Get_Barrier (Parsers, Parser_Status, Cost_Limit, 
Min_Success_Check_Count, Check_Delta_Limit, Enqueue_Limit)
-      is
-         use all type SAL.Base_Peek_Type;
-         Done_Count     : SAL.Base_Peek_Type := 0;
-         Min_Cost       : Integer            := Integer'Last;
-         Min_Cost_Index : SAL.Base_Peek_Type;
-
-         procedure Set_Outputs (I : in SAL.Peek_Type)
-         is begin
-            Parser_Index := I;
-            Config       := Parser_Status 
(I).Parser_State.Recover.Config_Heap.Remove;
-            Status       := Valid;
-
-            Parser_Status (I).Parser_State.Recover.Check_Count :=
-              Parser_Status (I).Parser_State.Recover.Check_Count + 1;
-
-            Parser_Status (I).Active_Workers := Parser_Status 
(I).Active_Workers + 1;
-         end Set_Outputs;
-
-         procedure Set_All_Done
-         is begin
-            Parser_Index := SAL.Base_Peek_Type'First;
-            Config       := (others => <>);
-            Status       := All_Done;
-         end Set_All_Done;
-
-      begin
-         if Fatal_Called or All_Parsers_Done then
-            Set_All_Done;
-            return;
-         end if;
-
-         --  Same logic as in Get_Barrier, but different actions.
-         for I in Parser_Status'Range loop
-            declare
-               P_Status : Base.Parser_Status renames Parser_Status (I);
-            begin
-               case P_Status.Recover_State is
-               when Active =>
-                  if P_Status.Parser_State.Recover.Config_Heap.Count > 0 then
-                     if P_Status.Parser_State.Recover.Check_Count - 
Check_Delta_Limit >= Min_Success_Check_Count then
-                        if Trace_McKenzie > Outline then
-                           Put_Line (Trace.all, P_Status.Parser_State.Label, 
"fail; check delta (limit" &
-                                       Integer'Image (Min_Success_Check_Count 
+ Check_Delta_Limit) & ")");
-                        end if;
-                        P_Status.Recover_State := Fail;
-                        P_Status.Fail_Mode     := Fail_Check_Delta;
-
-                        Done_Count := Done_Count + 1;
-
-                     elsif P_Status.Parser_State.Recover.Enqueue_Count >= 
Enqueue_Limit then
-                        if Trace_McKenzie > Outline then
-                           Put_Line (Trace.all, P_Status.Parser_State.Label, 
"fail; enqueue limit (" &
-                                       Integer'Image (Enqueue_Limit) & ")");
-                        end if;
-                        P_Status.Recover_State := Fail;
-                        P_Status.Fail_Mode     := Fail_Enqueue_Limit;
-
-                        Done_Count := Done_Count + 1;
-
-                     elsif P_Status.Parser_State.Recover.Config_Heap.Min_Key 
<= Cost_Limit then
-                        if P_Status.Parser_State.Recover.Config_Heap.Min_Key < 
Min_Cost then
-                           Min_Cost       := 
P_Status.Parser_State.Recover.Config_Heap.Min_Key;
-                           Min_Cost_Index := I;
-                        end if;
-
-                     else
-                        if P_Status.Active_Workers = 0 then
-                           if Trace_McKenzie > Outline then
-                              Put_Line (Trace.all, 
P_Status.Parser_State.Label, "fail; cost");
-                           end if;
-                           P_Status.Recover_State := Fail;
-                           P_Status.Fail_Mode     := Fail_Cost;
-
-                           Done_Count := Done_Count + 1;
-                        end if;
-                     end if;
-                  else
-                     if P_Status.Active_Workers = 0 then
-                        --  No configs left to check (rarely happens with real 
languages).
-                        if Trace_McKenzie > Outline then
-                           Put_Line (Trace.all, P_Status.Parser_State.Label, 
"fail; no configs left");
-                        end if;
-                        P_Status.Recover_State := Fail;
-                        P_Status.Fail_Mode     := Fail_No_Configs_Left;
-
-                        Done_Count := Done_Count + 1;
-                     end if;
-                  end if;
-
-               when Ready =>
-                  if P_Status.Parser_State.Recover.Enqueue_Count >= 
Enqueue_Limit then
-                     if Trace_McKenzie > Outline then
-                        Put_Line (Trace.all, P_Status.Parser_State.Label, 
"fail; enqueue limit (" &
-                                    Integer'Image (Enqueue_Limit) & ")");
-                     end if;
-                     P_Status.Recover_State := Fail;
-                     P_Status.Fail_Mode     := Fail_Enqueue_Limit;
-
-                     Done_Count := Done_Count + 1;
-
-                  elsif P_Status.Parser_State.Recover.Config_Heap.Count > 0 
and then
-                    P_Status.Parser_State.Recover.Config_Heap.Min_Key <= 
P_Status.Parser_State.Recover.Results.Min_Key
-                  then
-                     --  Still more to check.
-                     Set_Outputs (I);
-                     return;
-
-                  elsif P_Status.Active_Workers = 0 then
-                     P_Status.Recover_State := Success;
-                     Done_Count             := Done_Count + 1;
-                  end if;
-
-               when Success | Fail =>
-                  Done_Count := Done_Count + 1;
-               end case;
-            end;
-         end loop;
-
-         if Min_Cost /= Integer'Last then
-            Set_Outputs (Min_Cost_Index);
-
-         elsif Done_Count = Parsers.Count then
-            if Trace_McKenzie > Extra then
-               Trace.Put_Line ("Supervisor: done, " & (if Success_Counter > 0 
then "succeed" else "fail"));
-            end if;
-
-            Set_All_Done;
-            All_Parsers_Done := True;
-         else
-            raise SAL.Programmer_Error with "Get_Barrier and Get logic do not 
match";
-         end if;
-      end Get;
-
-      procedure Success
-        (Parser_Index : in     SAL.Peek_Type;
-         Config       : in     Configuration;
-         Configs      : in out Config_Heaps.Heap_Type)
-      is
-         use all type SAL.Base_Peek_Type;
-         Data : McKenzie_Data renames Parser_Status 
(Parser_Index).Parser_State.Recover;
-      begin
-         Put (Parser_Index, Configs); --  Decrements Active_Worker_Count.
-
-         if Trace_McKenzie > Detail then
-            Put
-              ("succeed: enqueue" & Integer'Image (Data.Enqueue_Count) & ", 
check " & Integer'Image (Data.Check_Count),
-               Trace.all, Parser_Status (Parser_Index).Parser_State.Label, 
Terminals.all, Config);
-         end if;
-
-         if Force_Full_Explore then
-            return;
-         end if;
-
-         Success_Counter := Success_Counter + 1;
-         Result          := Success;
-
-         Data.Success := True;
-
-         if Data.Check_Count < Min_Success_Check_Count then
-            Min_Success_Check_Count := Data.Check_Count;
-         end if;
-
-         if Force_High_Cost_Solutions then
-            Data.Results.Add (Config);
-            if Data.Results.Count > 3 then
-               Parser_Status (Parser_Index).Recover_State := Ready;
-            end if;
-         else
-            if Data.Results.Count = 0 then
-               Data.Results.Add (Config);
-
-               Parser_Status (Parser_Index).Recover_State := Ready;
-
-            elsif Config.Cost < Data.Results.Min_Key then
-               --  delete higher cost configs from Results
-               loop
-                  Data.Results.Drop;
-                  exit when Data.Results.Count = 0 or else
-                    Config.Cost >= Data.Results.Min_Key;
-               end loop;
-
-               Data.Results.Add (Config);
-
-            elsif Config.Cost = Data.Results.Min_Key then
-               Data.Results.Add (Config);
-
-            else
-               --  Config.Cost > Results.Min_Key
-               null;
-            end if;
-         end if;
-      end Success;
-
-      procedure Put (Parser_Index : in SAL.Peek_Type; Configs : in out 
Config_Heaps.Heap_Type)
-      is
-         use all type SAL.Base_Peek_Type;
-         Configs_Count : constant SAL.Base_Peek_Type := Configs.Count; -- 
Before it is emptied, for Trace.
-
-         P_Status : Base.Parser_Status renames Parser_Status (Parser_Index);
-         Data : McKenzie_Data renames P_Status.Parser_State.Recover;
-      begin
-         P_Status.Active_Workers := P_Status.Active_Workers - 1;
-
-         loop
-            exit when Configs.Count = 0;
-
-            --  [1] has a check for duplicate configs here; that only happens 
with
-            --  higher costs, which take too long for our application.
-            Data.Config_Heap.Add (Configs.Remove);
-            Data.Enqueue_Count := Data.Enqueue_Count + 1;
-         end loop;
-
-         if Trace_McKenzie > Detail then
-            Put_Line
-              (Trace.all, P_Status.Parser_State.Label,
-               "enqueue:" & SAL.Base_Peek_Type'Image (Configs_Count) &
-                 "/" & SAL.Base_Peek_Type'Image (Data.Config_Heap.Count) &
-                 "/" & Trimmed_Image (Data.Enqueue_Count) &
-                 "/" & Trimmed_Image (Data.Check_Count) &
-                 ", min cost:" &
-                 (if Data.Config_Heap.Count > 0
-                  then Integer'Image (Data.Config_Heap.Min_Key)
-                  else " ? ") &
-                 ", active workers:" & Integer'Image 
(P_Status.Active_Workers));
-         end if;
-      end Put;
-
-      function Recover_Result return Recover_Status
-      is
-         Temp : Recover_Status := Result;
-      begin
-         if Result = Success then
-            return Success;
-         else
-            for S of Parser_Status loop
-               Temp := Recover_Status'Max (Result, S.Fail_Mode);
-            end loop;
-            return Temp;
-         end if;
-      end Recover_Result;
-
-      procedure Fatal (E : in Ada.Exceptions.Exception_Occurrence)
-      is
-         use Ada.Exceptions;
-         Task_ID : constant String := Ada.Task_Identification.Image 
(Ada.Task_Identification.Current_Task);
-      begin
-         if Trace_McKenzie > Outline then
-            Trace.Put_Line (Task_ID & " Supervisor: Error");
-         end if;
-         Fatal_Called   := True;
-         Error_ID       := Exception_Identity (E);
-         Error_Message  := +Exception_Message (E);
-         if Debug_Mode then
-            Trace.Put_Line (Exception_Name (E) & ": " & Exception_Message (E));
-            Trace.Put_Line (GNAT.Traceback.Symbolic.Symbolic_Traceback (E));
-         end if;
-      end Fatal;
-
-      entry Done (Error_ID : out Ada.Exceptions.Exception_Id; Message : out 
Ada.Strings.Unbounded.Unbounded_String)
-        when All_Parsers_Done or Fatal_Called
-      is begin
-         Error_ID := Supervisor.Error_ID;
-         Message  := Error_Message;
-         if Trace_McKenzie > Detail then
-            Trace.New_Line;
-            Trace.Put_Line ("Supervisor: Done");
-         end if;
-      end Done;
-
-      function Parser_State (Parser_Index : in SAL.Peek_Type) return 
Parser_Lists.Constant_Reference_Type
-      is begin
-         return (Element => Parser_Status (Parser_Index).Parser_State);
-      end Parser_State;
-
-      function Label (Parser_Index : in SAL.Peek_Type) return Natural
-      is begin
-         return Parser_Status (Parser_Index).Parser_State.Label;
-      end Label;
-
-   end Supervisor;
-
-   procedure Put
-     (Message      : in              String;
-      Super        : not null access Base.Supervisor;
-      Shared       : not null access Base.Shared;
-      Parser_Index : in              SAL.Peek_Type;
-      Config       : in              Configuration;
-      Task_ID      : in              Boolean := True)
-   is begin
-      Put (Message, Super.Trace.all, Super.Parser_State (Parser_Index).Label,
-           Shared.Terminals.all, Config, Task_ID);
-   end Put;
-
-end WisiToken.Parse.LR.McKenzie_Recover.Base;
+--  Abstract :
+--
+--  Base utilities for McKenzie_Recover
+--
+--  Copyright (C) 2018, 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with GNAT.Traceback.Symbolic;
+package body WisiToken.Parse.LR.McKenzie_Recover.Base is
+
+   function Get_Barrier
+     (Parsers                 : not null access Parser_Lists.List;
+      Parser_Status           : in              Parser_Status_Array;
+      Min_Success_Check_Count : in              Natural;
+      Check_Delta_Limit       : in              Natural;
+      Enqueue_Limit           : in              Natural)
+     return Boolean
+   is
+      use all type SAL.Base_Peek_Type;
+      Done_Count : SAL.Base_Peek_Type := 0;
+   begin
+      --  Return True if all parsers are done, or if any parser has a config
+      --  available to check.
+      for P_Status of Parser_Status loop
+         case P_Status.Recover_State is
+         when Active | Ready =>
+            if P_Status.Parser_State.Recover.Config_Heap.Count > 0 then
+               if P_Status.Parser_State.Recover.Check_Count - 
Check_Delta_Limit >= Min_Success_Check_Count then
+                  --  fail; another parser succeeded, this one taking too long.
+                  Done_Count := Done_Count + 1;
+
+               elsif P_Status.Parser_State.Recover.Enqueue_Count +
+                 P_Status.Parser_State.Recover.Config_Full_Count >= 
Enqueue_Limit
+               then
+                  --  fail
+                  Done_Count := Done_Count + 1;
+               end if;
+            end if;
+
+            case P_Status.Recover_State is
+            when Active =>
+               if P_Status.Parser_State.Recover.Config_Heap.Count > 0 then
+                  --  Still working
+                  return True;
+               else
+                  if P_Status.Active_Workers = 0 then
+                     --  fail; no configs left to check.
+                     Done_Count := Done_Count + 1;
+                  end if;
+               end if;
+
+            when Ready =>
+               if P_Status.Parser_State.Recover.Config_Heap.Count > 0 and then
+                 P_Status.Parser_State.Recover.Config_Heap.Min_Key <= 
P_Status.Parser_State.Recover.Results.Min_Key
+               then
+                  --  Still more to check.
+                  return True;
+
+               elsif P_Status.Active_Workers = 0 then
+                  Done_Count := Done_Count + 1;
+               end if;
+
+            when others =>
+               null;
+            end case;
+
+         when Success | Fail =>
+            Done_Count := Done_Count + 1;
+         end case;
+      end loop;
+
+      return Done_Count = Parsers.Count;
+   end Get_Barrier;
+
+   protected body Supervisor is
+
+      procedure Initialize
+        (Parsers   : not null access Parser_Lists.List;
+         Terminals : not null access constant Base_Token_Arrays.Vector)
+      is
+         use all type SAL.Base_Peek_Type;
+         Index : SAL.Peek_Type := 1;
+      begin
+         Supervisor.Parsers      := Parsers;
+         Supervisor.Terminals    := Terminals;
+         All_Parsers_Done        := False;
+         Success_Counter         := 0;
+         Min_Success_Check_Count := Natural'Last;
+         Fatal_Called            := False;
+         Result                  := Recover_Status'First;
+         Error_ID                := Ada.Exceptions.Null_Id;
+
+         for I in Parsers.Iterate loop
+            if Parsers.Reference (I).Recover_Insert_Delete.Length > 0 then
+               --  Previous error recovery resume not finished; this is 
supposed to
+               --  be checked in Parser.
+               raise SAL.Programmer_Error;
+            end if;
+
+            Parser_Status (Index) :=
+              (Recover_State  => Active,
+               Parser_State   => Parser_Lists.Persistent_State_Ref (I),
+               Fail_Mode      => Success,
+               Active_Workers => 0);
+
+            declare
+               Data : McKenzie_Data renames Parsers.Reference (I).Recover;
+            begin
+               Data.Config_Heap.Clear;
+               Data.Results.Clear;
+               Data.Enqueue_Count := 0;
+               Data.Check_Count   := 0;
+               Data.Success       := False;
+            end;
+
+            Index := Index + 1;
+         end loop;
+      end Initialize;
+
+      entry Get
+        (Parser_Index : out SAL.Base_Peek_Type;
+         Config       : out Configuration;
+         Status       : out Config_Status)
+        when (Fatal_Called or All_Parsers_Done) or else
+          Get_Barrier (Parsers, Parser_Status, Min_Success_Check_Count, 
Check_Delta_Limit, Enqueue_Limit)
+      is
+         use all type SAL.Base_Peek_Type;
+         Done_Count     : SAL.Base_Peek_Type := 0;
+         Min_Cost       : Integer            := Integer'Last;
+         Min_Cost_Index : SAL.Base_Peek_Type;
+
+         procedure Set_Outputs (I : in SAL.Peek_Type)
+         is begin
+            Parser_Index := I;
+            Config       := Parser_Status 
(I).Parser_State.Recover.Config_Heap.Remove;
+            Status       := Valid;
+
+            Parser_Status (I).Parser_State.Recover.Check_Count :=
+              Parser_Status (I).Parser_State.Recover.Check_Count + 1;
+
+            Parser_Status (I).Active_Workers := Parser_Status 
(I).Active_Workers + 1;
+         end Set_Outputs;
+
+         procedure Set_All_Done
+         is begin
+            Parser_Index := SAL.Base_Peek_Type'First;
+            Config       := (others => <>);
+            Status       := All_Done;
+         end Set_All_Done;
+
+      begin
+         if Fatal_Called or All_Parsers_Done then
+            Set_All_Done;
+            return;
+         end if;
+
+         --  Same logic as in Get_Barrier, but different actions.
+         --
+         --  No task_id in outline trace messages, because they may appear in
+         --  .parse_good
+         for I in Parser_Status'Range loop
+            declare
+               P_Status : Base.Parser_Status renames Parser_Status (I);
+            begin
+               case P_Status.Recover_State is
+               when Active | Ready =>
+                  if P_Status.Parser_State.Recover.Config_Heap.Count > 0 then
+                     if P_Status.Parser_State.Recover.Check_Count - 
Check_Delta_Limit >= Min_Success_Check_Count then
+                        if Trace_McKenzie > Outline then
+                           Put_Line
+                             (Trace.all,
+                              P_Status.Parser_State.Label, "fail; check delta 
(limit" &
+                                Integer'Image (Min_Success_Check_Count + 
Check_Delta_Limit) & ")",
+                              Task_ID => False);
+                        end if;
+                        P_Status.Recover_State := Fail;
+                        P_Status.Fail_Mode     := Fail_Check_Delta;
+
+                        Done_Count := Done_Count + 1;
+
+                     elsif P_Status.Parser_State.Recover.Enqueue_Count +
+                       P_Status.Parser_State.Recover.Config_Full_Count >= 
Enqueue_Limit
+                     then
+                        if Trace_McKenzie > Outline then
+                           Put_Line
+                             (Trace.all,
+                              P_Status.Parser_State.Label, "fail; enqueue 
limit (" &
+                                Enqueue_Limit'Image & " cost" &
+                                
P_Status.Parser_State.Recover.Config_Heap.Min_Key'Image & ")",
+                              Task_ID => False);
+                        end if;
+                        P_Status.Recover_State := Fail;
+                        P_Status.Fail_Mode     := Fail_Enqueue_Limit;
+
+                        Done_Count := Done_Count + 1;
+                     end if;
+                  end if;
+
+                  case P_Status.Recover_State is
+                  when Active =>
+                     if P_Status.Parser_State.Recover.Config_Heap.Count > 0 
then
+                        if P_Status.Parser_State.Recover.Config_Heap.Min_Key < 
Min_Cost then
+                           Min_Cost       := 
P_Status.Parser_State.Recover.Config_Heap.Min_Key;
+                           Min_Cost_Index := I;
+                           --  not done
+                        end if;
+                     else
+                        if P_Status.Active_Workers = 0 then
+                           --  No configs left to check (rarely happens with 
real languages).
+                           if Trace_McKenzie > Outline then
+                              Put_Line
+                                (Trace.all, P_Status.Parser_State.Label, 
"fail; no configs left", Task_ID => False);
+                           end if;
+                           P_Status.Recover_State := Fail;
+                           P_Status.Fail_Mode     := Fail_No_Configs_Left;
+
+                           Done_Count := Done_Count + 1;
+                        end if;
+                     end if;
+
+                  when Ready =>
+                     if P_Status.Parser_State.Recover.Config_Heap.Count > 0 
and then
+                       P_Status.Parser_State.Recover.Config_Heap.Min_Key <=
+                       P_Status.Parser_State.Recover.Results.Min_Key
+                     then
+                        --  Still more to check. We don't check Min_Cost here 
so this parser
+                        --  can finish quickly.
+                        Set_Outputs (I);
+                        return;
+
+                     elsif P_Status.Active_Workers = 0 then
+                        P_Status.Recover_State := Success;
+                        Done_Count             := Done_Count + 1;
+                     end if;
+                  when others =>
+                     null;
+                  end case;
+
+               when Success | Fail =>
+                  Done_Count := Done_Count + 1;
+               end case;
+            end;
+         end loop;
+
+         if Min_Cost /= Integer'Last then
+            Set_Outputs (Min_Cost_Index);
+
+         elsif Done_Count = Parsers.Count then
+            if Trace_McKenzie > Extra then
+               Trace.Put_Line ("Supervisor: done, " & (if Success_Counter > 0 
then "succeed" else "fail"));
+            end if;
+
+            Set_All_Done;
+            All_Parsers_Done := True;
+         else
+            raise SAL.Programmer_Error with "Get_Barrier and Get logic do not 
match";
+         end if;
+      end Get;
+
+      procedure Success
+        (Parser_Index : in     SAL.Peek_Type;
+         Config       : in     Configuration;
+         Configs      : in out Config_Heaps.Heap_Type)
+      is
+         use all type SAL.Base_Peek_Type;
+         Data : McKenzie_Data renames Parser_Status 
(Parser_Index).Parser_State.Recover;
+      begin
+         Put (Parser_Index, Configs); --  Decrements Active_Worker_Count.
+
+         if Trace_McKenzie > Detail then
+            Put
+              ("succeed: enqueue" & Integer'Image (Data.Enqueue_Count) & ", 
check " & Integer'Image (Data.Check_Count),
+               Trace.all, Parser_Status (Parser_Index).Parser_State.Label, 
Terminals.all, Config);
+         end if;
+
+         if Force_Full_Explore then
+            return;
+         end if;
+
+         Success_Counter := Success_Counter + 1;
+         Result          := Success;
+
+         Data.Success := True;
+
+         if Data.Check_Count < Min_Success_Check_Count then
+            Min_Success_Check_Count := Data.Check_Count;
+         end if;
+
+         if Force_High_Cost_Solutions then
+            Data.Results.Add (Config);
+            if Data.Results.Count > 3 then
+               Parser_Status (Parser_Index).Recover_State := Ready;
+            end if;
+         else
+            if Data.Results.Count = 0 then
+               Data.Results.Add (Config);
+
+               Parser_Status (Parser_Index).Recover_State := Ready;
+
+            elsif Config.Cost < Data.Results.Min_Key then
+               --  delete higher cost configs from Results
+               loop
+                  Data.Results.Drop;
+                  exit when Data.Results.Count = 0 or else
+                    Config.Cost >= Data.Results.Min_Key;
+               end loop;
+
+               Data.Results.Add (Config);
+
+            elsif Config.Cost = Data.Results.Min_Key then
+               Data.Results.Add (Config);
+
+            else
+               --  Config.Cost > Results.Min_Key
+               null;
+            end if;
+         end if;
+      end Success;
+
+      procedure Put (Parser_Index : in SAL.Peek_Type; Configs : in out 
Config_Heaps.Heap_Type)
+      is
+         use all type SAL.Base_Peek_Type;
+         Configs_Count : constant SAL.Base_Peek_Type := Configs.Count; -- 
Before it is emptied, for Trace.
+
+         P_Status : Base.Parser_Status renames Parser_Status (Parser_Index);
+         Data : McKenzie_Data renames P_Status.Parser_State.Recover;
+      begin
+         P_Status.Active_Workers := P_Status.Active_Workers - 1;
+
+         loop
+            exit when Configs.Count = 0;
+
+            --  [1] has a check for duplicate configs here; that only happens 
with
+            --  higher costs, which take too long for our application.
+            Data.Config_Heap.Add (Configs.Remove);
+            Data.Enqueue_Count := Data.Enqueue_Count + 1;
+         end loop;
+
+         if Trace_McKenzie > Detail then
+            Put_Line
+              (Trace.all, P_Status.Parser_State.Label,
+               "enqueue:" & SAL.Base_Peek_Type'Image (Configs_Count) &
+                 "/" & SAL.Base_Peek_Type'Image (Data.Config_Heap.Count) &
+                 "/" & Trimmed_Image (Data.Enqueue_Count) &
+                 "/" & Trimmed_Image (Data.Check_Count) &
+                 ", min cost:" &
+                 (if Data.Config_Heap.Count > 0
+                  then Integer'Image (Data.Config_Heap.Min_Key)
+                  else " ? ") &
+                 ", active workers:" & Integer'Image 
(P_Status.Active_Workers));
+         end if;
+      end Put;
+
+      procedure Config_Full (Parser_Index : in SAL.Peek_Type)
+      is
+         P_Status : Base.Parser_Status renames Parser_Status (Parser_Index);
+         Data : McKenzie_Data renames P_Status.Parser_State.Recover;
+      begin
+         Data.Config_Full_Count := Data.Config_Full_Count + 1;
+         if Trace_McKenzie > Outline then
+            Put_Line (Trace.all, Label (Parser_Index), "config.ops is full; " 
& Data.Config_Full_Count'Image);
+         end if;
+      end Config_Full;
+
+      function Recover_Result return Recover_Status
+      is
+         Temp : Recover_Status := Result;
+      begin
+         if Result = Success then
+            return Success;
+         else
+            for S of Parser_Status loop
+               Temp := Recover_Status'Max (Result, S.Fail_Mode);
+            end loop;
+            return Temp;
+         end if;
+      end Recover_Result;
+
+      procedure Fatal (E : in Ada.Exceptions.Exception_Occurrence)
+      is
+         use Ada.Exceptions;
+      begin
+         if Trace_McKenzie > Outline then
+            Trace.Put_Line ("task " & Task_Attributes.Value'Image & " 
Supervisor: Error");
+         end if;
+         Fatal_Called   := True;
+         Error_ID       := Exception_Identity (E);
+         Error_Message  := +Exception_Message (E);
+         if Debug_Mode then
+            Trace.Put_Line (Exception_Name (E) & ": " & Exception_Message (E));
+            Trace.Put_Line (GNAT.Traceback.Symbolic.Symbolic_Traceback (E));
+         end if;
+      end Fatal;
+
+      entry Done (Error_ID : out Ada.Exceptions.Exception_Id; Message : out 
Ada.Strings.Unbounded.Unbounded_String)
+        when All_Parsers_Done or Fatal_Called
+      is begin
+         Error_ID := Supervisor.Error_ID;
+         Message  := Error_Message;
+         if Trace_McKenzie > Detail then
+            Trace.New_Line;
+            Trace.Put_Line ("Supervisor: Done");
+         end if;
+      end Done;
+
+      function Parser_State (Parser_Index : in SAL.Peek_Type) return 
Parser_Lists.Constant_Reference_Type
+      is begin
+         return (Element => Parser_Status (Parser_Index).Parser_State);
+      end Parser_State;
+
+      function Label (Parser_Index : in SAL.Peek_Type) return Natural
+      is begin
+         return Parser_Status (Parser_Index).Parser_State.Label;
+      end Label;
+
+   end Supervisor;
+
+   procedure Put
+     (Message      : in              String;
+      Super        : not null access Base.Supervisor;
+      Shared       : not null access Base.Shared;
+      Parser_Index : in              SAL.Peek_Type;
+      Config       : in              Configuration;
+      Task_ID      : in              Boolean := True)
+   is begin
+      Put (Message, Super.Trace.all, Super.Parser_State (Parser_Index).Label,
+           Shared.Terminals.all, Config, Task_ID);
+   end Put;
+
+end WisiToken.Parse.LR.McKenzie_Recover.Base;
diff --git a/wisitoken-parse-lr-mckenzie_recover-base.ads 
b/wisitoken-parse-lr-mckenzie_recover-base.ads
index c5d78ea..cde4bd5 100644
--- a/wisitoken-parse-lr-mckenzie_recover-base.ads
+++ b/wisitoken-parse-lr-mckenzie_recover-base.ads
@@ -1,181 +1,184 @@
---  Abstract :
---
---  Base utilities for McKenzie_Recover
---
---  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with Ada.Exceptions;
-with WisiToken.Parse.LR.Parser;
-with WisiToken.Parse.LR.Parser_Lists;
-private package WisiToken.Parse.LR.McKenzie_Recover.Base is
-
-   ----------
-   --  Protected object specs.
-   --
-   --  Tasking design requirements:
-   --
-   --  1) For each parse_state, find all solutions of the same lowest
-   --  cost.
-   --
-   --  2) use as many CPUs as available as fully as possible.
-   --
-   --  3) avoid
-   --     a) busy waits
-   --     b) race conditions
-   --     c) deadlocks.
-   --
-   --  For 2), we use worker_tasks to perform the check computations on
-   --  each configuration. We allocate N - 1 worker_tasks, where N is the
-   --  number of available CPUs, saving one CPU for Supervisor and the
-   --  foreground IDE.
-   --
-   --  For 1), worker_tasks always get the lowest cost configuration
-   --  available. However, some active worker_task may have a lower cost
-   --  configuration that it has not yet delivered to Supervisor.
-   --  Therefore we always wait until all current active worker_tasks
-   --  deliver their results before deciding we are done.
-   --
-   --  For 3a) we have one Supervisor protected object that controls
-   --  access to all Parse_States and configurations, and a Shared object
-   --  that provides appropriate access to the Shared_Parser components.
-   --
-   --  It is tempting to try to reduce contention for Supervisor by
-   --  having one protected object per parser, but that requires the
-   --  worker tasks to busy loop checking all the parsers.
-   --
-   --  There is still a race condition on Success; the solutions can be
-   --  delivered in different orders on different runs. This matters
-   --  because each solution results in a successful parse, possibly with
-   --  different actions (different indentation computed, for example).
-   --  Which solution finally succeeds depends on which are terminated
-   --  due to identical parser stacks, which in turn depends on the order
-   --  they were delivered. See ada-mode/tests/ada_mode-interactive_2.adb
-   --  for an example.
-   --
-   --  There is also a race condition on how many failed or higher cost
-   --  configurations are checked, before the final solutions are found.
-
-   type Config_Status is (Valid, All_Done);
-   type Recover_State is (Active, Ready, Success, Fail);
-
-   type Parser_Status is record
-      Recover_State : Base.Recover_State;
-      Parser_State  : Parser_Lists.State_Access;
-      Fail_Mode     : Recover_Status;
-
-      Active_Workers : Natural;
-      --  Count of Worker_Tasks that have done Get but not Put or Success.
-   end record;
-
-   type Parser_Status_Array is array (SAL.Peek_Type range <>) of Parser_Status;
-
-   protected type Supervisor
-     (Trace             : not null access WisiToken.Trace'Class;
-      Cost_Limit        : Natural;
-      Check_Delta_Limit : Natural;
-      Enqueue_Limit     : Natural;
-      Parser_Count      : SAL.Peek_Type)
-   is
-      --  There is only one object of this type, declared in Recover.
-
-      procedure Initialize
-        (Parsers   : not null access Parser_Lists.List;
-         Terminals : not null access constant Base_Token_Arrays.Vector);
-
-      entry Get
-        (Parser_Index : out SAL.Base_Peek_Type;
-         Config       : out Configuration;
-         Status       : out Config_Status);
-      --  Get a new configuration to check. Available when there is a
-      --  configuration to get, or when all configs have been checked.
-      --
-      --  Increments active worker count.
-      --
-      --  Status values mean:
-      --
-      --  Valid - Parser_Index, Config are valid, should be checked.
-      --
-      --  All_Done - Parser_Index, Config are not valid.
-
-      procedure Success
-        (Parser_Index : in     SAL.Peek_Type;
-         Config       : in     Configuration;
-         Configs      : in out Config_Heaps.Heap_Type);
-      --  Report that Configuration succeeds for Parser_Label, and enqueue
-      --  Configs.
-      --
-      --  Decrements active worker count.
-
-      procedure Put (Parser_Index : in SAL.Peek_Type; Configs : in out 
Config_Heaps.Heap_Type);
-      --  Add Configs to the McKenzie_Data Config_Heap for Parser_Label
-      --
-      --  Decrements active worker count.
-
-      function Recover_Result return Recover_Status;
-
-      procedure Fatal (E : in Ada.Exceptions.Exception_Occurrence);
-      --  Report a fatal error; abort all processing, make Done
-      --  available.
-
-      entry Done (Error_ID : out Ada.Exceptions.Exception_Id; Message : out 
Ada.Strings.Unbounded.Unbounded_String);
-      --  Available when all parsers have failed or succeeded, or an error
-      --  occured.
-      --
-      --  If Error_ID is not Null_Id, an error occured.
-
-      function Parser_State (Parser_Index : in SAL.Peek_Type) return 
Parser_Lists.Constant_Reference_Type;
-      function Label (Parser_Index : in SAL.Peek_Type) return Natural;
-
-   private
-      Parsers   : access Parser_Lists.List;
-      Terminals : access constant Base_Token_Arrays.Vector;
-
-      All_Parsers_Done        : Boolean;
-      Success_Counter         : Natural;
-      Min_Success_Check_Count : Natural;
-      Fatal_Called            : Boolean;
-      Result                  : Recover_Status;
-      Error_ID                : Ada.Exceptions.Exception_Id;
-      Error_Message           : Ada.Strings.Unbounded.Unbounded_String;
-      Parser_Status           : Parser_Status_Array (1 .. Parser_Count);
-   end Supervisor;
-
-   type Shared
-     (Trace                                 : not null access 
WisiToken.Trace'Class;
-      Lexer                                 : not null access constant 
WisiToken.Lexer.Instance'Class;
-      Table                                 : not null access constant 
Parse_Table;
-      Language_Fixes                        : 
WisiToken.Parse.LR.Parser.Language_Fixes_Access;
-      Language_Use_Minimal_Complete_Actions : 
WisiToken.Parse.LR.Parser.Language_Use_Minimal_Complete_Actions_Access;
-      Language_String_ID_Set                : 
WisiToken.Parse.LR.Parser.Language_String_ID_Set_Access;
-      Terminals                             : not null access constant 
Base_Token_Arrays.Vector;
-      Line_Begin_Token                      : not null access constant 
Line_Begin_Token_Vectors.Vector)
-     is null record;
-   --  There is only one object of this type, declared in Recover. It
-   --  provides appropriate access to Shared_Parser components.
-   --
-   --  Since all the accessible objects are read-only (except Trace),
-   --  there are no protected operations, and this is not a protected
-   --  type.
-
-   procedure Put
-     (Message      : in              String;
-      Super        : not null access Base.Supervisor;
-      Shared       : not null access Base.Shared;
-      Parser_Index : in              SAL.Peek_Type;
-      Config       : in              Configuration;
-      Task_ID      : in              Boolean := True);
-
-end WisiToken.Parse.LR.McKenzie_Recover.Base;
+--  Abstract :
+--
+--  Base utilities for McKenzie_Recover
+--
+--  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Exceptions;
+with WisiToken.Parse.LR.Parser;
+with WisiToken.Parse.LR.Parser_Lists;
+private package WisiToken.Parse.LR.McKenzie_Recover.Base is
+
+   ----------
+   --  Protected object specs.
+   --
+   --  Tasking design requirements:
+   --
+   --  1) For each parse_state, find all solutions of the same lowest
+   --  cost.
+   --
+   --  2) use as many CPUs as available as fully as possible.
+   --
+   --  3) avoid
+   --     a) busy waits
+   --     b) race conditions
+   --     c) deadlocks.
+   --
+   --  For 2), we use worker_tasks to perform the check computations on
+   --  each configuration. We allocate N - 1 worker_tasks, where N is the
+   --  number of available CPUs, saving one CPU for Supervisor and the
+   --  foreground IDE.
+   --
+   --  For 1), worker_tasks always get the lowest cost configuration
+   --  available. However, some active worker_task may have a lower cost
+   --  configuration that it has not yet delivered to Supervisor.
+   --  Therefore we always wait until all current active worker_tasks
+   --  deliver their results before deciding we are done.
+   --
+   --  For 3a) we have one Supervisor protected object that controls
+   --  access to all Parse_States and configurations, and a Shared object
+   --  that provides appropriate access to the Shared_Parser components.
+   --
+   --  It is tempting to try to reduce contention for Supervisor by
+   --  having one protected object per parser, but that requires the
+   --  worker tasks to busy loop checking all the parsers.
+   --
+   --  There is still a race condition on Success; the solutions can be
+   --  delivered in different orders on different runs. This matters
+   --  because each solution results in a successful parse, possibly with
+   --  different actions (different indentation computed, for example).
+   --  Which solution finally succeeds depends on which are terminated
+   --  due to identical parser stacks, which in turn depends on the order
+   --  they were delivered. See ada-mode/tests/ada_mode-interactive_2.adb
+   --  for an example.
+   --
+   --  There is also a race condition on how many failed or higher cost
+   --  configurations are checked, before the final solutions are found.
+
+   type Config_Status is (Valid, All_Done);
+   type Recover_State is (Active, Ready, Success, Fail);
+
+   type Parser_Status is record
+      Recover_State : Base.Recover_State;
+      Parser_State  : Parser_Lists.State_Access;
+      Fail_Mode     : Recover_Status;
+
+      Active_Workers : Natural;
+      --  Count of Worker_Tasks that have done Get but not Put or Success.
+   end record;
+
+   type Parser_Status_Array is array (SAL.Peek_Type range <>) of Parser_Status;
+
+   protected type Supervisor
+     (Trace             : not null access WisiToken.Trace'Class;
+      Check_Delta_Limit : Natural;
+      Enqueue_Limit     : Natural;
+      Parser_Count      : SAL.Peek_Type)
+   is
+      --  There is only one object of this type, declared in Recover.
+
+      procedure Initialize
+        (Parsers   : not null access Parser_Lists.List;
+         Terminals : not null access constant Base_Token_Arrays.Vector);
+
+      entry Get
+        (Parser_Index : out SAL.Base_Peek_Type;
+         Config       : out Configuration;
+         Status       : out Config_Status);
+      --  Get a new configuration to check. Available when there is a
+      --  configuration to get, or when all configs have been checked.
+      --
+      --  Increments active worker count.
+      --
+      --  Status values mean:
+      --
+      --  Valid - Parser_Index, Config are valid, should be checked.
+      --
+      --  All_Done - Parser_Index, Config are not valid.
+
+      procedure Success
+        (Parser_Index : in     SAL.Peek_Type;
+         Config       : in     Configuration;
+         Configs      : in out Config_Heaps.Heap_Type);
+      --  Report that Configuration succeeds for Parser_Label, and enqueue
+      --  Configs.
+      --
+      --  Decrements active worker count.
+
+      procedure Put (Parser_Index : in SAL.Peek_Type; Configs : in out 
Config_Heaps.Heap_Type);
+      --  Add Configs to the McKenzie_Data Config_Heap for Parser_Label
+      --
+      --  Decrements active worker count.
+
+      procedure Config_Full (Parser_Index : in SAL.Peek_Type);
+      --  Report that a config.ops was full when trying to add another op.
+      --  This is counted towards the enqueue limit.
+
+      function Recover_Result return Recover_Status;
+
+      procedure Fatal (E : in Ada.Exceptions.Exception_Occurrence);
+      --  Report a fatal error; abort all processing, make Done
+      --  available.
+
+      entry Done (Error_ID : out Ada.Exceptions.Exception_Id; Message : out 
Ada.Strings.Unbounded.Unbounded_String);
+      --  Available when all parsers have failed or succeeded, or an error
+      --  occured.
+      --
+      --  If Error_ID is not Null_Id, an error occured.
+
+      function Parser_State (Parser_Index : in SAL.Peek_Type) return 
Parser_Lists.Constant_Reference_Type;
+      function Label (Parser_Index : in SAL.Peek_Type) return Natural;
+
+   private
+      Parsers   : access Parser_Lists.List;
+      Terminals : access constant Base_Token_Arrays.Vector;
+
+      All_Parsers_Done        : Boolean;
+      Success_Counter         : Natural;
+      Min_Success_Check_Count : Natural;
+      Fatal_Called            : Boolean;
+      Result                  : Recover_Status;
+      Error_ID                : Ada.Exceptions.Exception_Id;
+      Error_Message           : Ada.Strings.Unbounded.Unbounded_String;
+      Parser_Status           : Parser_Status_Array (1 .. Parser_Count);
+   end Supervisor;
+
+   type Shared
+     (Trace                          : not null access WisiToken.Trace'Class;
+      Lexer                          : not null access constant 
WisiToken.Lexer.Instance'Class;
+      Table                          : not null access constant Parse_Table;
+      Language_Fixes                 : 
WisiToken.Parse.LR.Parser.Language_Fixes_Access;
+      Language_Matching_Begin_Tokens : 
WisiToken.Parse.LR.Parser.Language_Matching_Begin_Tokens_Access;
+      Language_String_ID_Set         : 
WisiToken.Parse.LR.Parser.Language_String_ID_Set_Access;
+      Terminals                      : not null access constant 
Base_Token_Arrays.Vector;
+      Line_Begin_Token               : not null access constant 
Line_Begin_Token_Vectors.Vector)
+     is null record;
+   --  There is only one object of this type, declared in Recover. It
+   --  provides appropriate access to Shared_Parser components.
+   --
+   --  Since all the accessible objects are read-only (except Trace),
+   --  there are no protected operations, and this is not a protected
+   --  type.
+
+   procedure Put
+     (Message      : in              String;
+      Super        : not null access Base.Supervisor;
+      Shared       : not null access Base.Shared;
+      Parser_Index : in              SAL.Peek_Type;
+      Config       : in              Configuration;
+      Task_ID      : in              Boolean := True);
+
+end WisiToken.Parse.LR.McKenzie_Recover.Base;
diff --git a/wisitoken-parse-lr-mckenzie_recover-explore.adb 
b/wisitoken-parse-lr-mckenzie_recover-explore.adb
index 56bd3fe..272eadf 100644
--- a/wisitoken-parse-lr-mckenzie_recover-explore.adb
+++ b/wisitoken-parse-lr-mckenzie_recover-explore.adb
@@ -1,1406 +1,1668 @@
---  Abstract :
---
---  See spec.
---
---  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with WisiToken.Parse.LR.McKenzie_Recover.Parse;
-with WisiToken.Parse.LR.Parser;
-package body WisiToken.Parse.LR.McKenzie_Recover.Explore is
-
-   procedure Do_Shift
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Peek_Type;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
-      Config            : in out          Configuration;
-      State             : in              State_Index;
-      ID                : in              Token_ID;
-      Cost_Delta        : in              Integer;
-      Strategy          : in              Strategies)
-   is
-      use all type SAL.Base_Peek_Type;
-      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
-
-      Op : constant Config_Op := (Insert, ID, Config.Current_Shared_Token);
-   begin
-      Config.Strategy_Counts (Strategy) := Config.Strategy_Counts (Strategy) + 
1;
-
-      begin
-         if Config.Current_Ops = No_Insert_Delete then
-            Config.Ops.Append (Op);
-         else
-            Config.Ops.Insert (Op, Before => Config.Current_Ops);
-            Config.Current_Ops := Config.Current_Ops + 1;
-         end if;
-      exception
-      when SAL.Container_Full =>
-         if Trace_McKenzie > Outline then
-            Put_Line (Super.Trace.all, Super.Label (Parser_Index), "config.ops 
is full");
-         end if;
-         raise Bad_Config;
-      end;
-
-      if Cost_Delta = 0 then
-         Config.Cost := Config.Cost + McKenzie_Param.Insert (ID);
-      else
-         --  Cost_Delta /= 0 comes from Try_Insert_Terminal when
-         --  Minimal_Complete_Actions is useful. That doesn't mean it is better
-         --  than any other solution, so don't let cost be 0.
-         Config.Cost := Integer'Max (1, Config.Cost + McKenzie_Param.Insert 
(ID) + Cost_Delta);
-      end if;
-
-      Config.Error_Token.ID := Invalid_Token_ID;
-      Config.Check_Status   := (Label => WisiToken.Semantic_Checks.Ok);
-
-      Config.Stack.Push ((State, Syntax_Trees.Invalid_Node_Index, (ID, Virtual 
=> True, others => <>)));
-      if Trace_McKenzie > Detail then
-         Base.Put ("insert " & Image (ID, Super.Trace.Descriptor.all), Super, 
Shared, Parser_Index, Config);
-      end if;
-
-      Local_Config_Heap.Add (Config);
-   end Do_Shift;
-
-   procedure Do_Reduce_1
-     (Label             : in              String;
-      Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Peek_Type;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
-      Config            : in out          Configuration;
-      Action            : in              Reduce_Action_Rec;
-      Do_Language_Fixes : in              Boolean := True)
-   is
-      use all type SAL.Base_Peek_Type;
-      use all type Semantic_Checks.Check_Status_Label;
-      use all type WisiToken.Parse.LR.Parser.Language_Fixes_Access;
-
-      Prev_State : constant Unknown_State_Index := Config.Stack.Peek.State;
-
-      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
-      Table      : Parse_Table renames Shared.Table.all;
-      Nonterm    : Recover_Token;
-      New_State  : Unknown_State_Index;
-   begin
-      Config.Check_Status := Parse.Reduce_Stack (Shared, Config.Stack, Action, 
Nonterm, Default_Virtual => True);
-      case Config.Check_Status.Label is
-      when Ok =>
-         null;
-
-      when Semantic_Checks.Error =>
-         Config.Error_Token       := Nonterm;
-         Config.Check_Token_Count := Action.Token_Count;
-
-         if Do_Language_Fixes then
-            if Shared.Language_Fixes /= null then
-               Shared.Language_Fixes
-                 (Super.Trace.all, Shared.Lexer, Super.Label (Parser_Index), 
Shared.Table.all, Shared.Terminals.all,
-                  Super.Parser_State (Parser_Index).Tree, Local_Config_Heap,
-                  Config);
-            end if;
-         end if;
-
-         --  Finish the reduce; ignore the check fail.
-         Config.Stack.Pop (SAL.Base_Peek_Type (Config.Check_Token_Count));
-         Config.Error_Token.ID := Invalid_Token_ID;
-         Config.Check_Status   := (Label => Ok);
-      end case;
-
-      if Config.Stack.Depth = 0 or else Config.Stack (1).State = Unknown_State 
then
-         raise Bad_Config;
-      end if;
-
-      New_State := Goto_For (Table, Config.Stack (1).State, 
Action.Production.LHS);
-
-      if New_State = Unknown_State then
-         raise Bad_Config;
-      end if;
-
-      Config.Stack.Push ((New_State, Syntax_Trees.Invalid_Node_Index, 
Nonterm));
-
-      if Trace_McKenzie > Extra and Label'Length > 0 then
-         Put_Line
-           (Super.Trace.all, Super.Label (Parser_Index), Label &
-              ": state" & State_Index'Image (Prev_State) & " reduce to " &
-              Image (Action.Production.LHS, Descriptor) & ", goto" &
-              State_Index'Image (New_State));
-      end if;
-   end Do_Reduce_1;
-
-   procedure Do_Reduce_2
-     (Label             : in              String;
-      Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Peek_Type;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
-      Config            : in out          Configuration;
-      Inserted_ID       : in              Token_ID;
-      Cost_Delta        : in              Integer;
-      Strategy          : in              Strategies)
-   is
-      --  Perform reduce actions until shift Inserted_ID; if all succeed,
-      --  add the final configuration to the heap. If a conflict is
-      --  encountered, process the other action the same way. If a semantic
-      --  check fails, enqueue possible solutions. For parse table error
-      --  actions, or exception Bad_Config, just return.
-
-      Table       : Parse_Table renames Shared.Table.all;
-      Next_Action : constant Parse_Action_Node_Ptr := Action_For (Table, 
Config.Stack (1).State, Inserted_ID);
-   begin
-      if Next_Action.Next /= null then
-         --  There is a conflict; create a new config to shift or reduce.
-         declare
-            New_Config : Configuration := Config;
-            Action     : Parse_Action_Rec renames Next_Action.Next.Item;
-         begin
-            case Action.Verb is
-            when Shift =>
-               Do_Shift
-                 (Super, Shared, Parser_Index, Local_Config_Heap, New_Config, 
Action.State, Inserted_ID,
-                  Cost_Delta, Strategy);
-
-            when Reduce =>
-               Do_Reduce_1 (Label, Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Action);
-               Do_Reduce_2 (Label, Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Inserted_ID,
-                            Cost_Delta, Strategy);
-
-            when Accept_It =>
-               raise SAL.Programmer_Error with "found test case for Do_Reduce 
Accept_It conflict";
-
-            when Error =>
-               null;
-            end case;
-         end;
-
-         --  There can be only one conflict.
-      end if;
-
-      case Next_Action.Item.Verb is
-      when Shift =>
-         Do_Shift
-           (Super, Shared, Parser_Index, Local_Config_Heap, Config, 
Next_Action.Item.State, Inserted_ID,
-            Cost_Delta, Strategy);
-
-      when Reduce =>
-         Do_Reduce_1 (Label, Super, Shared, Parser_Index, Local_Config_Heap, 
Config, Next_Action.Item);
-         Do_Reduce_2 (Label, Super, Shared, Parser_Index, Local_Config_Heap, 
Config, Inserted_ID, Cost_Delta, Strategy);
-
-      when Accept_It =>
-         raise SAL.Programmer_Error with "found test case for Do_Reduce 
Accept_It";
-
-      when Error =>
-         null;
-      end case;
-
-   exception
-   when Bad_Config =>
-      null;
-   end Do_Reduce_2;
-
-   function Fast_Forward
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Base_Peek_Type;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
-      Config            : in out          Configuration)
-     return Non_Success_Status
-   is
-      --  Apply the ops in Config; they were inserted by some fix.
-      --  Return Abandon if Config should be abandoned, otherwise Continue.
-      --  Leaves Config.Error_Token, Config.Check_Status set.
-      --
-      --  If there are conflicts, all are parsed; if more than one succeed,
-      --  all are enqueued in Local_Config_Heap, and this returns Abandon.
-
-      use all type SAL.Base_Peek_Type;
-      use all type Ada.Containers.Count_Type;
-
-      Parse_Items : Parse.Parse_Item_Arrays.Vector;
-   begin
-      if Parse.Parse
-        (Super, Shared, Parser_Index, Parse_Items, Config,
-         Shared_Token_Goal => Invalid_Token_Index,
-         All_Conflicts     => True,
-         Trace_Prefix      => "fast_forward")
-      then
-         --  At least one config parsed without error, so continue with them.
-         if Parse_Items.Length = 1 then
-            Config := Parse_Items (1).Config;
-            Config.Current_Ops := No_Insert_Delete;
-            Config.Ops.Append ((Fast_Forward, Config.Current_Shared_Token));
-            return Continue;
-         else
-            --  Enqueue all passing configs, abandon current.
-            for Item of Parse_Items loop
-               if Item.Parsed and Item.Config.Error_Token.ID = 
Invalid_Token_ID then
-                  Item.Config.Ops.Append ((Fast_Forward, 
Item.Config.Current_Shared_Token));
-                  Config.Current_Ops := No_Insert_Delete;
-                  Local_Config_Heap.Add (Item.Config);
-
-                  if Trace_McKenzie > Detail then
-                     Base.Put ("fast forward conflict", Super, Shared, 
Parser_Index, Item.Config);
-                  end if;
-               end if;
-            end loop;
-            return Abandon;
-         end if;
-
-      else
-         --  No parse item parsed without error. This indicates that Config.Ops
-         --  (enqueued by language_fixes) did not fix all the problems; see
-         --  test_mckenzie_recover Two_Missing_Ends. If it made progress we try
-         --  more fixes.
-         for Item of Parse_Items loop
-            declare
-               Parsed_Config : Configuration renames Item.Config;
-               Remaining : SAL.Base_Peek_Type;
-            begin
-               if Parsed_Config.Current_Insert_Delete = No_Insert_Delete then
-                  --  Insert_Delete contains only Deletes, and the next token 
caused an
-                  --  error.
-                  Parsed_Config.Ops.Append ((Fast_Forward, 
Config.Current_Shared_Token));
-                  Local_Config_Heap.Add (Parsed_Config);
-                  if Trace_McKenzie > Detail then
-                     Base.Put ("fast forward failure", Super, Shared, 
Parser_Index, Item.Config);
-                  end if;
-
-               elsif Parsed_Config.Current_Insert_Delete = 1 then
-                  --  No progress made; abandon config
-                  null;
-
-               else
-                  --  Find fixes at the failure point. We don't reset
-                  --  Config.Current_Insert_Delete here, to allow skipping 
Check.
-                  --
-                  --  If the unparsed ops are at Config.Current_Shared_Token, 
then new
-                  --  ops applied in Process_One below must be inserted in 
Config.Ops
-                  --  before the unparsed ops, so the final order applied to 
the full
-                  --  parser is correct.
-                  if Parsed_Config.Insert_Delete 
(Parsed_Config.Current_Insert_Delete).Token_Index =
-                    Parsed_Config.Current_Shared_Token
-                  then
-                     Parsed_Config.Current_Ops := Parsed_Config.Ops.Last_Index;
-                     Remaining := Parsed_Config.Insert_Delete.Last_Index - 
Parsed_Config.Current_Insert_Delete;
-                     loop
-                        exit when Remaining = 0;
-                        if Parsed_Config.Ops (Parsed_Config.Current_Ops).Op in 
Insert_Delete_Op_Label then
-                           Remaining := Remaining - 1;
-                        end if;
-                        Parsed_Config.Current_Ops := Parsed_Config.Current_Ops 
- 1;
-                        if Parsed_Config.Current_Ops < 
Parsed_Config.Ops.First_Index then
-                           if Trace_McKenzie > Outline then
-                              Put_Line
-                                (Super.Trace.all, Super.Label (Parser_Index),
-                                 "Insert_Delete is out of sync with Ops");
-                           end if;
-                           raise Bad_Config;
-                        end if;
-                     end loop;
-                  end if;
-
-                  if Parsed_Config.Current_Insert_Delete > 1 then
-                     if Parsed_Config.Current_Ops = No_Insert_Delete then
-                        Parsed_Config.Ops.Append ((Fast_Forward, 
Config.Current_Shared_Token));
-                     else
-                        Parsed_Config.Ops.Insert
-                          ((Fast_Forward, Config.Current_Shared_Token), Before 
=> Parsed_Config.Current_Ops);
-                        Parsed_Config.Current_Ops := Parsed_Config.Current_Ops 
+ 1;
-                     end if;
-                  end if;
-                  Local_Config_Heap.Add (Parsed_Config);
-                  if Trace_McKenzie > Detail then
-                     Base.Put ("fast forward failure", Super, Shared, 
Parser_Index, Item.Config);
-                  end if;
-               end if;
-            end;
-         end loop;
-         return Abandon;
-      end if;
-   exception
-   when SAL.Container_Full | Bad_Config =>
-      return Abandon;
-   end Fast_Forward;
-
-   function Check
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Base_Peek_Type;
-      Config            : in out          Configuration;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
-     return Check_Status
-   is
-      use all type Ada.Containers.Count_Type;
-      use all type Semantic_Checks.Check_Status_Label;
-      use all type Parser.Language_Fixes_Access;
-
-      Parse_Items : Parse.Parse_Item_Arrays.Vector;
-   begin
-      if Parse.Parse
-        (Super, Shared, Parser_Index, Parse_Items, Config, 
Config.Resume_Token_Goal,
-         All_Conflicts => False,
-         Trace_Prefix  => "check")
-      then
-         Config.Error_Token.ID := Invalid_Token_ID;
-         return Success;
-      end if;
-
-      --  All Parse_Items failed; enqueue them so Language_Fixes can try to 
fix them.
-      declare
-         Parse_Error_Found : Boolean := False;
-      begin
-         for Item of Parse_Items loop
-
-            if Item.Config.Error_Token.ID /= Invalid_Token_ID and 
Item.Config.Check_Status.Label = Ok then
-               Parse_Error_Found := True;
-
-               if Item.Shift_Count = 0 or
-                 ((Item.Config.Ops.Length > 0 and then
-                     Item.Config.Ops (Item.Config.Ops.Last_Index).Op in 
Undo_Reduce | Push_Back) and
-                    Item.Config.Current_Shared_Token = 
Config.Current_Shared_Token)
-               then
-                  --  (Item.config.ops is empty on the very first Check). This 
is the
-                  --  same error Config originally found; report it in Config, 
so
-                  --  Use_Minimal_Complete_Actions can see it.
-                  Config.Error_Token  := Item.Config.Error_Token;
-                  Config.Check_Status := (Label => Ok);
-               end if;
-            end if;
-
-            if Item.Shift_Count > 0 and then
-              (Item.Config.Check_Status.Label /= Ok or
-                 Item.Config.Error_Token.ID /= Invalid_Token_ID)
-            then
-               --  Some progress was made; let Language_Fixes try to fix the 
new
-               --  error.
-               --
-               --  This is abandoning the original location of the error, 
which may
-               --  not be entirely fixed. So we increase the cost. See
-               --  test_mckenzie_recover Loop_Bounds.
-               Item.Config.Cost := Item.Config.Cost + 1;
-               begin
-                  if Item.Config.Ops (Item.Config.Ops.Last_Index).Op = 
Fast_Forward then
-                     Item.Config.Ops 
(Item.Config.Ops.Last_Index).FF_Token_Index :=
-                       Item.Config.Current_Shared_Token;
-                  else
-                     Item.Config.Ops.Append ((Fast_Forward, 
Item.Config.Current_Shared_Token));
-                  end if;
-               exception
-               when SAL.Container_Full =>
-                  raise Bad_Config;
-               end;
-               Local_Config_Heap.Add (Item.Config);
-               if Trace_McKenzie > Detail then
-                  Base.Put ("for Language_Fixes ", Super, Shared, 
Parser_Index, Item.Config);
-               end if;
-            end if;
-         end loop;
-
-         if Parse_Error_Found then
-            return Continue;
-         else
-            --  Failed due to Semantic_Check
-            if Shared.Language_Fixes = null then
-               --  Only fix is to ignore the error
-               return Continue;
-            else
-               --  Assume Language_Fixes handles this, not Explore.
-               return Abandon;
-            end if;
-         end if;
-      end;
-   exception
-   when Bad_Config =>
-      return Abandon;
-   end Check;
-
-   function Check_Reduce_To_Start
-     (Super        : not null access Base.Supervisor;
-      Shared       : not null access Base.Shared;
-      Parser_Index : in              SAL.Base_Peek_Type;
-      Orig_Config  : in              Configuration)
-     return Boolean
-      --  Returns True if Config reduces to the start nonterm.
-   is
-      Table : Parse_Table renames Shared.Table.all;
-
-      function To_Reduce_Action (Item : in Minimal_Action) return 
Reduce_Action_Rec
-      is begin
-         return (Reduce, (Item.Nonterm, 0), null, null, Item.Token_Count);
-      end To_Reduce_Action;
-
-      Local_Config_Heap : Config_Heaps.Heap_Type; -- never used, because 
Do_Language_Fixes is False.
-
-      Config : Configuration  := Orig_Config;
-      Action : Minimal_Action := Table.States 
(Config.Stack.Peek.State).Minimal_Complete_Action;
-   begin
-      loop
-         case Action.Verb is
-         when Pause =>
-            return True;
-
-         when Shift =>
-            return False;
-
-         when Reduce =>
-            Do_Reduce_1
-              ("", Super, Shared, Parser_Index, Local_Config_Heap, Config,
-               To_Reduce_Action (Action),
-               Do_Language_Fixes => False);
-
-            Action := Table.States 
(Config.Stack.Peek.State).Minimal_Complete_Action;
-         end case;
-         --  loop only exits via returns above
-      end loop;
-   end Check_Reduce_To_Start;
-
-   procedure Try_Push_Back
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Base_Peek_Type;
-      Config            : in out          Configuration;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
-   is
-      Trace          : WisiToken.Trace'Class renames Super.Trace.all;
-      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
-
-      Token : constant Recover_Token := Config.Stack (1).Token;
-   begin
-      --  Try pushing back the stack top, to allow insert and other
-      --  operations at that point.
-      --
-      --  Since we are not actually changing the source text, it is tempting
-      --  to give this operation zero cost. But then we keep doing push_back
-      --  forever, making no progress. So we give it a cost.
-
-      if not Token.Virtual then
-         --  If Virtual, this is from earlier in this recover session; no point
-         --  in trying to redo it.
-
-         declare
-            New_Config : Configuration := Config;
-         begin
-            New_Config.Error_Token.ID := Invalid_Token_ID;
-            New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
-
-            New_Config.Stack.Pop;
-
-            if Token.Min_Terminal_Index = Invalid_Token_Index then
-               --  Token is empty; Config.current_shared_token does not 
change, no
-               --  cost increase.
-               New_Config.Ops.Append ((Push_Back, Token.ID, 
New_Config.Current_Shared_Token));
-            else
-               New_Config.Cost := New_Config.Cost + McKenzie_Param.Push_Back 
(Token.ID);
-               New_Config.Ops.Append ((Push_Back, Token.ID, 
Token.Min_Terminal_Index));
-               New_Config.Current_Shared_Token := Token.Min_Terminal_Index;
-            end if;
-
-            Local_Config_Heap.Add (New_Config);
-
-            if Trace_McKenzie > Detail then
-               Base.Put ("push_back " & Image (Token.ID, 
Trace.Descriptor.all), Super, Shared,
-                         Parser_Index, New_Config);
-            end if;
-         end;
-      end if;
-   exception
-   when SAL.Container_Full =>
-      if Trace_McKenzie > Outline then
-         Put_Line (Super.Trace.all, Super.Label (Parser_Index), "config.ops is 
full");
-      end if;
-   end Try_Push_Back;
-
-   procedure Insert_From_Action_List
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Base_Peek_Type;
-      Config            : in              Configuration;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
-   is
-      use all type Ada.Containers.Count_Type;
-
-      Table  : Parse_Table renames Shared.Table.all;
-      EOF_ID : Token_ID renames Super.Trace.Descriptor.EOI_ID;
-
-      --  Find terminal insertions from the current state's action_list to try.
-      --
-      --  We perform any needed reductions and one shift, so the config is
-      --  in a consistent state, and enqueue the result. If there are any
-      --  conflicts or semantic check fails encountered, they create other
-      --  configs to enqueue.
-
-      I : Action_List_Iterator := First (Table.States (Config.Stack 
(1).State));
-
-      Cached_Config : Configuration;
-      Cached_Action : Reduce_Action_Rec;
-      --  Most of the time, all the reductions in a state are the same. So
-      --  we cache the first result. This includes one reduction; if an
-      --  associated semantic check failed, this does not include the fixes.
-   begin
-      loop
-         exit when I.Is_Done;
-
-         declare
-            ID     : constant Token_ID := I.Symbol;
-            Action : Parse_Action_Rec renames I.Action;
-         begin
-            if ID /= EOF_ID and then --  can't insert eof
-              ID /= Invalid_Token_ID and then -- invalid when Verb = Error
-              (Config.Ops.Length = 0 or else
-                 --  Don't insert an ID we just pushed back or deleted; we 
know that failed.
-                 (Config.Ops (Config.Ops.Last_Index) /= (Push_Back, ID, 
Config.Current_Shared_Token) and
-                    Config.Ops (Config.Ops.Last_Index) /= (Delete, ID, 
Config.Current_Shared_Token)))
-            then
-               case Action.Verb is
-               when Shift =>
-                  declare
-                     New_Config : Configuration := Config;
-                  begin
-                     New_Config.Error_Token.ID := Invalid_Token_ID;
-                     New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
-
-                     Do_Shift
-                       (Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Action.State, ID,
-                        Cost_Delta => 0,
-                        Strategy   => Explore_Table);
-                  end;
-
-               when Reduce =>
-                  if not Equal (Action, Cached_Action) then
-                     declare
-                        New_Config : Configuration := Config;
-                     begin
-                        New_Config.Error_Token.ID := Invalid_Token_ID;
-                        New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
-
-                        Do_Reduce_1 ("Insert", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Action);
-                        Cached_Config := New_Config;
-                        Cached_Action := Action;
-
-                        Do_Reduce_2
-                          ("Insert", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, ID,
-                           Cost_Delta => 0,
-                           Strategy   => Explore_Table);
-                     end;
-
-                  else
-                     declare
-                        New_Config : Configuration := Cached_Config;
-                     begin
-                        Do_Reduce_2
-                          ("Insert", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, ID,
-                           Cost_Delta => 0,
-                           Strategy   => Explore_Table);
-                     end;
-                  end if;
-
-               when Accept_It =>
-                  raise SAL.Programmer_Error with "found test case for 
Process_One Accept_It";
-
-               when Error =>
-                  null;
-               end case;
-            end if;
-         end;
-         I.Next;
-      end loop;
-   end Insert_From_Action_List;
-
-   function Insert_Minimal_Complete_Actions
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Base_Peek_Type;
-      Orig_Config       : in              Configuration;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
-     return Integer
-      --  Returns count of terminals inserted; 0 if Orig_Config reduces to
-      --  the start nonterm.
-   is
-      use all type Ada.Containers.Count_Type;
-
-      Table        : Parse_Table renames Shared.Table.all;
-      Descriptor   : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
-      Insert_Count : Integer := 0;
-
-      Cost_Delta : constant Integer := -1;
-
-      New_Config      : Configuration  := Orig_Config;
-      Complete_Action : Minimal_Action := Table.States 
(New_Config.Stack.Peek.State).Minimal_Complete_Action;
-
-      function To_Reduce_Action (Item : in Minimal_Action) return 
Reduce_Action_Rec
-      is begin
-         return (Reduce, (Item.Nonterm, 0), null, null, Item.Token_Count);
-      end To_Reduce_Action;
-
-      procedure Minimal_Do_Shift
-      is begin
-         if New_Config.Ops.Length > 0 and then
-           --  Don't insert an ID we just pushed back or deleted; we know that 
failed.
-           (New_Config.Ops (New_Config.Ops.Last_Index) =
-              (Push_Back, Complete_Action.ID, New_Config.Current_Shared_Token) 
or
-              New_Config.Ops (New_Config.Ops.Last_Index) =
-              (Delete, Complete_Action.ID, New_Config.Current_Shared_Token))
-         then
-            if Trace_McKenzie > Extra then
-               Put_Line
-                 (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions abandoned " &
-                    Image (Complete_Action.ID, Descriptor));
-            end if;
-            pragma Assert (Insert_Count = 0);
-         else
-            if Trace_McKenzie > Extra then
-               Put_Line
-                 (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions shift " &
-                    Image (Complete_Action.ID, Descriptor));
-            end if;
-            New_Config.Check_Status := (Label => WisiToken.Semantic_Checks.Ok);
-            Insert_Count            := Insert_Count + 1;
-
-            Do_Shift
-              (Super, Shared, Parser_Index, Local_Config_Heap, New_Config, 
Complete_Action.State, Complete_Action.ID,
-               Cost_Delta,
-               Strategy   => Minimal_Complete);
-         end if;
-      end Minimal_Do_Shift;
-
-   begin
-      case Complete_Action.Verb is
-      when Pause =>
-         return 0;
-
-      when Reduce =>
-         --  Do a reduce, look at resulting state. Keep reducing until we can't
-         --  anymore.
-         declare
-            Reduce_Action : Reduce_Action_Rec := To_Reduce_Action 
(Complete_Action);
-         begin
-            loop
-               Do_Reduce_1
-                 ("Minimal_Complete_Actions", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Reduce_Action,
-                  Do_Language_Fixes => False);
-
-               Complete_Action := Table.States 
(New_Config.Stack.Peek.State).Minimal_Complete_Action;
-
-               case Complete_Action.Verb is
-               when Pause =>
-                  return 0;
-               when Shift =>
-                  Minimal_Do_Shift;
-                  return Insert_Count;
-               when Reduce =>
-                  null;
-               end case;
-
-               Reduce_Action := To_Reduce_Action
-                 (Table.States 
(New_Config.Stack.Peek.State).Minimal_Complete_Action);
-            end loop;
-         end;
-
-      when Shift =>
-         Minimal_Do_Shift;
-      end case;
-      return Insert_Count;
-   end Insert_Minimal_Complete_Actions;
-
-   procedure Insert_Matching_Begin
-     (Super                : not null access Base.Supervisor;
-      Shared               : not null access Base.Shared;
-      Parser_Index         : in              SAL.Base_Peek_Type;
-      Config               : in              Configuration;
-      Local_Config_Heap    : in out          Config_Heaps.Heap_Type;
-      Matching_Begin_Token : in              Token_ID)
-   is
-      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
-      New_Config : Configuration                  := Config;
-
-      Action     : constant Parse_Action_Node_Ptr := Action_For
-        (Shared.Table.all, New_Config.Stack (1).State, Matching_Begin_Token);
-   begin
-      if Trace_McKenzie > Extra then
-         Put_Line
-           (Super.Trace.all, Super.Label (Parser_Index), "Matching_Begin 
insert " &
-              Image (Matching_Begin_Token, Descriptor));
-      end if;
-
-      case Action.Item.Verb is
-      when Shift =>
-         Do_Shift
-           (Super, Shared, Parser_Index, Local_Config_Heap, New_Config, 
Action.Item.State,
-            Matching_Begin_Token,
-            Cost_Delta => -1,
-            Strategy   => Matching_Begin);
-
-      when Reduce =>
-         Do_Reduce_1
-           ("Matching_Begin", Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Action.Item,
-            Do_Language_Fixes => False);
-
-         Do_Reduce_2
-           ("Matching_Begin", Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Matching_Begin_Token,
-            Cost_Delta => -1,
-            Strategy   => Matching_Begin);
-
-      when Error | Accept_It =>
-         if Trace_McKenzie > Extra then
-            Put_Line
-              (Super.Trace.all, Super.Label (Parser_Index), "Matching_Begin 
abandoned " &
-                 Image (Matching_Begin_Token, Descriptor));
-         end if;
-      end case;
-   end Insert_Matching_Begin;
-
-   procedure Try_Insert_Terminal
-     (Super                        : not null access Base.Supervisor;
-      Shared                       : not null access Base.Shared;
-      Parser_Index                 : in              SAL.Base_Peek_Type;
-      Config                       : in              Configuration;
-      Local_Config_Heap            : in out          Config_Heaps.Heap_Type;
-      Use_Minimal_Complete_Actions : in              Boolean;
-      Matching_Begin_Token         : in              Token_ID)
-   is begin
-      if Use_Minimal_Complete_Actions then
-         if 0 = Insert_Minimal_Complete_Actions (Super, Shared, Parser_Index, 
Config, Local_Config_Heap) then
-            if Matching_Begin_Token /= Invalid_Token_ID then
-               Insert_Matching_Begin (Super, Shared, Parser_Index, Config, 
Local_Config_Heap, Matching_Begin_Token);
-            else
-               Insert_From_Action_List (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
-            end if;
-         else
-            --  We _do not_ do Insert_From_Action_list when
-            --  Insert_Minimal_Complete inserted something; let it try again 
next
-            --  cycle.
-            null;
-         end if;
-      else
-         Insert_From_Action_List (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
-      end if;
-
-      --  It is tempting to use the Goto_List to find nonterms to insert.
-      --  But that can easily lead to error states, and it turns out to be
-      --  not useful, especially if the grammar has been relaxed so most
-      --  expressions and lists can be empty.
-
-   exception
-   when Bad_Config =>
-      null;
-   end Try_Insert_Terminal;
-
-   procedure Try_Insert_Quote
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Base_Peek_Type;
-      Config            : in out          Configuration;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
-   is
-      use all type Parser.Language_String_ID_Set_Access;
-
-      Descriptor  : WisiToken.Descriptor renames Shared.Trace.Descriptor.all;
-      Check_Limit : Token_Index renames 
Shared.Table.McKenzie_Param.Check_Limit;
-
-      Current_Line            : constant Line_Number_Type := 
Shared.Terminals.all (Config.Current_Shared_Token).Line;
-      Lexer_Error_Token_Index : Base_Token_Index;
-      Lexer_Error_Token       : Base_Token;
-
-      function Recovered_Lexer_Error (Line : in Line_Number_Type) return 
Base_Token_Index
-      is begin
-         --  We are assuming the list of lexer errors is short, so binary
-         --  search would not be significantly faster.
-         for Err of reverse Shared.Lexer.Errors loop
-            if Err.Recover_Token /= Invalid_Token_Index and then
-              Shared.Terminals.all (Err.Recover_Token).Line = Line
-            then
-               return Err.Recover_Token;
-            end if;
-         end loop;
-         return Invalid_Token_Index;
-      end Recovered_Lexer_Error;
-
-      function String_ID_Set (String_ID : in Token_ID) return Token_ID_Set
-      is begin
-         return
-           (if Shared.Language_String_ID_Set = null
-            then (Descriptor.First_Terminal .. Descriptor.Last_Terminal => 
True)
-            else Shared.Language_String_ID_Set (Descriptor, String_ID));
-      end String_ID_Set;
-
-      procedure String_Literal_In_Stack
-        (New_Config        : in out Configuration;
-         Matching          : in     SAL.Peek_Type;
-         String_Literal_ID : in     Token_ID)
-      is
-         Saved_Shared_Token : constant Token_Index := 
New_Config.Current_Shared_Token;
-
-         Tok         : Recover_Token;
-         J           : Token_Index;
-         Parse_Items : Parse.Parse_Item_Arrays.Vector;
-      begin
-         --  Matching is the index of a token on New_Config.Stack containing a 
string
-         --  literal. Push back thru that token, then delete all tokens after
-         --  the string literal to Saved_Shared_Token.
-         for I in 1 .. Matching loop
-            Tok := New_Config.Stack.Pop.Token;
-            New_Config.Ops.Append ((Push_Back, Tok.ID, 
Tok.Min_Terminal_Index));
-         end loop;
-
-         New_Config.Current_Shared_Token := Tok.Min_Terminal_Index;
-
-         --  Find last string literal in pushed back terminals.
-         J := Saved_Shared_Token - 1;
-         loop
-            exit when Shared.Terminals.all (J).ID = String_Literal_ID;
-            J := J - 1;
-         end loop;
-
-         begin
-            if Parse.Parse
-              (Super, Shared, Parser_Index, Parse_Items, New_Config,
-               Shared_Token_Goal => J,
-               All_Conflicts     => False,
-               Trace_Prefix      => "insert quote parse pushback")
-            then
-               --  The non-deleted tokens parsed without error. We don't care 
if any
-               --  conflicts were encountered; we are not using the parse 
result.
-               New_Config := Parse_Items (1).Config;
-               New_Config.Ops.Append ((Fast_Forward, 
New_Config.Current_Shared_Token));
-            else
-               raise SAL.Programmer_Error;
-            end if;
-         exception
-         when Bad_Config =>
-            raise SAL.Programmer_Error;
-         end;
-         J := New_Config.Current_Shared_Token; -- parse result
-         loop
-            exit when J = Saved_Shared_Token;
-            New_Config.Ops.Append ((Delete, Shared.Terminals.all (J).ID, J));
-            J := J + 1;
-         end loop;
-
-         New_Config.Current_Shared_Token := Saved_Shared_Token;
-
-      end String_Literal_In_Stack;
-
-      procedure Finish
-        (Label       : in     String;
-         New_Config  : in out Configuration;
-         First, Last : in     Base_Token_Index)
-      is begin
-         --  Delete tokens First .. Last; either First - 1 or Last + 1 should
-         --  be a String_Literal. Leave Current_Shared_Token at Last + 1.
-
-         New_Config.Error_Token.ID := Invalid_Token_ID;
-         New_Config.Check_Status   := (Label => WisiToken.Semantic_Checks.Ok);
-
-         --  This is a guess, so we give it a nominal cost
-         New_Config.Cost := New_Config.Cost + 1;
-
-         for I in First .. Last loop
-            if New_Config.Ops.Is_Full then
-               if Trace_McKenzie > Outline then
-                  Put_Line (Super.Trace.all, Super.Label (Parser_Index), 
"config.ops is full");
-               end if;
-               raise Bad_Config;
-            end if;
-            New_Config.Ops.Append ((Delete, Shared.Terminals.all (I).ID, I));
-         end loop;
-         New_Config.Current_Shared_Token := Last + 1;
-
-         --  Allow insert/delete tokens
-         New_Config.Ops.Append ((Fast_Forward, 
New_Config.Current_Shared_Token));
-
-         if New_Config.Resume_Token_Goal - Check_Limit < 
New_Config.Current_Shared_Token then
-            New_Config.Resume_Token_Goal := New_Config.Current_Shared_Token + 
Check_Limit;
-            if Trace_McKenzie > Detail then
-               Put_Line
-                 (Super.Trace.all, Super.Label (Parser_Index), 
"resume_token_goal:" & Token_Index'Image
-                    (New_Config.Resume_Token_Goal));
-            end if;
-         end if;
-
-         Config.Strategy_Counts (String_Quote) := Config.Strategy_Counts 
(String_Quote) + 1;
-
-         if Trace_McKenzie > Detail then
-            Base.Put ("insert missing quote " & Label & " ", Super, Shared, 
Parser_Index, New_Config);
-         end if;
-      end Finish;
-
-   begin
-      --  When the lexer finds an unbalanced quote, it inserts a virtual
-      --  balancing quote at the same character position as the unbalanced
-      --  quote, returning an empty string literal token there. The parser
-      --  does not see that as an error; it encounters a syntax error
-      --  before, at, or after that string literal.
-      --
-      --  Here we assume the parse error in Config.Error_Token is due to
-      --  putting the balancing quote in the wrong place, and attempt to
-      --  find a better place to put the balancing quote. Then all tokens
-      --  from the balancing quote to the unbalanced quote are now part of a
-      --  string literal, so delete them, leaving just the string literal
-      --  created by Lexer error recovery.
-
-      --  First we check to see if there is an unbalanced quote in the
-      --  current line; if not, just return. Some lexer errors are for other
-      --  unrecognized characters; see ada_mode-recover_bad_char.adb.
-      --
-      --  An alternate strategy is to treat the lexer error as a parse error
-      --  immediately, but that complicates the parse logic.
-
-      Config.String_Quote_Checked := Current_Line;
-
-      Lexer_Error_Token_Index := Recovered_Lexer_Error (Current_Line);
-
-      if Lexer_Error_Token_Index = Invalid_Token_Index then
-         return;
-      end if;
-
-      Lexer_Error_Token := Shared.Terminals.all (Lexer_Error_Token_Index);
-
-      --  It is not possible to tell where the best place to put the
-      --  balancing quote is, so we always try all reasonable places.
-
-      if Lexer_Error_Token.Byte_Region.First = 
Config.Error_Token.Byte_Region.First then
-         --  The parse error token is the string literal at the lexer error.
-         --
-         --  case a: Insert the balancing quote somewhere before the error
-         --  point. There is no way to tell how far back to put the balancing
-         --  quote, so we just do one non-empty token. See
-         --  test_mckenzie_recover.adb String_Quote_0. So far we have not found
-         --  a test case for more than one token.
-         declare
-            New_Config : Configuration := Config;
-            Token      : Recover_Token;
-         begin
-            loop
-               Token := New_Config.Stack.Pop.Token;
-               if Token.Byte_Region /= Null_Buffer_Region then
-                  New_Config.Ops.Append ((Push_Back, Token.ID, 
Token.Min_Terminal_Index));
-                  exit;
-               end if;
-            end loop;
-
-            Finish ("a", New_Config, Token.Min_Terminal_Index, 
Config.Current_Shared_Token - 1);
-            Local_Config_Heap.Add (New_Config);
-         end;
-
-         --  Note that it is not reasonable to insert a quote after the error
-         --  in this case. If that were the right solution, the parser error
-         --  token would not be the lexer repaired string literal, since a
-         --  string literal would be legal here.
-
-      elsif Lexer_Error_Token.Byte_Region.First < 
Config.Error_Token.Byte_Region.First then
-         --  The unbalanced quote is before the parse error token; see
-         --  test_mckenzie_recover.adb String_Quote_2.
-         --
-         --  The missing quote belongs after the parse error token, before or
-         --  at the end of the current line; try inserting it at the end of
-         --  the current line.
-         --
-         --  The lexer repaired string literal may be in a reduced token on the
-         --  stack.
-
-         declare
-            use all type SAL.Base_Peek_Type;
-            Matching : SAL.Peek_Type := 1;
-         begin
-            Find_Descendant_ID
-              (Super.Parser_State (Parser_Index).Tree, Config, 
Lexer_Error_Token.ID,
-               String_ID_Set (Lexer_Error_Token.ID), Matching);
-
-            if Matching = Config.Stack.Depth then
-               --  String literal is in a virtual nonterm; give up. So far 
this only
-               --  happens in a high cost non critical config.
-               if Trace_McKenzie > Detail then
-                  Put_Line
-                    (Super.Trace.all, Super.Label (Parser_Index), "abandon 
missing quote b; string literal in virtual");
-               end if;
-               return;
-            end if;
-
-            declare
-               New_Config : Configuration := Config;
-            begin
-               String_Literal_In_Stack (New_Config, Matching, 
Lexer_Error_Token.ID);
-
-               Finish
-                 ("b", New_Config, Config.Current_Shared_Token, 
Shared.Line_Begin_Token.all (Current_Line + 1) - 1);
-               Local_Config_Heap.Add (New_Config);
-            end;
-         end;
-
-      else
-         --  The unbalanced quote is after the parse error token.
-
-         --  case c: Assume a missing quote belongs immediately before the 
current token.
-         --  See test_mckenzie_recover.adb String_Quote_3.
-         declare
-            New_Config : Configuration := Config;
-         begin
-            Finish ("c", New_Config, Config.Current_Shared_Token, 
Lexer_Error_Token_Index - 1);
-            Local_Config_Heap.Add (New_Config);
-         exception
-         when Bad_Config =>
-            null;
-         end;
-
-         --  case d: Assume a missing quote belongs somewhere farther before
-         --  the current token; try one non-empty (as in case a above). See
-         --  test_mckenzie_recover.adb String_Quote_4.
-         declare
-            New_Config : Configuration := Config;
-            Token      : Recover_Token;
-         begin
-            loop
-               Token := New_Config.Stack.Pop.Token;
-               if Token.Byte_Region /= Null_Buffer_Region then
-                  New_Config.Ops.Append ((Push_Back, Token.ID, 
Token.Min_Terminal_Index));
-                  exit;
-               end if;
-            end loop;
-
-            Finish ("d", New_Config, Token.Min_Terminal_Index, 
Lexer_Error_Token_Index - 1);
-            Local_Config_Heap.Add (New_Config);
-         exception
-         when Bad_Config =>
-            null;
-         end;
-
-         --  case e: Assume the actual error is an extra quote that terminates
-         --  an intended string literal early, in which case there is a token
-         --  on the stack containing the string literal that should be extended
-         --  to the found quote. See test_mckenzie_recover.adb String_Quote_1.
-         declare
-            use all type SAL.Base_Peek_Type;
-            Matching : SAL.Peek_Type := 1;
-         begin
-            --  Lexer_Error_Token is a string literal; find a matching one.
-            Find_Descendant_ID
-              (Super.Parser_State (Parser_Index).Tree, Config, 
Lexer_Error_Token.ID, String_ID_Set
-                 (Lexer_Error_Token.ID), Matching);
-
-            if Matching = Config.Stack.Depth then
-               --  No matching string literal, so this case does not apply.
-               null;
-            else
-               declare
-                  New_Config : Configuration := Config;
-               begin
-                  String_Literal_In_Stack (New_Config, Matching, 
Lexer_Error_Token.ID);
-
-                  Finish ("e", New_Config, Config.Current_Shared_Token, 
Lexer_Error_Token_Index);
-                  Local_Config_Heap.Add (New_Config);
-               end;
-            end if;
-         end;
-      end if;
-   exception
-   when SAL.Container_Full =>
-      if Trace_McKenzie > Outline then
-         Put_Line (Super.Trace.all, Super.Label (Parser_Index), "config.ops is 
full");
-      end if;
-
-   when Bad_Config =>
-      null;
-   end Try_Insert_Quote;
-
-   procedure Try_Delete_Input
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Base_Peek_Type;
-      Config            : in out          Configuration;
-      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
-   is
-      --  Try deleting (= skipping) the current shared input token.
-      Trace       : WisiToken.Trace'Class renames Super.Trace.all;
-      EOF_ID      : Token_ID renames Trace.Descriptor.EOI_ID;
-      Check_Limit : Token_Index renames 
Shared.Table.McKenzie_Param.Check_Limit;
-
-      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
-
-      ID : constant Token_ID := Shared.Terminals.all 
(Config.Current_Shared_Token).ID;
-   begin
-      if ID /= EOF_ID then
-         --  can't delete EOF
-         declare
-            New_Config : Configuration := Config;
-         begin
-            New_Config.Error_Token.ID := Invalid_Token_ID;
-            New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
-
-            New_Config.Cost := New_Config.Cost + McKenzie_Param.Delete (ID);
-
-            if Match_Since_FF (Config.Ops, (Push_Back, ID, 
Config.Current_Shared_Token))
-            then
-               --  We are deleting a push_back; cancel the push_back cost, to 
make
-               --  this the same as plain deleting.
-               New_Config.Cost := Natural'Max (Natural'First, New_Config.Cost 
- McKenzie_Param.Push_Back (ID));
-            end if;
-
-            New_Config.Ops.Append ((Delete, ID, Config.Current_Shared_Token));
-            New_Config.Current_Shared_Token := New_Config.Current_Shared_Token 
+ 1;
-            loop
-               exit when not Super.Parser_State 
(Parser_Index).Prev_Deleted.Contains (New_Config.Current_Shared_Token);
-               New_Config.Current_Shared_Token := 
New_Config.Current_Shared_Token + 1;
-            end loop;
-
-            if New_Config.Resume_Token_Goal - Check_Limit < 
New_Config.Current_Shared_Token then
-               New_Config.Resume_Token_Goal := New_Config.Current_Shared_Token 
+ Check_Limit;
-            end if;
-
-            Local_Config_Heap.Add (New_Config);
-
-            if Trace_McKenzie > Detail then
-               Base.Put
-                 ("delete " & Image (ID, Trace.Descriptor.all), Super, Shared, 
Parser_Index, New_Config);
-            end if;
-         end;
-      end if;
-   exception
-   when SAL.Container_Full =>
-      if Trace_McKenzie > Outline then
-         Put_Line (Super.Trace.all, Super.Label (Parser_Index), "config.ops is 
full");
-      end if;
-   end Try_Delete_Input;
-
-   procedure Process_One
-     (Super         : not null access Base.Supervisor;
-      Shared        : not null access Base.Shared;
-      Config_Status : out             Base.Config_Status)
-   is
-      --  Get one config from Super, check to see if it is a viable
-      --  solution. If not, enqueue variations to check.
-
-      use all type Base.Config_Status;
-      use all type Parser.Language_Fixes_Access;
-      use all type SAL.Base_Peek_Type;
-      use all type Semantic_Checks.Check_Status_Label;
-      use all type 
WisiToken.Parse.LR.Parser.Language_Use_Minimal_Complete_Actions_Access;
-
-      Trace      : WisiToken.Trace'Class renames Super.Trace.all;
-      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
-      Table      : Parse_Table renames Shared.Table.all;
-
-      Parser_Index : SAL.Base_Peek_Type;
-      Config       : Configuration;
-
-      Local_Config_Heap : Config_Heaps.Heap_Type;
-      --  We collect all the variants to enqueue, then deliver them all at
-      --  once to Super, to minimizes task interactions.
-
-      Use_Minimal_Complete_Actions : Boolean  := False;
-      Matching_Begin_Token         : Token_ID := Invalid_Token_ID;
-
-      function Allow_Insert_Terminal (Config : in Configuration) return Boolean
-      is begin
-         if Use_Minimal_Complete_Actions then
-            if Table.States 
(Config.Stack.Peek.State).Minimal_Complete_Action.Verb = Pause then
-               --  This happens when there is an extra token after an 
acceptable
-               --  grammar statement. There is no production to complete, so 
try
-               --  other things.
-               Use_Minimal_Complete_Actions := False;
-            else
-               if Trace_McKenzie > Detail then
-                  Put_Line (Super.Trace.all, Super.Label (Parser_Index), "use 
Minimal_Complete_Actions");
-               end if;
-               return True;
-            end if;
-         end if;
-         return None_Since_FF (Config.Ops, Delete);
-      end Allow_Insert_Terminal;
-
-   begin
-      Super.Get (Parser_Index, Config, Config_Status);
-
-      if Config_Status = All_Done then
-         return;
-      end if;
-
-      if Trace_McKenzie > Extra then
-         Base.Put ("dequeue", Super, Shared, Parser_Index, Config);
-         Put_Line (Trace, Super.Label (Parser_Index), "stack: " & Image 
(Config.Stack, Trace.Descriptor.all));
-      end if;
-
-      if Config.Current_Insert_Delete = 1 then
-         --  If Config.Current_Insert_Delete > 1 then Fast_Forward failed on 
this
-         --  config; don't fast_forward again.
-
-         case Fast_Forward (Super, Shared, Parser_Index, Local_Config_Heap, 
Config) is
-         when Abandon =>
-            --  We know Local_Config_Heap is empty; just tell
-            --  Super we are done working.
-            Super.Put (Parser_Index, Local_Config_Heap);
-            return;
-         when Continue =>
-            --  We don't increase cost for this Fast_Forward, since it is due 
to a
-            --  Language_Fixes.
-            null;
-         end case;
-      end if;
-
-      if Config.Error_Token.ID /= Invalid_Token_ID then
-         if Shared.Language_Fixes = null then
-            null;
-         else
-            Shared.Language_Fixes
-              (Trace, Shared.Lexer, Super.Label (Parser_Index), 
Shared.Table.all,
-               Shared.Terminals.all, Super.Parser_State (Parser_Index).Tree, 
Local_Config_Heap,
-               Config);
-
-            --  The solutions provided by Language_Fixes should be lower cost 
than
-            --  others (typically 0), so they will be checked first.
-
-            if Config.Check_Status.Label = Ok then
-               --  Parse table Error action.
-               --
-               --  We don't clear Config.Error_Token here, because 
Try_Insert_Terminal calls
-               --  Language_Use_Minimal_Complete_Actions, which needs it. We 
only clear it
-               --  when a parse results in no error (or a different error), or 
a
-               --  push_back moves the Current_Token.
-               null;
-
-            else
-               --  Assume "ignore check error" is a viable solution. But give 
it a
-               --  cost, so a solution provided by Language_Fixes is preferred.
-
-               declare
-                  New_State : Unknown_State_Index;
-               begin
-                  Config.Cost := Config.Cost + 
Table.McKenzie_Param.Ignore_Check_Fail;
-
-                  --  finish reduce.
-                  Config.Stack.Pop (SAL.Base_Peek_Type 
(Config.Check_Token_Count));
-
-                  New_State := Goto_For (Table, Config.Stack (1).State, 
Config.Error_Token.ID);
-
-                  if New_State = Unknown_State then
-                     if Config.Stack.Depth = 1 then
-                        --  Stack is empty, and we did not get Accept; really 
bad syntax got
-                        --  us here; abandon this config. See 
ada_mode-recover_bad_char.adb.
-                        Super.Put (Parser_Index, Local_Config_Heap);
-                        return;
-                     else
-                        raise SAL.Programmer_Error with
-                          "process_one found test case for new_state = 
Unknown; old state " &
-                          Trimmed_Image (Config.Stack (1).State) & " nonterm " 
& Image
-                            (Config.Error_Token.ID, Trace.Descriptor.all);
-                     end if;
-                  end if;
-
-                  Config.Stack.Push ((New_State, 
Syntax_Trees.Invalid_Node_Index, Config.Error_Token));
-
-                  --  We must clear Check_Status here, so if this config comes 
back
-                  --  here, we don't try to reduce the stack again. We also 
clear
-                  --  Error_Token, so this doesn't look like a parse error.
-                  Config.Check_Status := (Label => Ok);
-
-                  Config.Error_Token.ID := Invalid_Token_ID;
-               end;
-            end if;
-         end if;
-      end if;
-
-      if Config.Current_Insert_Delete > 1 then
-         --  Fast_Forward failed on this config; no need to check it. Remove
-         --  already parsed items from Insert_Delete, setting
-         --  Current_Insert_Delete to 1, so it will be checked after the Ops
-         --  applied below.
-         Config.Insert_Delete.Delete_First (Config.Current_Insert_Delete - 1);
-         Config.Current_Insert_Delete := 1;
-      else
-         case Check (Super, Shared, Parser_Index, Config, Local_Config_Heap) is
-         when Success =>
-            Super.Success (Parser_Index, Config, Local_Config_Heap);
-            return;
-
-         when Abandon =>
-            Super.Put (Parser_Index, Local_Config_Heap);
-            return;
-
-         when Continue =>
-            null;
-
-         end case;
-      end if;
-
-      if Trace_McKenzie > Detail then
-         Base.Put ("continuing", Super, Shared, Parser_Index, Config);
-         if Trace_McKenzie > Extra then
-            Put_Line (Trace, Super.Label (Parser_Index), "stack: " & Image 
(Config.Stack, Trace.Descriptor.all));
-         end if;
-      end if;
-
-      --  Grouping these operations ensures that there are no duplicate
-      --  solutions found. We reset the grouping after each fast_forward.
-      --
-      --  All possible permutations will be explored.
-
-      if Shared.Language_Use_Minimal_Complete_Actions = null then
-         Use_Minimal_Complete_Actions := False;
-         Matching_Begin_Token         := Invalid_Token_ID;
-      else
-         declare
-            Current_Token : Token_ID;
-            Next_Token : Token_ID;
-         begin
-            Current_Token_ID_Peek_2
-              (Shared.Terminals.all, Config.Current_Shared_Token, 
Config.Insert_Delete, Config.Current_Insert_Delete,
-               Super.Parser_State (Parser_Index).Prev_Deleted, Current_Token, 
Next_Token);
-
-            Shared.Language_Use_Minimal_Complete_Actions
-              (Current_Token, Next_Token, Config,
-               Use_Complete         => Use_Minimal_Complete_Actions,
-               Matching_Begin_Token => Matching_Begin_Token);
-         end;
-      end if;
-
-      if None_Since_FF (Config.Ops, Delete) and then
-        None_Since_FF (Config.Ops, Insert) and then
-        Config.Stack.Depth > 1 and then -- can't delete the first state
-        (not (Use_Minimal_Complete_Actions and then Check_Reduce_To_Start 
(Super, Shared, Parser_Index, Config)))
-        --  If Config reduces to the start nonterm, there's no point in 
push_back.
-      then
-         Try_Push_Back (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
-      end if;
-
-      if Allow_Insert_Terminal (Config) then
-         Try_Insert_Terminal
-           (Super, Shared, Parser_Index, Config, Local_Config_Heap, 
Use_Minimal_Complete_Actions, Matching_Begin_Token);
-      end if;
-
-      if Config.Current_Insert_Delete = No_Insert_Delete then
-         if Config.Check_Status.Label = Ok and
-           (Descriptor.String_1_ID /= Invalid_Token_ID or 
Descriptor.String_2_ID /= Invalid_Token_ID) and
-           (Config.String_Quote_Checked = Invalid_Line_Number or else
-              Config.String_Quote_Checked < Shared.Terminals.all 
(Config.Current_Shared_Token).Line)
-         then
-            --  See if there is a mismatched quote. The solution is to delete
-            --  tokens, replacing them with a string literal. So we try this 
when
-            --  it is ok to try delete.
-            Try_Insert_Quote (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
-         end if;
-
-         Try_Delete_Input (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
-      end if;
-
-      Super.Put (Parser_Index, Local_Config_Heap);
-   exception
-   when others =>
-      --  Just abandon this config; tell Super we are done.
-      Super.Put (Parser_Index, Local_Config_Heap);
-      if Debug_Mode then
-         raise;
-      end if;
-   end Process_One;
-
-end WisiToken.Parse.LR.McKenzie_Recover.Explore;
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with Ada.Exceptions;
+with WisiToken.Parse.LR.McKenzie_Recover.Parse;
+with WisiToken.Parse.LR.Parser;
+package body WisiToken.Parse.LR.McKenzie_Recover.Explore is
+
+   procedure Do_Shift
+     (Label             : in              String;
+      Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Peek_Type;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
+      Config            : in out          Configuration;
+      State             : in              State_Index;
+      ID                : in              Token_ID;
+      Cost_Delta        : in              Integer;
+      Strategy          : in              Strategies)
+   is
+      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
+
+      Op : constant Config_Op := (Insert, ID, Config.Current_Shared_Token, 
State, Config.Stack.Depth);
+   begin
+      Config.Strategy_Counts (Strategy) := Config.Strategy_Counts (Strategy) + 
1;
+
+      begin
+         Config.Ops.Append (Op);
+      exception
+      when SAL.Container_Full =>
+         Super.Config_Full (Parser_Index);
+         raise Bad_Config;
+      end;
+
+      if Cost_Delta = 0 then
+         Config.Cost := Config.Cost + McKenzie_Param.Insert (ID);
+      else
+         --  Cost_Delta /= 0 comes from Insert_Minimal_Complete_Actions. That
+         --  doesn't mean it is better than any other solution, so don't let
+         --  cost be 0.
+         --
+         --  We don't just eliminate all cost for Minimal_Complete_Actions;
+         --  that leads to using it far too much at the expense of better
+         --  solutions.
+         Config.Cost := Integer'Max (1, Config.Cost + McKenzie_Param.Insert 
(ID) + Cost_Delta);
+      end if;
+
+      Config.Error_Token.ID := Invalid_Token_ID;
+      Config.Check_Status   := (Label => WisiToken.Semantic_Checks.Ok);
+
+      Config.Stack.Push ((State, Syntax_Trees.Invalid_Node_Index, (ID, Virtual 
=> True, others => <>)));
+      if Trace_McKenzie > Detail then
+         Base.Put
+           ((if Label'Length > 0 then Label & ": " else "") & "insert " & 
Image (ID, Super.Trace.Descriptor.all),
+            Super, Shared, Parser_Index, Config);
+      end if;
+
+      Local_Config_Heap.Add (Config);
+   end Do_Shift;
+
+   procedure Do_Reduce_1
+     (Label             : in              String;
+      Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Peek_Type;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
+      Config            : in out          Configuration;
+      Action            : in              Reduce_Action_Rec;
+      Do_Language_Fixes : in              Boolean := True)
+   is
+      use all type SAL.Base_Peek_Type;
+      use all type Semantic_Checks.Check_Status_Label;
+      use all type WisiToken.Parse.LR.Parser.Language_Fixes_Access;
+
+      Prev_State : constant Unknown_State_Index := Config.Stack.Peek.State;
+
+      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
+      Table      : Parse_Table renames Shared.Table.all;
+      Nonterm    : Recover_Token;
+      New_State  : Unknown_State_Index;
+   begin
+      Config.Check_Status := Parse.Reduce_Stack (Shared, Config.Stack, Action, 
Nonterm, Default_Virtual => True);
+      case Config.Check_Status.Label is
+      when Ok =>
+         null;
+
+      when Semantic_Checks.Error =>
+         Config.Error_Token       := Nonterm;
+         Config.Check_Token_Count := Action.Token_Count;
+
+         if Do_Language_Fixes then
+            if Shared.Language_Fixes /= null then
+               Shared.Language_Fixes
+                 (Super.Trace.all, Shared.Lexer, Super.Label (Parser_Index), 
Shared.Table.all, Shared.Terminals.all,
+                  Super.Parser_State (Parser_Index).Tree, Local_Config_Heap,
+                  Config);
+            end if;
+         end if;
+
+         --  Finish the reduce; ignore the check fail.
+         Config.Stack.Pop (SAL.Base_Peek_Type (Config.Check_Token_Count));
+         Config.Error_Token.ID := Invalid_Token_ID;
+         Config.Check_Status   := (Label => Ok);
+      end case;
+
+      if Config.Stack.Depth = 0 or else Config.Stack (1).State = Unknown_State 
then
+         raise Bad_Config;
+      end if;
+
+      New_State := Goto_For (Table, Config.Stack (1).State, 
Action.Production.LHS);
+
+      if New_State = Unknown_State then
+         if Trace_McKenzie > Extra then
+            Put_Line
+              (Super.Trace.all, Super.Label (Parser_Index), Label &
+                 ": Do_Reduce_1: unknown_State " & 
Config.Stack.Peek.State'Image & " " &
+                 Image (Action.Production.LHS, Descriptor));
+         end if;
+         raise Bad_Config;
+      end if;
+
+      Config.Stack.Push ((New_State, Syntax_Trees.Invalid_Node_Index, 
Nonterm));
+
+      if Trace_McKenzie > Extra and Label'Length > 0 then
+         Put_Line
+           (Super.Trace.all, Super.Label (Parser_Index), Label &
+              ": state" & State_Index'Image (Prev_State) & " reduce" &
+              Ada.Containers.Count_Type'Image (Action.Token_Count) & " to " &
+              Image (Action.Production.LHS, Descriptor) & ", goto" &
+              State_Index'Image (New_State) & " via" & State_Index'Image 
(Config.Stack (2).State));
+      end if;
+   end Do_Reduce_1;
+
+   procedure Do_Reduce_2
+     (Label             : in              String;
+      Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Peek_Type;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
+      Config            : in out          Configuration;
+      Inserted_ID       : in              Token_ID;
+      Cost_Delta        : in              Integer;
+      Strategy          : in              Strategies)
+   is
+      --  Perform reduce actions until shift Inserted_ID; if all succeed,
+      --  add the final configuration to the heap, return True. If a conflict 
is
+      --  encountered, process the other action the same way. If a semantic
+      --  check fails, enqueue possible solutions. For parse table error
+      --  actions, or exception Bad_Config, return False.
+
+      Orig_Config : Configuration;
+      Table       : Parse_Table renames Shared.Table.all;
+      Next_Action : Parse_Action_Node_Ptr := Action_For (Table, Config.Stack 
(1).State, Inserted_ID);
+   begin
+      if Next_Action.Next /= null then
+         Orig_Config := Config;
+      end if;
+
+      case Next_Action.Item.Verb is
+      when Shift =>
+         Do_Shift
+           (Label, Super, Shared, Parser_Index, Local_Config_Heap, Config, 
Next_Action.Item.State, Inserted_ID,
+            Cost_Delta, Strategy);
+
+      when Reduce =>
+         Do_Reduce_1 (Label, Super, Shared, Parser_Index, Local_Config_Heap, 
Config, Next_Action.Item);
+         Do_Reduce_2
+           (Label, Super, Shared, Parser_Index, Local_Config_Heap, Config, 
Inserted_ID, Cost_Delta, Strategy);
+
+      when Accept_It =>
+         raise SAL.Programmer_Error with "found test case for Do_Reduce 
Accept_It";
+
+      when Error =>
+         null;
+      end case;
+
+      loop
+         exit when Next_Action.Next = null;
+         --  There is a conflict; create a new config to shift or reduce.
+         declare
+            New_Config : Configuration := Orig_Config;
+            Action     : Parse_Action_Rec renames Next_Action.Next.Item;
+         begin
+            case Action.Verb is
+            when Shift =>
+               Do_Shift
+                 (Label, Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Action.State, Inserted_ID,
+                  Cost_Delta, Strategy);
+
+            when Reduce =>
+               Do_Reduce_1 (Label, Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Action);
+               Do_Reduce_2
+                 (Label, Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Inserted_ID,
+                  Cost_Delta, Strategy);
+
+            when Accept_It =>
+               raise SAL.Programmer_Error with "found test case for Do_Reduce 
Accept_It conflict";
+
+            when Error =>
+               null;
+            end case;
+         end;
+
+         Next_Action := Next_Action.Next;
+      end loop;
+   exception
+   when Bad_Config =>
+      if Debug_Mode then
+         raise;
+      end if;
+   end Do_Reduce_2;
+
+   function Fast_Forward
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type;
+      Config            : in out          Configuration)
+     return Non_Success_Status
+   is
+      --  Apply the ops in Config; they were inserted by some fix.
+      --  Return Abandon if Config should be abandoned, otherwise Continue.
+      --  Leaves Config.Error_Token, Config.Check_Status set.
+      --
+      --  If there are conflicts, all are parsed; if more than one succeed,
+      --  all are enqueued in Local_Config_Heap, and this returns Abandon.
+
+      use all type SAL.Base_Peek_Type;
+      use all type Ada.Containers.Count_Type;
+
+      Parse_Items : Parse.Parse_Item_Arrays.Vector;
+
+      Dummy : Boolean := Parse.Parse
+        (Super, Shared, Parser_Index, Parse_Items, Config,
+         Shared_Token_Goal => Invalid_Token_Index,
+         All_Conflicts     => True,
+         Trace_Prefix      => "fast_forward");
+   begin
+      --  This solution is from Language_Fixes; any cost increase is done 
there.
+
+      if Parse_Items.Length = 1 then
+         declare
+            Item : Parse.Parse_Item renames Parse_Items (1);
+         begin
+            if Item.Parsed and Item.Config.Current_Insert_Delete = 
No_Insert_Delete then
+               --  Item.Config.Error_Token.ID, Check_Status are correct.
+               Config := Item.Config;
+
+               Config.Ops.Append ((Fast_Forward, Config.Current_Shared_Token));
+               Config.Minimal_Complete_State := None;
+               Config.Matching_Begin_Done    := False;
+               return Continue;
+            else
+               return Abandon;
+            end if;
+         end;
+      else
+         for Item of Parse_Items loop
+            if Item.Parsed and Item.Config.Current_Insert_Delete = 
No_Insert_Delete then
+               Item.Config.Ops.Append ((Fast_Forward, 
Item.Config.Current_Shared_Token));
+               Item.Config.Minimal_Complete_State := None;
+               Item.Config.Matching_Begin_Done    := False;
+               Local_Config_Heap.Add (Item.Config);
+
+               if Trace_McKenzie > Detail then
+                  Base.Put ("fast forward enqueue", Super, Shared, 
Parser_Index, Item.Config);
+               end if;
+            end if;
+         end loop;
+         return Abandon;
+      end if;
+   exception
+   when Bad_Config =>
+      return Abandon;
+   when SAL.Container_Full =>
+      Super.Config_Full (Parser_Index);
+      return Abandon;
+   end Fast_Forward;
+
+   function Check
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Config            : in out          Configuration;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+     return Check_Status
+   is
+      use all type Semantic_Checks.Check_Status_Label;
+
+      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
+
+      Parse_Items : Parse.Parse_Item_Arrays.Vector;
+      Result      : Check_Status := Continue;
+
+      function Max_Push_Back_Token_Index (Ops : in Config_Op_Arrays.Vector) 
return WisiToken.Base_Token_Index
+      is
+         Result : WisiToken.Base_Token_Index := 
WisiToken.Base_Token_Index'First;
+      begin
+         --  For Ops since last Fast_Forward, return maximum Token_Index in a
+         --  Push_Back. If there are no such ops, return a value that will be
+         --  less than the current token index.
+         for Op of reverse Ops loop
+            exit when Op.Op = Fast_Forward;
+            if Op.Op = Push_Back and then Op.PB_Token_Index > Result then
+               Result := Op.PB_Token_Index;
+            end if;
+         end loop;
+         return Result;
+      end Max_Push_Back_Token_Index;
+
+   begin
+      if Parse.Parse
+        (Super, Shared, Parser_Index, Parse_Items, Config, 
Config.Resume_Token_Goal,
+         All_Conflicts => False,
+         Trace_Prefix  => "check")
+      then
+         Config.Error_Token.ID := Invalid_Token_ID;
+         --  FIXME: if there were conflicts, enqueue them; they might yield a
+         --  cheaper or same cost solution?
+         return Success;
+      end if;
+
+      --  Set Config.error to reflect failure, if it is at current token, so
+      --  Use_Minimal_Complete_Actions can see it.
+      declare
+         Item          : Parse.Parse_Item renames Parse_Items 
(Parse_Items.First_Index);
+         Parsed_Config : Configuration renames Item.Config;
+      begin
+         if Parsed_Config.Check_Status.Label /= Ok then
+            Config.Check_Status := Parsed_Config.Check_Status;
+            Config.Error_Token  := Parsed_Config.Error_Token;
+
+            --  Explore cannot fix a check fail; only Language_Fixes can. The
+            --  "ignore error" case is handled immediately on return from
+            --  Language_Fixes in Process_One, below.
+            Result := Abandon;
+
+         elsif Parsed_Config.Error_Token.ID /= Invalid_Token_ID then
+
+            if Item.Shift_Count = 0 then
+               Config.Error_Token  := Parsed_Config.Error_Token;
+               Config.Check_Status := (Label => Ok);
+            else
+               --  Error is not at current token, but Explore might find 
something
+               --  that will help (see test_mckenzie_recover.adb Extra_Begin). 
On the
+               --  other hand, this can lead to lots of bogus configs (see
+               --  If_In_Handler).
+               Config.Error_Token.ID := Invalid_Token_ID;
+               Config.Check_Status   := (Label => Ok);
+            end if;
+         end if;
+      end;
+
+      --  All Parse_Items either failed or were not parsed; if they failed
+      --  and made progress, enqueue them so Language_Fixes can try to fix
+      --  them.
+      for Item of Parse_Items loop
+         if Item.Config.Error_Token.ID /= Invalid_Token_ID and then
+           Item.Shift_Count > 0 and then
+           Max_Push_Back_Token_Index (Item.Config.Ops) < 
Item.Config.Current_Shared_Token - 1
+         then
+            --  Some progress was made; explore at the new error point. It is
+            --  likely that there is only one actual error point, and this 
moves
+            --  away from it, so we give it a cost.
+            begin
+               Item.Config.Minimal_Complete_State := None;
+               Item.Config.Matching_Begin_Done    := False;
+               if Item.Config.Ops (Item.Config.Ops.Last_Index).Op = 
Fast_Forward then
+
+                  Item.Config.Cost := Item.Config.Cost + 
McKenzie_Param.Fast_Forward;
+
+                  Item.Config.Ops (Item.Config.Ops.Last_Index).FF_Token_Index 
:=
+                    Item.Config.Current_Shared_Token;
+
+               else
+                  Item.Config.Cost := Item.Config.Cost + 
McKenzie_Param.Fast_Forward;
+
+                  Item.Config.Ops.Append ((Fast_Forward, 
Item.Config.Current_Shared_Token));
+               end if;
+            exception
+            when SAL.Container_Full =>
+               Super.Config_Full (Parser_Index);
+               raise Bad_Config;
+            end;
+            Local_Config_Heap.Add (Item.Config);
+            if Trace_McKenzie > Detail then
+               Base.Put ("new error point ", Super, Shared, Parser_Index, 
Item.Config);
+            end if;
+         end if;
+      end loop;
+
+      if Trace_McKenzie > Extra then
+         Put_Line (Super.Trace.all, Super.Label (Parser_Index), "check result: 
" & Result'Image);
+      end if;
+      return Result;
+   exception
+   when Bad_Config =>
+      return Abandon;
+   end Check;
+
+   function Check_Reduce_To_Start
+     (Super        : not null access Base.Supervisor;
+      Shared       : not null access Base.Shared;
+      Parser_Index : in              SAL.Base_Peek_Type;
+      Orig_Config  : in              Configuration)
+     return Boolean
+      --  Returns True if Config reduces to the start nonterm.
+   is
+      Table : Parse_Table renames Shared.Table.all;
+
+      function To_Reduce_Action (Item : in Minimal_Action) return 
Reduce_Action_Rec
+      is begin
+         return (Reduce, (Item.Nonterm, 0), null, null, Item.Token_Count);
+      end To_Reduce_Action;
+
+      Local_Config_Heap : Config_Heaps.Heap_Type; -- never used, because 
Do_Language_Fixes is False.
+
+      Config  : Configuration                := Orig_Config;
+      Actions : Minimal_Action_Arrays.Vector := Table.States 
(Config.Stack.Peek.State).Minimal_Complete_Actions;
+   begin
+      loop
+         case Actions.Length is
+         when 0 =>
+            if (for some Item of Table.States (Config.Stack.Peek.State).Kernel 
=>
+                  Item.LHS = Super.Trace.Descriptor.Accept_ID)
+            then
+               return True;
+            else
+               return False;
+            end if;
+
+         when 1 =>
+            case Actions (Actions.First_Index).Verb is
+            when Shift =>
+               return False;
+
+            when Reduce =>
+               Do_Reduce_1
+                 ("", Super, Shared, Parser_Index, Local_Config_Heap, Config,
+                  To_Reduce_Action (Actions (Actions.First_Index)),
+                  Do_Language_Fixes => False);
+
+               Actions := Table.States 
(Config.Stack.Peek.State).Minimal_Complete_Actions;
+            end case;
+
+         when others =>
+            return False;
+         end case;
+
+         --  loop only exits via returns above
+      end loop;
+   end Check_Reduce_To_Start;
+
+   procedure Try_Push_Back
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Config            : in out          Configuration;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+   is
+      Trace          : WisiToken.Trace'Class renames Super.Trace.all;
+      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
+
+      Token : constant Recover_Token := Config.Stack (1).Token;
+   begin
+      --  Try pushing back the stack top, to allow insert and other
+      --  operations at that point.
+      --
+      --  Since we are not actually changing the source text, it is tempting
+      --  to give this operation zero cost. But then we keep doing push_back
+      --  forever, making no progress. So we give it a cost.
+
+      if not Token.Virtual then
+         --  If Virtual, this is from earlier in this recover session; no point
+         --  in trying to redo it.
+
+         declare
+            New_Config : Configuration := Config;
+         begin
+            New_Config.Error_Token.ID := Invalid_Token_ID;
+            New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
+
+            New_Config.Stack.Pop;
+
+            if Token.Min_Terminal_Index = Invalid_Token_Index then
+               --  Token is empty; Config.current_shared_token does not 
change, no
+               --  cost increase.
+               New_Config.Ops.Append ((Push_Back, Token.ID, 
New_Config.Current_Shared_Token));
+            else
+               New_Config.Cost := New_Config.Cost + McKenzie_Param.Push_Back 
(Token.ID);
+               New_Config.Ops.Append ((Push_Back, Token.ID, 
Token.Min_Terminal_Index));
+               New_Config.Current_Shared_Token := Token.Min_Terminal_Index;
+            end if;
+            New_Config.Strategy_Counts (Explore_Table) := 
New_Config.Strategy_Counts (Explore_Table) + 1;
+
+            Local_Config_Heap.Add (New_Config);
+
+            if Trace_McKenzie > Detail then
+               Base.Put ("push_back " & Image (Token.ID, 
Trace.Descriptor.all), Super, Shared,
+                         Parser_Index, New_Config);
+            end if;
+         end;
+      end if;
+   exception
+   when SAL.Container_Full =>
+      Super.Config_Full (Parser_Index);
+   end Try_Push_Back;
+
+   function Just_Pushed_Back_Or_Deleted (Config : in Configuration; ID : in 
Token_ID) return Boolean
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Config.Ops.Length = 0 then
+         return False;
+      else
+         declare
+            Last_Op : Config_Op renames Config.Ops (Config.Ops.Last_Index);
+         begin
+            return Last_Op = (Push_Back, ID, Config.Current_Shared_Token) or
+              Last_Op = (Push_Back, ID, Config.Current_Shared_Token - 1) or
+            Last_Op = (Delete, ID, Config.Current_Shared_Token) or
+              Last_Op = (Delete, ID, Config.Current_Shared_Token - 1);
+         end;
+      end if;
+   end Just_Pushed_Back_Or_Deleted;
+
+   procedure Try_Undo_Reduce
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Config            : in out          Configuration;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+   is
+      Trace : WisiToken.Trace'Class renames Super.Trace.all;
+      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
+
+      Token : constant Recover_Token := Config.Stack (1).Token;
+   begin
+      --  Try expanding the nonterm on the stack top, to allow pushing_back
+      --  its components, or insert and other operations at that point.
+
+      if Undo_Reduce_Valid (Config.Stack, Super.Parser_State 
(Parser_Index).Tree) then
+         declare
+            New_Config  : Configuration := Config;
+            Token_Count : Ada.Containers.Count_Type;
+         begin
+            New_Config.Error_Token.ID := Invalid_Token_ID;
+            New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
+
+            Token_Count := Undo_Reduce (New_Config.Stack, Super.Parser_State 
(Parser_Index).Tree);
+
+            if Token.Min_Terminal_Index /= Invalid_Token_Index  then
+               --  If Token is empty no cost increase.
+               New_Config.Cost := New_Config.Cost + McKenzie_Param.Undo_Reduce 
(Token.ID);
+            end if;
+
+            New_Config.Ops.Append ((Undo_Reduce, Token.ID, Token_Count));
+
+            New_Config.Strategy_Counts (Explore_Table) := 
New_Config.Strategy_Counts (Explore_Table) + 1;
+
+            Local_Config_Heap.Add (New_Config);
+
+            if Trace_McKenzie > Detail then
+               Base.Put ("undo_reduce " & Image (Token.ID, 
Trace.Descriptor.all), Super, Shared,
+                         Parser_Index, New_Config);
+            end if;
+         end;
+      end if;
+   exception
+   when SAL.Container_Full =>
+      Super.Config_Full (Parser_Index);
+   end Try_Undo_Reduce;
+
+   procedure Insert_From_Action_List
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Config            : in              Configuration;
+      Minimal_Insert    : in              Token_ID_Arrays.Vector;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+   is
+      Table      : Parse_Table renames Shared.Table.all;
+      EOF_ID     : Token_ID renames Super.Trace.Descriptor.EOI_ID;
+      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
+
+      --  Find terminal insertions from the current state's action_list to try.
+      --
+      --  We perform any needed reductions and one shift, so the config is
+      --  in a consistent state, and enqueue the result. If there are any
+      --  conflicts or semantic check fails encountered, they create other
+      --  configs to enqueue.
+
+      I : Action_List_Iterator := First (Table.States 
(Config.Stack.Peek.State));
+
+      Current_Token : constant Token_ID := Current_Token_ID_Peek
+        (Shared.Terminals.all, Config.Current_Shared_Token, 
Config.Insert_Delete, Config.Current_Insert_Delete);
+
+      Cached_Config : Configuration;
+      Cached_Action : Reduce_Action_Rec;
+      --  Most of the time, all the reductions in a state are the same. So
+      --  we cache the first result. This includes one reduction; if an
+      --  associated semantic check failed, this does not include the fixes.
+   begin
+
+      loop
+         exit when I.Is_Done;
+
+         declare
+            ID     : constant Token_ID := I.Symbol;
+            Action : Parse_Action_Rec renames I.Action;
+         begin
+            if ID /= EOF_ID and then --  can't insert eof
+              ID /= Invalid_Token_ID -- invalid when Verb = Error
+            then
+               if Just_Pushed_Back_Or_Deleted (Config, ID) then
+                  if Trace_McKenzie > Extra then
+                     Put_Line
+                       (Super.Trace.all, Super.Label (Parser_Index), "Insert: 
abandon " & Image (ID, Descriptor) &
+                          ": undo push_back");
+                  end if;
+               elsif ID = Current_Token then
+                  --  This needed because we allow explore when the error is 
not at the
+                  --  explore point; it prevents inserting useless tokens (ie
+                  --  'identifier ;' in ada_lite).
+                  if Trace_McKenzie > Extra then
+                     Put_Line
+                       (Super.Trace.all, Super.Label (Parser_Index), "Insert: 
abandon " & Image (ID, Descriptor) &
+                          ": current token");
+                  end if;
+
+               elsif (for some Minimal of Minimal_Insert => ID = Minimal) then
+                  --  Was inserted by Insert_Minimal_Complete_Actions
+                  null;
+
+               else
+                  case Action.Verb is
+                  when Shift =>
+                     declare
+                        New_Config : Configuration := Config;
+                     begin
+                        New_Config.Error_Token.ID := Invalid_Token_ID;
+                        New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
+
+                        Do_Shift
+                          ("Insert", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Action.State, ID,
+                           Cost_Delta => 0,
+                           Strategy   => Explore_Table);
+                     end;
+
+                  when Reduce =>
+                     if not Equal (Action, Cached_Action) then
+                        declare
+                           New_Config : Configuration := Config;
+                        begin
+                           New_Config.Error_Token.ID := Invalid_Token_ID;
+                           New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
+
+                           Do_Reduce_1 ("Insert", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Action);
+                           Cached_Config := New_Config;
+                           Cached_Action := Action;
+
+                           Do_Reduce_2
+                             ("Insert", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, ID,
+                              Cost_Delta => 0,
+                              Strategy   => Explore_Table);
+                        end;
+
+                     else
+                        declare
+                           New_Config : Configuration := Cached_Config;
+                        begin
+                           Do_Reduce_2
+                             ("Insert", Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, ID,
+                              Cost_Delta => 0,
+                              Strategy   => Explore_Table);
+                        end;
+                     end if;
+
+                  when Accept_It =>
+                     raise SAL.Programmer_Error with "found test case for 
Process_One Accept_It";
+
+                  when Error =>
+                     null;
+                  end case;
+               end if;
+            end if;
+         end;
+         I.Next;
+      end loop;
+   end Insert_From_Action_List;
+
+   function Insert_Minimal_Complete_Actions
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Orig_Config       : in out          Configuration;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+     return Token_ID_Arrays.Vector
+      --  Return tokens inserted (empty if none).
+   is
+      use all type SAL.Base_Peek_Type;
+      use Ada.Containers;
+
+      Table         : Parse_Table renames Shared.Table.all;
+      Descriptor    : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
+      Inserted      : Token_ID_Array (1 .. 10) := (others => Invalid_Token_ID);
+      Inserted_Last : Integer                  := Inserted'First - 1;
+
+      type Work_Item is record
+         Action : Minimal_Action;
+         Config : Configuration;
+      end record;
+
+      package Item_Queues is new SAL.Gen_Unbounded_Definite_Queues (Work_Item);
+
+      Work : Item_Queues.Queue;
+
+      function To_Reduce_Action (Action : in Minimal_Action) return 
Reduce_Action_Rec
+        is (Reduce, (Action.Nonterm, 0), null, null, Action.Token_Count);
+
+      procedure Minimal_Do_Shift (Action : in Minimal_Action; Config : in out 
Configuration)
+      is begin
+         --  Check for a cycle. We compare stack depth as well as state, so
+         --  nested compound statements don't look like a cycle; see
+         --  test_mckenzie_recover Push_Back_1. We don't check for cycles in
+         --  Insert_From_Action_List because we assume cost eliminates cycles
+         --  there; Minimal_Complete_Delta is usually negative, so cost does
+         --  not necessarily increase here.
+         for Op of reverse Config.Ops loop
+            if Op.Op = Insert and then
+              (Op.Ins_ID = Action.ID and Op.State = Action.State and 
Op.Stack_Depth = Config.Stack.Depth)
+            then
+               if Trace_McKenzie > Extra then
+                  Put_Line
+                    (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions: abandon " &
+                       Image (Action.ID, Descriptor) & Action.State'Image & ": 
cycle");
+               end if;
+               return;
+            end if;
+         end loop;
+
+         --  We don't check Action.ID = Current_Token; the error is at the
+         --  explore point, so ID is valid.
+
+         if Just_Pushed_Back_Or_Deleted (Config, Action.ID) then
+            if Trace_McKenzie > Extra then
+               Put_Line
+                 (Super.Trace.all, Super.Label (Parser_Index),
+                  "Minimal_Complete_Actions: abandon " & Image (Action.ID, 
Descriptor) & ": undo push back");
+            end if;
+         else
+            Config.Check_Status           := (Label => 
WisiToken.Semantic_Checks.Ok);
+            Config.Minimal_Complete_State := Active;
+            Inserted_Last                 := Inserted_Last + 1;
+            Inserted (Inserted_Last)      := Action.ID;
+
+            Do_Shift
+              ("Minimal_Complete_Actions", Super, Shared, Parser_Index, 
Local_Config_Heap, Config,
+               Action.State, Action.ID, 
Table.McKenzie_Param.Minimal_Complete_Cost_Delta,
+               Strategy => Minimal_Complete);
+         end if;
+      end Minimal_Do_Shift;
+
+      procedure Enqueue_Min_Actions
+        (Label       : in String;
+         Actions     : in Minimal_Action_Arrays.Vector;
+         Recursive   : in Boolean;
+         Config      : in Configuration;
+         Reduce_Only : in Boolean)
+      is
+         use SAL;
+         Length : array (Actions.First_Index .. Actions.Last_Index) of 
Count_Type := (others => Count_Type'Last);
+
+         Item_Not_Recursive  : array (Actions.First_Index .. 
Actions.Last_Index) of Boolean := (others => False);
+
+         Not_Recursive_Count : Count_Type := 0;
+         Min_Length          : Count_Type := Count_Type'Last;
+         Use_Recursive       : Boolean;
+      begin
+         --  Enqueue non-minimal actions on Work,
+         if Trace_McKenzie > Extra then
+            Put_Line
+              (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions: " & Label &
+                 Image (Actions, Descriptor) & (if Recursive then " recursive" 
else ""));
+         end if;
+
+         if Actions.Length = 0 then
+            return;
+         elsif Actions.Length = 1 then
+            if (not Reduce_Only) or Actions (Actions.First_Index).Verb = 
Reduce then
+               Work.Add ((Actions (Actions.First_Index), Config));
+            end if;
+            return;
+         end if;
+
+         for I in Actions.First_Index .. Actions.Last_Index loop
+            declare
+               Action     : Minimal_Action renames Actions (I);
+               Next_State : constant State_Index :=
+                 (case Action.Verb is
+                  when Shift => Action.State,
+                  when Reduce => Goto_For
+                    (Shared.Table.all,
+                     Config.Stack.Peek (Base_Peek_Type (Action.Token_Count) + 
1).State,
+                     Action.Nonterm));
+               Before_Dot : constant Token_ID :=
+                 (case Action.Verb is
+                  when Shift => Action.ID,
+                  when Reduce => Action.Nonterm);
+               Kernel     : Kernel_Info_Arrays.Vector renames 
Shared.Table.States (Next_State).Kernel;
+            begin
+               if (not Reduce_Only) or Action.Verb = Reduce then
+                  for Item of Kernel loop
+                     Item_Not_Recursive (I) := Item_Not_Recursive (I) or not 
Item.Recursive;
+                     if Item.Before_Dot = Before_Dot and
+                       Item.Length_After_Dot < Length (I)
+                     then
+                        Length (I) := Item.Length_After_Dot;
+                        if Length (I) < Min_Length then
+                           Min_Length := Length (I);
+                        end if;
+                     end if;
+                  end loop;
+               end if;
+            end;
+            if Item_Not_Recursive (I) then
+               Not_Recursive_Count := Not_Recursive_Count + 1;
+            end if;
+         end loop;
+
+         Use_Recursive := Recursive and Not_Recursive_Count > 0 and 
Not_Recursive_Count < Actions.Length;
+
+         for I in Length'Range loop
+            if (Use_Recursive and Item_Not_Recursive (I)) or ((not 
Use_Recursive) and Length (I) = Min_Length) then
+               Work.Add ((Actions (I), Config));
+            elsif Trace_McKenzie > Extra then
+               Put_Line
+                 (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions: drop " &
+                    Image (Actions (I), Descriptor));
+            end if;
+         end loop;
+      end Enqueue_Min_Actions;
+
+   begin
+      if Orig_Config.Stack.Depth = 1 then
+         --  Get here with an empty source file, or a syntax error on the first
+         --  token.
+         return Token_ID_Arrays.Empty_Vector;
+
+      elsif Orig_Config.Minimal_Complete_State = Done then
+         if Trace_McKenzie > Extra then
+            Put_Line
+              (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions: done");
+         end if;
+         return Token_ID_Arrays.Empty_Vector;
+      end if;
+
+      Enqueue_Min_Actions
+        ("",
+         Table.States (Orig_Config.Stack.Peek.State).Minimal_Complete_Actions,
+         Table.States 
(Orig_Config.Stack.Peek.State).Minimal_Complete_Actions_Recursive,
+         Orig_Config, Reduce_Only => False);
+
+      loop
+         exit when Work.Is_Empty;
+
+         declare
+            Item : Work_Item := Work.Get;
+         begin
+            if Trace_McKenzie > Extra then
+               Put_Line
+                 (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions: dequeue work item " &
+                    Image (Item.Action, Descriptor));
+            end if;
+
+            case Item.Action.Verb is
+            when Reduce =>
+               --  Do a reduce, look at resulting state. Keep reducing until 
we can't
+               --  anymore.
+               declare
+                  Reduce_Action : Reduce_Action_Rec := To_Reduce_Action 
(Item.Action);
+                  Actions       : Minimal_Action_Arrays.Vector;
+                  Recursive     : Boolean;
+               begin
+                  loop
+                     Do_Reduce_1
+                       ("Minimal_Complete_Actions", Super, Shared, 
Parser_Index, Local_Config_Heap, Item.Config,
+                        Reduce_Action,
+                        Do_Language_Fixes => False);
+
+                     Actions   := Table.States 
(Item.Config.Stack.Peek.State).Minimal_Complete_Actions;
+                     Recursive := Table.States 
(Item.Config.Stack.Peek.State).Minimal_Complete_Actions_Recursive;
+
+                     case Actions.Length is
+                     when 0 =>
+                        if Trace_McKenzie > Extra then
+                           Put_Line
+                             (Super.Trace.all, Super.Label (Parser_Index),
+                              "Minimal_Complete_Actions abandoned: no 
actions");
+                        end if;
+                        exit;
+                     when 1 =>
+                        case Actions (Actions.First_Index).Verb is
+                        when Shift =>
+                           Minimal_Do_Shift (Actions (Actions.First_Index), 
Item.Config);
+                           exit;
+                        when Reduce =>
+                           Reduce_Action := To_Reduce_Action (Actions 
(Actions.First_Index));
+                        end case;
+
+                     when others =>
+                        Enqueue_Min_Actions ("multiple actions ", Actions, 
Recursive, Item.Config, Reduce_Only => True);
+                        exit;
+                     end case;
+                  end loop;
+               end;
+
+            when Shift =>
+               Minimal_Do_Shift (Item.Action, Item.Config);
+            end case;
+         end;
+      end loop;
+
+      if Inserted_Last = Inserted'First - 1 then
+         if Orig_Config.Minimal_Complete_State = Active then
+            Orig_Config.Minimal_Complete_State := Done;
+         end if;
+      end if;
+
+      return To_Vector (Inserted (1 .. Inserted_Last));
+   end Insert_Minimal_Complete_Actions;
+
+   procedure Insert_Matching_Begin
+     (Super                 : not null access Base.Supervisor;
+      Shared                : not null access Base.Shared;
+      Parser_Index          : in              SAL.Base_Peek_Type;
+      Config                : in              Configuration;
+      Local_Config_Heap     : in out          Config_Heaps.Heap_Type;
+      Matching_Begin_Tokens : in              Token_ID_Arrays.Vector)
+   is
+      Table      : Parse_Table renames Shared.Table.all;
+      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
+   begin
+      --  We don't check for insert = current token; that's either ok or a
+      --  severe bug in Language_Use_Minimal_Complete.
+
+      if Config.Matching_Begin_Done then
+         if Trace_McKenzie > Extra then
+            Put_Line (Super.Trace.all, Super.Label (Parser_Index), 
"Matching_Begin abandoned: done");
+         end if;
+         return;
+      end if;
+
+      if Just_Pushed_Back_Or_Deleted (Config, Matching_Begin_Tokens 
(Matching_Begin_Tokens.First_Index)) then
+         if Trace_McKenzie > Extra then
+            Put_Line
+              (Super.Trace.all, Super.Label (Parser_Index), "Matching_Begin 
abandoned " &
+                 Image (Matching_Begin_Tokens 
(Matching_Begin_Tokens.First_Index), Descriptor) & ": undo push_back");
+         end if;
+         return;
+      end if;
+
+      --  Set up for Parse
+      declare
+         New_Config  : Configuration := Config;
+      begin
+         for ID of Matching_Begin_Tokens loop
+            Insert (New_Config, ID);
+         end loop;
+
+         declare
+            use all type SAL.Base_Peek_Type;
+            Parse_Items : Parse.Parse_Item_Arrays.Vector;
+            Dummy : constant Boolean :=  Parse.Parse
+              (Super, Shared, Parser_Index, Parse_Items, New_Config,
+               Shared_Token_Goal => Invalid_Token_Index,
+               All_Conflicts     => True,
+               Trace_Prefix      => "parse Matching_Begin");
+         begin
+            for Item of Parse_Items loop
+               if Item.Parsed and Item.Config.Current_Insert_Delete = 
No_Insert_Delete then
+                  Item.Config.Matching_Begin_Done := True;
+                  Item.Config.Cost := Item.Config.Cost + 
Table.McKenzie_Param.Matching_Begin;
+                  Item.Config.Strategy_Counts (Matching_Begin) := 
Item.Config.Strategy_Counts (Matching_Begin) + 1;
+                  Item.Config.Error_Token.ID := Invalid_Token_ID;
+                  Item.Config.Check_Status := (Label => 
WisiToken.Semantic_Checks.Ok);
+
+                  if Trace_McKenzie > Detail then
+                     Base.Put
+                       ("Matching_Begin: insert " & Image 
(Matching_Begin_Tokens, Descriptor),
+                        Super, Shared, Parser_Index, Item.Config);
+                  end if;
+                  Local_Config_Heap.Add (Item.Config);
+               else
+                  if Trace_McKenzie > Detail then
+                     Base.Put
+                     ("Matching_Begin: abandon " & Image 
(Matching_Begin_Tokens, Descriptor) & ": parse fail",
+                        Super, Shared, Parser_Index, Item.Config);
+                  end if;
+               end if;
+            end loop;
+         end;
+      end;
+   exception
+   when SAL.Container_Full =>
+      Super.Config_Full (Parser_Index);
+   end Insert_Matching_Begin;
+
+   procedure Try_Insert_Terminal
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Config            : in out          Configuration;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+   is
+      use all type 
WisiToken.Parse.LR.Parser.Language_Matching_Begin_Tokens_Access;
+      use all type Ada.Containers.Count_Type;
+      Tokens                : Token_ID_Array_1_3;
+      Matching_Begin_Tokens : Token_ID_Arrays.Vector;
+      Forbid_Minimal_Insert : Boolean := False;
+
+      Minimal_Inserted : Token_ID_Arrays.Vector;
+   begin
+      if Shared.Language_Matching_Begin_Tokens /= null then
+         Current_Token_ID_Peek_3
+           (Shared.Terminals.all, Config.Current_Shared_Token, 
Config.Insert_Delete, Config.Current_Insert_Delete,
+            Super.Parser_State (Parser_Index).Prev_Deleted, Tokens);
+
+         Shared.Language_Matching_Begin_Tokens (Tokens, Config, 
Matching_Begin_Tokens, Forbid_Minimal_Insert);
+      end if;
+
+      if not Forbid_Minimal_Insert then
+         --  See test_mckenzie_recover.adb Forbid_Minimal_Insert for rationale.
+         Minimal_Inserted := Insert_Minimal_Complete_Actions
+           (Super, Shared, Parser_Index, Config, Local_Config_Heap);
+      end if;
+
+      if Matching_Begin_Tokens.Length > 0 then
+         Insert_Matching_Begin (Super, Shared, Parser_Index, Config, 
Local_Config_Heap, Matching_Begin_Tokens);
+      end if;
+
+      --  We always do all three Insert_Minimal_Complete (unless
+      --  Forbid_Minimal_Insert), Insert_Matching_Begin,
+      --  Insert_From_Action_List; in general it's not possible to tell when
+      --  one will be better (see test_mckenzie_recover.adb
+      --  Always_Minimal_Complete, Always_Matching_Begin).
+      --  Insert_From_Action does not insert the Minimal_Inserted tokens,
+      --  and it will never insert the Matching_Begin_Tokens, so there is no
+      --  duplication. Insert_From_Action_List will normally be more
+      --  expensive.
+      Insert_From_Action_List (Super, Shared, Parser_Index, Config, 
Minimal_Inserted, Local_Config_Heap);
+
+      --  It is tempting to use the Goto_List to find nonterms to insert.
+      --  But that can easily lead to error states, and it turns out to be
+      --  not useful, especially if the grammar has been relaxed so most
+      --  expressions and lists can be empty.
+
+   exception
+   when Bad_Config =>
+      null;
+   end Try_Insert_Terminal;
+
+   procedure Try_Insert_Quote
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Config            : in out          Configuration;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+   is
+      use all type Parser.Language_String_ID_Set_Access;
+
+      Descriptor  : WisiToken.Descriptor renames Shared.Trace.Descriptor.all;
+      Check_Limit : WisiToken.Token_Index renames 
Shared.Table.McKenzie_Param.Check_Limit;
+
+      Current_Line            : constant Line_Number_Type := 
Shared.Terminals.all (Config.Current_Shared_Token).Line;
+      Lexer_Error_Token_Index : Base_Token_Index;
+      Lexer_Error_Token       : Base_Token;
+
+      function Recovered_Lexer_Error (Line : in Line_Number_Type) return 
Base_Token_Index
+      is begin
+         --  We are assuming the list of lexer errors is short, so binary
+         --  search would not be significantly faster.
+         for Err of reverse Shared.Lexer.Errors loop
+            if Err.Recover_Token /= Invalid_Token_Index and then
+              Shared.Terminals.all (Err.Recover_Token).Line = Line
+            then
+               return Err.Recover_Token;
+            end if;
+         end loop;
+         return Invalid_Token_Index;
+      end Recovered_Lexer_Error;
+
+      function String_ID_Set (String_ID : in Token_ID) return Token_ID_Set
+      is begin
+         if Shared.Language_String_ID_Set = null then
+            return (String_ID .. String_ID => True);
+         else
+            return Shared.Language_String_ID_Set (Descriptor, String_ID);
+         end if;
+      end String_ID_Set;
+
+      procedure String_Literal_In_Stack
+        (New_Config        : in out Configuration;
+         Matching          : in     SAL.Peek_Type;
+         String_Literal_ID : in     Token_ID)
+      is
+         Saved_Shared_Token : constant WisiToken.Token_Index := 
New_Config.Current_Shared_Token;
+
+         Tok         : Recover_Token;
+         J           : WisiToken.Token_Index;
+         Parse_Items : Parse.Parse_Item_Arrays.Vector;
+      begin
+         --  Matching is the index of a token on New_Config.Stack containing a 
string
+         --  literal. Push back thru that token, then delete all tokens after
+         --  the string literal to Saved_Shared_Token.
+         for I in 1 .. Matching loop
+            Tok := New_Config.Stack.Pop.Token;
+            New_Config.Ops.Append ((Push_Back, Tok.ID, 
Tok.Min_Terminal_Index));
+         end loop;
+
+         New_Config.Current_Shared_Token := Tok.Min_Terminal_Index;
+
+         --  Find last string literal in pushed back terminals.
+         J := Saved_Shared_Token - 1;
+         loop
+            exit when Shared.Terminals.all (J).ID = String_Literal_ID;
+            J := J - 1;
+         end loop;
+
+         begin
+            if Parse.Parse
+              (Super, Shared, Parser_Index, Parse_Items, New_Config,
+               Shared_Token_Goal => J,
+               All_Conflicts     => False,
+               Trace_Prefix      => "insert quote parse pushback")
+            then
+               --  The non-deleted tokens parsed without error. We don't care 
if any
+               --  conflicts were encountered; we are not using the parse 
result.
+               New_Config := Parse_Items (1).Config;
+               New_Config.Ops.Append ((Fast_Forward, 
New_Config.Current_Shared_Token));
+            else
+               raise SAL.Programmer_Error;
+            end if;
+         exception
+         when Bad_Config =>
+            raise SAL.Programmer_Error;
+         end;
+         J := New_Config.Current_Shared_Token; -- parse result
+         loop
+            exit when J = Saved_Shared_Token;
+            New_Config.Ops.Append ((Delete, Shared.Terminals.all (J).ID, J));
+            J := J + 1;
+         end loop;
+
+         New_Config.Current_Shared_Token := Saved_Shared_Token;
+
+      end String_Literal_In_Stack;
+
+      procedure Finish
+        (Label       : in     String;
+         New_Config  : in out Configuration;
+         First, Last : in     Base_Token_Index)
+      is begin
+         --  Delete tokens First .. Last; either First - 1 or Last + 1 should
+         --  be a String_Literal. Leave Current_Shared_Token at Last + 1.
+
+         New_Config.Error_Token.ID := Invalid_Token_ID;
+         New_Config.Check_Status   := (Label => WisiToken.Semantic_Checks.Ok);
+
+         --  This is a guess, so we give it a nominal cost
+         New_Config.Cost := New_Config.Cost + 1;
+
+         for I in First .. Last loop
+            New_Config.Ops.Append ((Delete, Shared.Terminals.all (I).ID, I));
+         end loop;
+         New_Config.Current_Shared_Token := Last + 1;
+
+         --  Let explore do insert after these deletes.
+         New_Config.Ops.Append ((Fast_Forward, 
New_Config.Current_Shared_Token));
+
+         if New_Config.Resume_Token_Goal - Check_Limit < 
New_Config.Current_Shared_Token then
+            New_Config.Resume_Token_Goal := New_Config.Current_Shared_Token + 
Check_Limit;
+            if Trace_McKenzie > Detail then
+               Put_Line
+                 (Super.Trace.all, Super.Label (Parser_Index), 
"resume_token_goal:" & WisiToken.Token_Index'Image
+                    (New_Config.Resume_Token_Goal));
+            end if;
+         end if;
+
+         New_Config.Strategy_Counts (String_Quote) := 
New_Config.Strategy_Counts (String_Quote) + 1;
+
+         if Trace_McKenzie > Detail then
+            Base.Put ("insert missing quote " & Label & " ", Super, Shared, 
Parser_Index, New_Config);
+         end if;
+      exception
+      when SAL.Container_Full =>
+         Super.Config_Full (Parser_Index);
+         raise Bad_Config;
+      end Finish;
+
+   begin
+      --  When the lexer finds an unbalanced quote, it inserts a virtual
+      --  balancing quote at the same character position as the unbalanced
+      --  quote, returning an empty string literal token there. The parser
+      --  does not see that as an error; it encounters a syntax error
+      --  before, at, or after that string literal.
+      --
+      --  Here we assume the parse error in Config.Error_Token is due to
+      --  putting the balancing quote in the wrong place, and attempt to
+      --  find a better place to put the balancing quote. Then all tokens
+      --  from the balancing quote to the unbalanced quote are now part of a
+      --  string literal, so delete them, leaving just the string literal
+      --  created by Lexer error recovery.
+
+      --  First we check to see if there is an unbalanced quote in the
+      --  current line; if not, just return. Some lexer errors are for other
+      --  unrecognized characters; see ada_mode-recover_bad_char.adb.
+      --
+      --  An alternate strategy is to treat the lexer error as a parse error
+      --  immediately, but that complicates the parse logic.
+
+      Config.String_Quote_Checked := Current_Line;
+
+      Lexer_Error_Token_Index := Recovered_Lexer_Error (Current_Line);
+
+      if Lexer_Error_Token_Index = Invalid_Token_Index then
+         return;
+      end if;
+
+      Lexer_Error_Token := Shared.Terminals.all (Lexer_Error_Token_Index);
+
+      --  It is not possible to tell where the best place to put the
+      --  balancing quote is, so we always try all reasonable places.
+
+      if Lexer_Error_Token.Byte_Region.First = 
Config.Error_Token.Byte_Region.First then
+         --  The parse error token is the string literal at the lexer error.
+         --
+         --  case a: Insert the balancing quote somewhere before the error
+         --  point. There is no way to tell how far back to put the balancing
+         --  quote, so we just do one non-empty token. See
+         --  test_mckenzie_recover.adb String_Quote_0. So far we have not found
+         --  a test case for more than one token.
+         declare
+            New_Config : Configuration := Config;
+            Token      : Recover_Token;
+         begin
+            loop
+               Token := New_Config.Stack.Pop.Token;
+               if Token.Byte_Region /= Null_Buffer_Region then
+                  New_Config.Ops.Append ((Push_Back, Token.ID, 
Token.Min_Terminal_Index));
+                  exit;
+               end if;
+            end loop;
+
+            Finish ("a", New_Config, Token.Min_Terminal_Index, 
Config.Current_Shared_Token - 1);
+            Local_Config_Heap.Add (New_Config);
+         end;
+
+         --  Note that it is not reasonable to insert a quote after the error
+         --  in this case. If that were the right solution, the parser error
+         --  token would not be the lexer repaired string literal, since a
+         --  string literal would be legal here.
+
+      elsif Lexer_Error_Token.Byte_Region.First < 
Config.Error_Token.Byte_Region.First then
+         --  The unbalanced quote is before the parse error token; see
+         --  test_mckenzie_recover.adb String_Quote_2.
+         --
+         --  The missing quote belongs after the parse error token, before or
+         --  at the end of the current line; try inserting it at the end of
+         --  the current line.
+         --
+         --  The lexer repaired string literal may be in a reduced token on the
+         --  stack.
+
+         declare
+            use all type SAL.Base_Peek_Type;
+            Matching : SAL.Peek_Type := 1;
+         begin
+            Find_Descendant_ID
+              (Super.Parser_State (Parser_Index).Tree, Config, 
Lexer_Error_Token.ID,
+               String_ID_Set (Lexer_Error_Token.ID), Matching);
+
+            if Matching = Config.Stack.Depth then
+               --  String literal is in a virtual nonterm; give up. So far 
this only
+               --  happens in a high cost non critical config.
+               if Trace_McKenzie > Detail then
+                  Put_Line
+                    (Super.Trace.all, Super.Label (Parser_Index), "abandon 
missing quote b; string literal in virtual");
+               end if;
+               return;
+            end if;
+
+            declare
+               New_Config : Configuration := Config;
+            begin
+               String_Literal_In_Stack (New_Config, Matching, 
Lexer_Error_Token.ID);
+
+               Finish
+                 ("b", New_Config, Config.Current_Shared_Token, 
Shared.Line_Begin_Token.all (Current_Line + 1) - 1);
+               Local_Config_Heap.Add (New_Config);
+            end;
+         end;
+
+      else
+         --  The unbalanced quote is after the parse error token.
+
+         --  case c: Assume a missing quote belongs immediately before the 
current token.
+         --  See test_mckenzie_recover.adb String_Quote_3.
+         declare
+            New_Config : Configuration := Config;
+         begin
+            Finish ("c", New_Config, Config.Current_Shared_Token, 
Lexer_Error_Token_Index - 1);
+            Local_Config_Heap.Add (New_Config);
+         exception
+         when Bad_Config =>
+            null;
+         end;
+
+         --  case d: Assume a missing quote belongs somewhere farther before
+         --  the current token; try one non-empty (as in case a above). See
+         --  test_mckenzie_recover.adb String_Quote_4.
+         declare
+            New_Config : Configuration := Config;
+            Token      : Recover_Token;
+         begin
+            loop
+               Token := New_Config.Stack.Pop.Token;
+               if Token.Byte_Region /= Null_Buffer_Region then
+                  New_Config.Ops.Append ((Push_Back, Token.ID, 
Token.Min_Terminal_Index));
+                  exit;
+               end if;
+            end loop;
+
+            Finish ("d", New_Config, Token.Min_Terminal_Index, 
Lexer_Error_Token_Index - 1);
+            Local_Config_Heap.Add (New_Config);
+         exception
+         when SAL.Container_Empty =>
+            --  From Stack.Pop
+            null;
+         when Bad_Config =>
+            null;
+         end;
+
+         --  case e: Assume the actual error is an extra quote that terminates
+         --  an intended string literal early, in which case there is a token
+         --  on the stack containing the string literal that should be extended
+         --  to the found quote. See test_mckenzie_recover.adb String_Quote_1.
+         declare
+            use all type SAL.Base_Peek_Type;
+            Matching : SAL.Peek_Type := 1;
+         begin
+            --  Lexer_Error_Token is a string literal; find a matching one.
+            Find_Descendant_ID
+              (Super.Parser_State (Parser_Index).Tree, Config, 
Lexer_Error_Token.ID, String_ID_Set
+                 (Lexer_Error_Token.ID), Matching);
+
+            if Matching = Config.Stack.Depth then
+               --  No matching string literal, so this case does not apply.
+               null;
+            else
+               declare
+                  New_Config : Configuration := Config;
+               begin
+                  String_Literal_In_Stack (New_Config, Matching, 
Lexer_Error_Token.ID);
+
+                  Finish ("e", New_Config, Config.Current_Shared_Token, 
Lexer_Error_Token_Index);
+                  Local_Config_Heap.Add (New_Config);
+               end;
+            end if;
+         end;
+      end if;
+   exception
+   when SAL.Container_Full =>
+      Super.Config_Full (Parser_Index);
+
+   when Bad_Config =>
+      null;
+   end Try_Insert_Quote;
+
+   procedure Try_Delete_Input
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Base_Peek_Type;
+      Config            : in out          Configuration;
+      Local_Config_Heap : in out          Config_Heaps.Heap_Type)
+   is
+      --  Try deleting (= skipping) the current shared input token.
+
+      use all type Ada.Containers.Count_Type;
+      Trace       : WisiToken.Trace'Class renames Super.Trace.all;
+      EOF_ID      : Token_ID renames Trace.Descriptor.EOI_ID;
+      Check_Limit : WisiToken.Token_Index renames 
Shared.Table.McKenzie_Param.Check_Limit;
+
+      McKenzie_Param : McKenzie_Param_Type renames Shared.Table.McKenzie_Param;
+
+      ID : constant Token_ID := Shared.Terminals.all 
(Config.Current_Shared_Token).ID;
+   begin
+      if ID /= EOF_ID and then
+         --  can't delete EOF
+         (Config.Ops.Length = 0 or else
+           --  Don't delete an ID we just inserted; waste of time
+           (not Equal (Config.Ops (Config.Ops.Last_Index), (Insert, ID, 
Config.Current_Shared_Token, 1, 0))))
+      then
+         declare
+            New_Config : Configuration := Config;
+
+            function Matching_Push_Back return Boolean
+            is begin
+               for Op of reverse New_Config.Ops loop
+                  exit when not (Op.Op in Undo_Reduce | Push_Back | Delete);
+                  if Op = (Push_Back, ID, New_Config.Current_Shared_Token) then
+                     return True;
+                  end if;
+               end loop;
+               return False;
+            end Matching_Push_Back;
+         begin
+            New_Config.Error_Token.ID := Invalid_Token_ID;
+            New_Config.Check_Status   := (Label => 
WisiToken.Semantic_Checks.Ok);
+
+            New_Config.Cost := New_Config.Cost + McKenzie_Param.Delete (ID);
+            New_Config.Strategy_Counts (Explore_Table) := 
Config.Strategy_Counts (Explore_Table) + 1;
+
+            if Matching_Push_Back then
+               --  We are deleting a push_back; cancel the push_back cost, to 
make
+               --  this the same as plain deleting.
+               New_Config.Cost := Natural'Max (Natural'First, New_Config.Cost 
- McKenzie_Param.Push_Back (ID));
+            end if;
+
+            New_Config.Ops.Append ((Delete, ID, Config.Current_Shared_Token));
+            New_Config.Current_Shared_Token := New_Config.Current_Shared_Token 
+ 1;
+            loop
+               exit when not Super.Parser_State 
(Parser_Index).Prev_Deleted.Contains (New_Config.Current_Shared_Token);
+               New_Config.Current_Shared_Token := 
New_Config.Current_Shared_Token + 1;
+            end loop;
+
+            if New_Config.Resume_Token_Goal - Check_Limit < 
New_Config.Current_Shared_Token then
+               New_Config.Resume_Token_Goal := New_Config.Current_Shared_Token 
+ Check_Limit;
+            end if;
+
+            Local_Config_Heap.Add (New_Config);
+
+            if Trace_McKenzie > Detail then
+               Base.Put
+                 ("delete " & Image (ID, Trace.Descriptor.all), Super, Shared, 
Parser_Index, New_Config);
+            end if;
+         end;
+      end if;
+   exception
+   when SAL.Container_Full =>
+      Super.Config_Full (Parser_Index);
+   end Try_Delete_Input;
+
+   procedure Process_One
+     (Super         : not null access Base.Supervisor;
+      Shared        : not null access Base.Shared;
+      Config_Status : out             Base.Config_Status)
+   is
+      --  Get one config from Super, check to see if it is a viable
+      --  solution. If not, enqueue variations to check.
+
+      use all type Base.Config_Status;
+      use all type Parser.Language_Fixes_Access;
+      use all type SAL.Base_Peek_Type;
+      use all type Semantic_Checks.Check_Status_Label;
+
+      Trace      : WisiToken.Trace'Class renames Super.Trace.all;
+      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
+      Table      : Parse_Table renames Shared.Table.all;
+
+      Parser_Index : SAL.Base_Peek_Type;
+      Config       : Configuration;
+
+      Local_Config_Heap : Config_Heaps.Heap_Type;
+      --  We collect all the variants to enqueue, then deliver them all at
+      --  once to Super, to minimizes task interactions.
+   begin
+      Super.Get (Parser_Index, Config, Config_Status);
+
+      if Config_Status = All_Done then
+         return;
+      end if;
+
+      if Trace_McKenzie > Detail then
+         Base.Put ("dequeue", Super, Shared, Parser_Index, Config);
+         if Trace_McKenzie > Extra then
+            Put_Line (Trace, Super.Label (Parser_Index), "stack: " & Image 
(Config.Stack, Trace.Descriptor.all));
+         end if;
+      end if;
+
+      --  Fast_Forward; parse Insert, Delete in Config.Ops that have not
+      --  been parsed yet. 'parse' here means adjusting Config.Stack and
+      --  Current_Terminal_Index. Code in this file always parses when
+      --  adding ops to Config (except as noted); Language_Fixes should use
+      --  McKenzie_Recover.Insert, Delete instead.
+      if Config.Current_Insert_Delete = 1 then
+         --  Config.Current_Insert_Delete > 1 is a programming error.
+
+         case Fast_Forward (Super, Shared, Parser_Index, Local_Config_Heap, 
Config) is
+         when Abandon =>
+            --  We know Local_Config_Heap is empty; just tell
+            --  Super we are done working.
+            Super.Put (Parser_Index, Local_Config_Heap);
+            return;
+         when Continue =>
+            --  We don't increase cost for this Fast_Forward, since it is due 
to a
+            --  Language_Fixes.
+            null;
+         end case;
+      end if;
+
+      pragma Assert (Config.Current_Insert_Delete = 0);
+
+      --  Language_Fixes: let it enqueue configs.
+      if Config.Error_Token.ID /= Invalid_Token_ID then
+         if Shared.Language_Fixes = null then
+            null;
+         else
+            Shared.Language_Fixes
+              (Trace, Shared.Lexer, Super.Label (Parser_Index), 
Shared.Table.all,
+               Shared.Terminals.all, Super.Parser_State (Parser_Index).Tree, 
Local_Config_Heap,
+               Config);
+
+            --  The solutions enqueued by Language_Fixes should be lower cost 
than
+            --  others (typically 0), so they will be checked first.
+
+            if Config.Check_Status.Label = Ok then
+               --  Parse table Error action.
+               --
+               --  We don't clear Config.Error_Token here, because
+               --  Language_Use_Minimal_Complete_Actions needs it. We only 
clear it
+               --  when a parse results in no error (or a different error), or 
a
+               --  push_back moves the Current_Token.
+               null;
+
+            else
+               --  Assume "ignore check error" is a viable solution. But give 
it a
+               --  cost, so a solution provided by Language_Fixes is preferred.
+
+               declare
+                  New_State : Unknown_State_Index;
+               begin
+                  Config.Cost := Config.Cost + 
Table.McKenzie_Param.Ignore_Check_Fail;
+
+                  --  finish reduce.
+                  Config.Stack.Pop (SAL.Base_Peek_Type 
(Config.Check_Token_Count));
+
+                  New_State := Goto_For (Table, Config.Stack (1).State, 
Config.Error_Token.ID);
+
+                  if New_State = Unknown_State then
+                     if Config.Stack.Depth = 1 then
+                        --  Stack is empty, and we did not get Accept; really 
bad syntax got
+                        --  us here; abandon this config. See 
ada_mode-recover_bad_char.adb.
+                        Super.Put (Parser_Index, Local_Config_Heap);
+                        return;
+                     else
+                        raise SAL.Programmer_Error with
+                          "process_one found test case for new_state = 
Unknown; old state " &
+                          Trimmed_Image (Config.Stack (1).State) & " nonterm " 
& Image
+                            (Config.Error_Token.ID, Trace.Descriptor.all);
+                     end if;
+                  end if;
+
+                  Config.Stack.Push ((New_State, 
Syntax_Trees.Invalid_Node_Index, Config.Error_Token));
+
+                  --  We clear Check_Status and Error_Token so the check error 
is ignored.
+                  Config.Check_Status := (Label => Ok);
+
+                  Config.Error_Token.ID := Invalid_Token_ID;
+               end;
+            end if;
+         end if;
+      end if;
+
+      --  Call Check to see if this config succeeds. Note that Check does
+      --  more than Fast_Forward, so the fact that Fast_Forward succeeds
+      --  does not mean we don't need to call Check.
+      case Check (Super, Shared, Parser_Index, Config, Local_Config_Heap) is
+      when Success =>
+         Super.Success (Parser_Index, Config, Local_Config_Heap);
+         return;
+
+      when Abandon =>
+         Super.Put (Parser_Index, Local_Config_Heap);
+         return;
+
+      when Continue =>
+         null;
+
+      end case;
+
+      if Trace_McKenzie > Detail then
+         Base.Put ("continuing", Super, Shared, Parser_Index, Config);
+         if Trace_McKenzie > Extra then
+            Put_Line (Trace, Super.Label (Parser_Index), "stack: " & Image 
(Config.Stack, Trace.Descriptor.all));
+         end if;
+      end if;
+
+      --  Grouping these operations (push_back, delete, insert) ensures that
+      --  there are no duplicate solutions found. We reset the grouping
+      --  after each fast_forward.
+      --
+      --  We do delete before insert so Insert_Matching_Begin can operate on
+      --  the new next token, before Fast_Forwarding past it.
+      --
+      --  All possible permutations will be explored.
+
+      Try_Insert_Terminal (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
+
+      if None_Since_FF (Config.Ops, Delete) and then
+        None_Since_FF (Config.Ops, Insert) and then
+        Config.Stack.Depth > 1 and then -- can't delete the first state
+        (not Check_Reduce_To_Start (Super, Shared, Parser_Index, Config))
+        --  If Config reduces to the start nonterm, there's no point in 
push_back.
+      then
+         Try_Push_Back (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
+         Try_Undo_Reduce (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
+      end if;
+
+      if None_Since_FF (Config.Ops, Insert) then
+         Try_Delete_Input (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
+      end if;
+
+      --  This is run once per input line, independent of what other ops
+      --  have been done.
+      if Config.Check_Status.Label = Ok and
+        (Descriptor.String_1_ID /= Invalid_Token_ID or Descriptor.String_2_ID 
/= Invalid_Token_ID) and
+        (Config.String_Quote_Checked = Invalid_Line_Number or else
+           Config.String_Quote_Checked < Shared.Terminals.all 
(Config.Current_Shared_Token).Line)
+      then
+         --  See if there is a mismatched quote. The solution is to delete
+         --  tokens, replacing them with a string literal. So we try this when
+         --  it is ok to try delete.
+         Try_Insert_Quote (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
+      end if;
+
+      Super.Put (Parser_Index, Local_Config_Heap);
+   exception
+   when E : others =>
+      --  Just abandon this config; tell Super we are done.
+      Super.Put (Parser_Index, Local_Config_Heap);
+      if Debug_Mode then
+         raise;
+      elsif Trace_McKenzie > Outline then
+         Put_Line
+           (Super.Trace.all, Super.Label (Parser_Index),
+            "Process_One: unhandled exception " & 
Ada.Exceptions.Exception_Name (E) & ": " &
+              Ada.Exceptions.Exception_Message (E));
+      end if;
+   end Process_One;
+
+end WisiToken.Parse.LR.McKenzie_Recover.Explore;
diff --git a/wisitoken-parse-lr-mckenzie_recover-explore.ads 
b/wisitoken-parse-lr-mckenzie_recover-explore.ads
index 1777728..b80124a 100644
--- a/wisitoken-parse-lr-mckenzie_recover-explore.ads
+++ b/wisitoken-parse-lr-mckenzie_recover-explore.ads
@@ -1,28 +1,28 @@
---  Abstract :
---
---  Code to explore parse table, enqueuing new configs to check.
---
---  Copyright (C) 2018 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with WisiToken.Parse.LR.McKenzie_Recover.Base;
-private package WisiToken.Parse.LR.McKenzie_Recover.Explore is
-
-   procedure Process_One
-     (Super         : not null access Base.Supervisor;
-      Shared        : not null access Base.Shared;
-      Config_Status : out             Base.Config_Status);
-
-end WisiToken.Parse.LR.McKenzie_Recover.Explore;
+--  Abstract :
+--
+--  Code to explore parse table, enqueuing new configs to check.
+--
+--  Copyright (C) 2018 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with WisiToken.Parse.LR.McKenzie_Recover.Base;
+private package WisiToken.Parse.LR.McKenzie_Recover.Explore is
+
+   procedure Process_One
+     (Super         : not null access Base.Supervisor;
+      Shared        : not null access Base.Shared;
+      Config_Status : out             Base.Config_Status);
+
+end WisiToken.Parse.LR.McKenzie_Recover.Explore;
diff --git a/wisitoken-parse-lr-mckenzie_recover-parse.adb 
b/wisitoken-parse-lr-mckenzie_recover-parse.adb
index 52ee7f1..963cf1a 100644
--- a/wisitoken-parse-lr-mckenzie_recover-parse.adb
+++ b/wisitoken-parse-lr-mckenzie_recover-parse.adb
@@ -1,304 +1,325 @@
---  Abstract :
---
---  See spec
---
---  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-package body WisiToken.Parse.LR.McKenzie_Recover.Parse is
-
-   procedure Compute_Nonterm
-     (ID              : in     Token_ID;
-      Stack           : in     Recover_Stacks.Stack;
-      Tokens          : in out Recover_Token_Array;
-      Nonterm         :    out Recover_Token;
-      Default_Virtual : in     Boolean)
-   is
-      use all type SAL.Base_Peek_Type;
-
-      Min_Terminal_Index_Set : Boolean := False;
-   begin
-      Nonterm :=
-        (ID      => ID,
-         Virtual => (if Tokens'Length = 0 then Default_Virtual else False),
-         others  => <>);
-
-      for I in Tokens'Range loop
-         Tokens (I) := Stack (Tokens'Last - I + 1).Token;
-      end loop;
-
-      for T of Tokens loop
-         Nonterm.Virtual := Nonterm.Virtual or T.Virtual;
-
-         if Nonterm.Byte_Region.First > T.Byte_Region.First then
-            Nonterm.Byte_Region.First := T.Byte_Region.First;
-         end if;
-
-         if Nonterm.Byte_Region.Last < T.Byte_Region.Last then
-            Nonterm.Byte_Region.Last := T.Byte_Region.Last;
-         end if;
-
-         if not Min_Terminal_Index_Set then
-            if T.Min_Terminal_Index /= Invalid_Token_Index then
-               Min_Terminal_Index_Set     := True;
-               Nonterm.Min_Terminal_Index := T.Min_Terminal_Index;
-            end if;
-         end if;
-      end loop;
-   end Compute_Nonterm;
-
-   function Reduce_Stack
-     (Shared          : not null access Base.Shared;
-      Stack           : in out          Recover_Stacks.Stack;
-      Action          : in              Reduce_Action_Rec;
-      Nonterm         :    out          Recover_Token;
-      Default_Virtual : in              Boolean)
-     return Semantic_Checks.Check_Status
-   is
-      use all type SAL.Base_Peek_Type;
-      use all type Semantic_Checks.Semantic_Check;
-      use all type Semantic_Checks.Check_Status_Label;
-
-      Last   : constant SAL.Base_Peek_Type := SAL.Base_Peek_Type 
(Action.Token_Count);
-      Tokens : Recover_Token_Array (1 .. Last);
-   begin
-      Compute_Nonterm (Action.Production.LHS, Stack, Tokens, Nonterm, 
Default_Virtual);
-
-      if Action.Check = null then
-         --  Now we can pop the stack.
-         Stack.Pop (SAL.Base_Peek_Type (Action.Token_Count));
-         return (Label => Ok);
-      else
-         return Status : constant Semantic_Checks.Check_Status :=
-           Action.Check (Shared.Lexer, Nonterm, Tokens, Recover_Active => True)
-         do
-            if Status.Label = Ok then
-               Stack.Pop (SAL.Base_Peek_Type (Action.Token_Count));
-            end if;
-         end return;
-      end if;
-   end Reduce_Stack;
-
-   function Parse_One_Item
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Peek_Type;
-      Parse_Items       : in out          Parse_Item_Arrays.Vector;
-      Parse_Item_Index  : in              Positive;
-      Shared_Token_Goal : in              Base_Token_Index;
-      Trace_Prefix      : in              String)
-     return Boolean
-   is
-      --  Perform parse actions on Parse_Items (Parse_Item_Index), until one
-      --  fails (return False) or Shared_Token_Goal is shifted (return
-      --  True).
-      --
-      --  We return Boolean, not Check_Status, because Abandon and Continue
-      --  are up to the caller.
-      --
-      --  If any actions have a conflict, append the conflict config and 
action to
-      --  Parse_Items.
-
-      use all type Ada.Containers.Count_Type;
-      use all type SAL.Base_Peek_Type;
-      use all type Semantic_Checks.Check_Status_Label;
-
-      Trace      : WisiToken.Trace'Class renames Super.Trace.all;
-      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
-      Table      : Parse_Table renames Shared.Table.all;
-
-      Item   : Parse_Item renames Parse_Items (Parse_Item_Index);
-      Config : Configuration renames Item.Config;
-      Action : Parse_Action_Node_Ptr renames Item.Action;
-
-      Restore_Terminals_Current : Base_Token_Index;
-      Current_Token             : Base_Token := McKenzie_Recover.Current_Token
-        (Terminals                 => Shared.Terminals.all,
-         Terminals_Current         => Config.Current_Shared_Token,
-         Restore_Terminals_Current => Restore_Terminals_Current,
-         Insert_Delete             => Config.Insert_Delete,
-         Current_Insert_Delete     => Config.Current_Insert_Delete,
-         Prev_Deleted              => Super.Parser_State 
(Parser_Index).Prev_Deleted);
-
-      New_State : Unknown_State_Index;
-      Success   : Boolean := True;
-
-   begin
-      if Trace_McKenzie > Detail then
-         Base.Put (Trace_Prefix & ": " & Image (Current_Token, Descriptor), 
Super, Shared, Parser_Index, Config);
-         if Shared_Token_Goal /= Invalid_Token_Index then
-            Put_Line (Trace, Super.Label (Parser_Index), Trace_Prefix & ": 
Shared_Token_Goal :" &
-                        Token_Index'Image (Shared_Token_Goal));
-         end if;
-      end if;
-
-      Item.Parsed := True;
-
-      if Action = null then
-         Action := Action_For (Table, Config.Stack (1).State, 
Current_Token.ID);
-      end if;
-
-      loop
-         if Action.Next /= null then
-            if Parse_Items.Is_Full then
-               if Trace_McKenzie > Outline then
-                  Put_Line (Trace, Super.Label (Parser_Index), Trace_Prefix & 
": too many conflicts; abandoning");
-               end if;
-            else
-               if Trace_McKenzie > Detail then
-                  Put_Line
-                    (Trace, Super.Label (Parser_Index), Trace_Prefix & ":" & 
State_Index'Image
-                       (Config.Stack.Peek.State) & ": add conflict " &
-                       Image (Action.Next.Item, Descriptor));
-               end if;
-
-               Parse_Items.Append ((Config, Action.Next, Parsed => False, 
Shift_Count => 0));
-            end if;
-         end if;
-
-         if Trace_McKenzie > Extra then
-            Put_Line
-              (Trace, Super.Label (Parser_Index), Trace_Prefix & ":" & 
State_Index'Image (Config.Stack.Peek.State) &
-                 " :" & Token_Index'Image (Config.Current_Shared_Token) &
-                 ":" & Image (Current_Token, Descriptor) &
-                 " : " & Image (Action.Item, Descriptor));
-         end if;
-
-         case Action.Item.Verb is
-         when Shift =>
-            Item.Shift_Count := Item.Shift_Count + 1;
-
-            Config.Stack.Push
-              ((Action.Item.State,
-                Syntax_Trees.Invalid_Node_Index,
-                (Current_Token.ID,
-                 Byte_Region        => Current_Token.Byte_Region,
-                 Min_Terminal_Index =>
-                   (if Config.Current_Insert_Delete = No_Insert_Delete
-                    then Config.Current_Shared_Token
-                    else Invalid_Token_Index),
-                 Name              => Null_Buffer_Region,
-                 Virtual           => Config.Current_Insert_Delete /= 
No_Insert_Delete)));
-
-            Current_Token := Next_Token
-              (Terminals                 => Shared.Terminals.all,
-               Terminals_Current         => Config.Current_Shared_Token,
-               Restore_Terminals_Current => Restore_Terminals_Current,
-               Insert_Delete             => Config.Insert_Delete,
-               Current_Insert_Delete     => Config.Current_Insert_Delete,
-               Prev_Deleted              => Super.Parser_State 
(Parser_Index).Prev_Deleted);
-
-         when Reduce =>
-            declare
-               Nonterm : Recover_Token;
-            begin
-               Config.Check_Status := Reduce_Stack
-                 (Shared, Config.Stack, Action.Item, Nonterm,
-                  Default_Virtual => Config.Current_Insert_Delete /= 
No_Insert_Delete);
-
-               case Config.Check_Status.Label is
-               when Ok =>
-                  New_State := Config.Stack.Peek.State;
-                  New_State := Goto_For (Table, New_State, 
Action.Item.Production.LHS);
-
-                  if New_State = Unknown_State then
-                     --  Most likely from an inappropriate language fix.
-                     if Trace_McKenzie > Outline then
-                        Base.Put (Trace_Prefix & ": Unknown_State: ", Super, 
Shared, Parser_Index, Config);
-                        Put_Line (Trace, Trace_Prefix & ": stack: " & Image 
(Config.Stack, Descriptor));
-                     end if;
-
-                     --  We can't just return False here; user must abandon 
this config.
-                     raise Bad_Config;
-                  end if;
-
-                  Config.Stack.Push ((New_State, 
Syntax_Trees.Invalid_Node_Index, Nonterm));
-
-               when Semantic_Checks.Error =>
-                  Config.Error_Token       := Nonterm;
-                  Config.Check_Token_Count := Action.Item.Token_Count;
-                  Success                  := False;
-               end case;
-            end;
-
-         when Error =>
-
-            Config.Error_Token :=
-              (ID          => Current_Token.ID,
-               Byte_Region => Current_Token.Byte_Region,
-               others      => <>);
-            Success            := False;
-
-         when Accept_It =>
-            null;
-         end case;
-
-         exit when not Success or
-           Action.Item.Verb = Accept_It or
-           (if Shared_Token_Goal = Invalid_Token_Index
-            then Config.Insert_Delete.Length = 0
-            else Config.Current_Shared_Token > Shared_Token_Goal);
-
-         Action := Action_For (Table, Config.Stack (1).State, 
Current_Token.ID);
-      end loop;
-
-      Config.Current_Shared_Token := Restore_Terminals_Current;
-
-      return Success;
-   end Parse_One_Item;
-
-   function Parse
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Peek_Type;
-      Parse_Items       :    out          Parse_Item_Arrays.Vector;
-      Config            : in              Configuration;
-      Shared_Token_Goal : in              Base_Token_Index;
-      All_Conflicts     : in              Boolean;
-      Trace_Prefix      : in              String)
-     return Boolean
-   is
-      Trace : WisiToken.Trace'Class renames Super.Trace.all;
-
-      Last_Index : Positive;
-      Success    : Boolean;
-   begin
-      Parse_Items.Clear;
-      Parse_Items.Append ((Config, Action => null, Parsed => False, 
Shift_Count => 0));
-
-      --  Clear any errors; so they reflect the parse result.
-      Parse_Items (Parse_Items.First_Index).Config.Error_Token.ID := 
Invalid_Token_ID;
-      Parse_Items (Parse_Items.First_Index).Config.Check_Status   := (Label => 
Semantic_Checks.Ok);
-
-      loop
-         --  Loop over initial config and any conflicts.
-         Last_Index := Parse_Items.Last_Index;
-
-         Success := Parse_One_Item
-           (Super, Shared, Parser_Index, Parse_Items, Last_Index, 
Shared_Token_Goal, Trace_Prefix);
-
-         exit when Parse_Items.Last_Index = Last_Index;
-
-         exit when Success and not All_Conflicts;
-
-         if Trace_McKenzie > Detail then
-            Put_Line (Trace, Super.Label (Parser_Index), Trace_Prefix & ": 
parse conflict");
-         end if;
-      end loop;
-
-      return Success;
-   end Parse;
-
-end WisiToken.Parse.LR.McKenzie_Recover.Parse;
+--  Abstract :
+--
+--  See spec
+--
+--  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+package body WisiToken.Parse.LR.McKenzie_Recover.Parse is
+
+   procedure Compute_Nonterm
+     (ID              : in     Token_ID;
+      Stack           : in     Recover_Stacks.Stack;
+      Tokens          : in out Recover_Token_Array;
+      Nonterm         :    out Recover_Token;
+      Default_Virtual : in     Boolean)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      Min_Terminal_Index_Set : Boolean := False;
+   begin
+      Nonterm :=
+        (ID      => ID,
+         Virtual => (if Tokens'Length = 0 then Default_Virtual else False),
+         others  => <>);
+
+      for I in Tokens'Range loop
+         Tokens (I) := Stack (Tokens'Last - I + 1).Token;
+      end loop;
+
+      for T of Tokens loop
+         Nonterm.Virtual := Nonterm.Virtual or T.Virtual;
+
+         if Nonterm.Byte_Region.First > T.Byte_Region.First then
+            Nonterm.Byte_Region.First := T.Byte_Region.First;
+         end if;
+
+         if Nonterm.Byte_Region.Last < T.Byte_Region.Last then
+            Nonterm.Byte_Region.Last := T.Byte_Region.Last;
+         end if;
+
+         if not Min_Terminal_Index_Set then
+            if T.Min_Terminal_Index /= Invalid_Token_Index then
+               Min_Terminal_Index_Set     := True;
+               Nonterm.Min_Terminal_Index := T.Min_Terminal_Index;
+            end if;
+         end if;
+      end loop;
+   end Compute_Nonterm;
+
+   function Reduce_Stack
+     (Shared          : not null access Base.Shared;
+      Stack           : in out          Recover_Stacks.Stack;
+      Action          : in              Reduce_Action_Rec;
+      Nonterm         :    out          Recover_Token;
+      Default_Virtual : in              Boolean)
+     return Semantic_Checks.Check_Status
+   is
+      use all type SAL.Base_Peek_Type;
+      use all type Semantic_Checks.Semantic_Check;
+      use all type Semantic_Checks.Check_Status_Label;
+
+      Last   : constant SAL.Base_Peek_Type := SAL.Base_Peek_Type 
(Action.Token_Count);
+      Tokens : Recover_Token_Array (1 .. Last);
+   begin
+      Compute_Nonterm (Action.Production.LHS, Stack, Tokens, Nonterm, 
Default_Virtual);
+
+      if Action.Check = null then
+         --  Now we can pop the stack.
+         Stack.Pop (SAL.Base_Peek_Type (Action.Token_Count));
+         return (Label => Ok);
+      else
+         return Status : constant Semantic_Checks.Check_Status :=
+           Action.Check (Shared.Lexer, Nonterm, Tokens, Recover_Active => True)
+         do
+            if Status.Label = Ok then
+               Stack.Pop (SAL.Base_Peek_Type (Action.Token_Count));
+            end if;
+         end return;
+      end if;
+   end Reduce_Stack;
+
+   function Parse_One_Item
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Peek_Type;
+      Parse_Items       : in out          Parse_Item_Arrays.Vector;
+      Parse_Item_Index  : in              Positive;
+      Shared_Token_Goal : in              Base_Token_Index;
+      Trace_Prefix      : in              String)
+     return Boolean
+   is
+      --  Perform parse actions on Parse_Items (Parse_Item_Index), until one
+      --  fails (return False) or Shared_Token_Goal is shifted (return
+      --  True).
+      --
+      --  We return Boolean, not Check_Status, because Abandon and Continue
+      --  are up to the caller.
+      --
+      --  If any actions have conflicts, append the conflict configs and 
actions to
+      --  Parse_Items.
+
+      use all type Ada.Containers.Count_Type;
+      use all type SAL.Base_Peek_Type;
+      use all type Semantic_Checks.Check_Status_Label;
+
+      Trace      : WisiToken.Trace'Class renames Super.Trace.all;
+      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
+      Table      : Parse_Table renames Shared.Table.all;
+
+      Item   : Parse_Item renames Parse_Items (Parse_Item_Index);
+      Config : Configuration renames Item.Config;
+      Action : Parse_Action_Node_Ptr renames Item.Action;
+
+      Conflict : Parse_Action_Node_Ptr;
+
+      Restore_Terminals_Current : Base_Token_Index;
+      Current_Token             : Base_Token := McKenzie_Recover.Current_Token
+        (Terminals                 => Shared.Terminals.all,
+         Terminals_Current         => Config.Current_Shared_Token,
+         Restore_Terminals_Current => Restore_Terminals_Current,
+         Insert_Delete             => Config.Insert_Delete,
+         Current_Insert_Delete     => Config.Current_Insert_Delete,
+         Prev_Deleted              => Super.Parser_State 
(Parser_Index).Prev_Deleted);
+
+      New_State : Unknown_State_Index;
+      Success   : Boolean := True;
+
+   begin
+      if Trace_McKenzie > Detail then
+         if Trace_McKenzie > Extra then
+            if Config.Current_Insert_Delete /= No_Insert_Delete then
+               Put_Line (Trace, Super.Label (Parser_Index), Trace_Prefix & ": 
Insert_Delete: " &
+                           Image (Insert_Delete_Arrays.Vector 
(Config.Insert_Delete), Trace.Descriptor.all));
+            end if;
+         end if;
+
+         Base.Put (Trace_Prefix & ": " & Image (Current_Token, Descriptor), 
Super, Shared, Parser_Index, Config);
+         if Shared_Token_Goal /= Invalid_Token_Index then
+            Put_Line (Trace, Super.Label (Parser_Index), Trace_Prefix & ": 
Shared_Token_Goal :" &
+                        WisiToken.Token_Index'Image (Shared_Token_Goal));
+         end if;
+      end if;
+
+      Item.Parsed := True;
+
+      if Action = null then
+         Action := Action_For (Table, Config.Stack (1).State, 
Current_Token.ID);
+      end if;
+
+      loop
+         Conflict := Action.Next;
+         loop
+            exit when Conflict = null;
+            if Parse_Items.Is_Full then
+               if Trace_McKenzie > Outline then
+                  Put_Line (Trace, Super.Label (Parser_Index), Trace_Prefix & 
": too many conflicts; abandoning");
+               end if;
+            else
+               declare
+                  New_Config : Configuration := Config;
+               begin
+                  New_Config.Current_Shared_Token := Restore_Terminals_Current;
+
+                  if Trace_McKenzie > Detail then
+                     Put_Line
+                       (Trace, Super.Label (Parser_Index), Trace_Prefix & ":" 
& State_Index'Image
+                          (New_Config.Stack.Peek.State) & ": add conflict " &
+                          Image (Conflict.Item, Descriptor));
+                  end if;
+
+                  Parse_Items.Append ((New_Config, Conflict, Parsed => False, 
Shift_Count => Item.Shift_Count));
+               end;
+            end if;
+            Conflict := Conflict.Next;
+         end loop;
+
+         if Trace_McKenzie > Extra then
+            Put_Line
+              (Trace, Super.Label (Parser_Index), Trace_Prefix & ":" & 
State_Index'Image (Config.Stack.Peek.State) &
+                 " :" & WisiToken.Token_Index'Image 
(Config.Current_Shared_Token) &
+                 ":" & Image (Current_Token, Descriptor) &
+                 " : " & Image (Action.Item, Descriptor) &
+                 (if Action.Item.Verb = Reduce
+                  then " via" & Config.Stack (SAL.Peek_Type 
(Action.Item.Token_Count + 1)).State'Image
+                  else ""));
+         end if;
+
+         case Action.Item.Verb is
+         when Shift =>
+            Item.Shift_Count := Item.Shift_Count + 1;
+
+            Config.Stack.Push
+              ((Action.Item.State,
+                Syntax_Trees.Invalid_Node_Index,
+                (Current_Token.ID,
+                 Byte_Region        => Current_Token.Byte_Region,
+                 Min_Terminal_Index =>
+                   (if Config.Current_Insert_Delete = No_Insert_Delete
+                    then Config.Current_Shared_Token
+                    else Invalid_Token_Index),
+                 Name              => Null_Buffer_Region,
+                 Virtual           => Config.Current_Insert_Delete /= 
No_Insert_Delete)));
+
+            Current_Token := Next_Token
+              (Terminals                 => Shared.Terminals.all,
+               Terminals_Current         => Config.Current_Shared_Token,
+               Restore_Terminals_Current => Restore_Terminals_Current,
+               Insert_Delete             => Config.Insert_Delete,
+               Current_Insert_Delete     => Config.Current_Insert_Delete,
+               Prev_Deleted              => Super.Parser_State 
(Parser_Index).Prev_Deleted);
+
+         when Reduce =>
+            declare
+               Nonterm : Recover_Token;
+            begin
+               Config.Check_Status := Reduce_Stack
+                 (Shared, Config.Stack, Action.Item, Nonterm,
+                  Default_Virtual => Config.Current_Insert_Delete /= 
No_Insert_Delete);
+
+               case Config.Check_Status.Label is
+               when Ok =>
+                  New_State := Config.Stack.Peek.State;
+                  New_State := Goto_For (Table, New_State, 
Action.Item.Production.LHS);
+
+                  if New_State = Unknown_State then
+                     --  Most likely from an inappropriate language fix.
+                     if Trace_McKenzie > Outline then
+                        Base.Put (Trace_Prefix & ": Unknown_State: ", Super, 
Shared, Parser_Index, Config);
+                        Put_Line (Trace, Trace_Prefix & ": stack: " & Image 
(Config.Stack, Descriptor));
+                     end if;
+
+                     --  We can't just return False here; user must abandon 
this config.
+                     raise Bad_Config;
+                  end if;
+
+                  Config.Stack.Push ((New_State, 
Syntax_Trees.Invalid_Node_Index, Nonterm));
+
+               when Semantic_Checks.Error =>
+                  Config.Error_Token       := Nonterm;
+                  Config.Check_Token_Count := Action.Item.Token_Count;
+                  Success                  := False;
+               end case;
+            end;
+
+         when Error =>
+
+            Config.Error_Token :=
+              (ID          => Current_Token.ID,
+               Byte_Region => Current_Token.Byte_Region,
+               others      => <>);
+            Success            := False;
+
+         when Accept_It =>
+            null;
+         end case;
+
+         exit when not Success or
+           Action.Item.Verb = Accept_It or
+           (if Shared_Token_Goal = Invalid_Token_Index
+            then Config.Insert_Delete.Length = 0
+            else Config.Current_Shared_Token > Shared_Token_Goal);
+
+         Action := Action_For (Table, Config.Stack (1).State, 
Current_Token.ID);
+      end loop;
+
+      Config.Current_Shared_Token := Restore_Terminals_Current;
+
+      return Success;
+   end Parse_One_Item;
+
+   function Parse
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Peek_Type;
+      Parse_Items       :    out          Parse_Item_Arrays.Vector;
+      Config            : in              Configuration;
+      Shared_Token_Goal : in              Base_Token_Index;
+      All_Conflicts     : in              Boolean;
+      Trace_Prefix      : in              String)
+     return Boolean
+   is
+      Trace : WisiToken.Trace'Class renames Super.Trace.all;
+
+      Last_Parsed : Natural;
+      Success     : Boolean;
+   begin
+      Parse_Items.Clear;
+      Parse_Items.Append ((Config, Action => null, Parsed => False, 
Shift_Count => 0));
+
+      --  Clear any errors; so they reflect the parse result.
+      Parse_Items (Parse_Items.First_Index).Config.Error_Token.ID := 
Invalid_Token_ID;
+      Parse_Items (Parse_Items.First_Index).Config.Check_Status   := (Label => 
Semantic_Checks.Ok);
+
+      Last_Parsed := Parse_Items.First_Index;
+      loop
+         --  Loop over initial config and any conflicts.
+         Success := Parse_One_Item
+           (Super, Shared, Parser_Index, Parse_Items, Last_Parsed, 
Shared_Token_Goal, Trace_Prefix);
+
+         exit when Parse_Items.Last_Index = Last_Parsed;
+
+         exit when Success and not All_Conflicts;
+
+         Last_Parsed := Last_Parsed + 1;
+         if Trace_McKenzie > Detail then
+            Put_Line (Trace, Super.Label (Parser_Index), Trace_Prefix & ": 
parse conflict");
+         end if;
+      end loop;
+
+      return Success;
+   end Parse;
+
+end WisiToken.Parse.LR.McKenzie_Recover.Parse;
diff --git a/wisitoken-parse-lr-mckenzie_recover-parse.ads 
b/wisitoken-parse-lr-mckenzie_recover-parse.ads
index 0db7178..e5e4d36 100644
--- a/wisitoken-parse-lr-mckenzie_recover-parse.ads
+++ b/wisitoken-parse-lr-mckenzie_recover-parse.ads
@@ -1,77 +1,80 @@
---  Abstract :
---
---  Config parsing subprograms.
---
---  Copyright (C) 2018 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with WisiToken.Parse.LR.McKenzie_Recover.Base;
-private package WisiToken.Parse.LR.McKenzie_Recover.Parse is
-
-   function Reduce_Stack
-     (Shared          : not null access Base.Shared;
-      Stack           : in out          Recover_Stacks.Stack;
-      Action          : in              Reduce_Action_Rec;
-      Nonterm         :    out          Recover_Token;
-      Default_Virtual : in              Boolean)
-     return Semantic_Checks.Check_Status;
-   --  Reduce Stack according to Action, setting Nonterm. If
-   --  Action.Token_Count = 0, set Nonterm.Virtual := Default_Virtual.
-
-   type Parse_Item is record
-      Config      : Configuration;
-      Action      : Parse_Action_Node_Ptr;
-      Parsed      : Boolean;
-      Shift_Count : Natural := 0;
-
-      --  On return from Parse, if Parsed = False, this item was queued by a
-      --  conflict, but not parsed; it should be ignored.
-      --
-      --  Otherwise, if Config.Error_Token.ID = Invalid_Token_ID and
-      --  Config.Check_Status.Label = Ok, Config was parsed successfully to
-      --  the goal.
-      --
-      --  Otherwise, the parser failed a semantic check, or encountered an
-      --  Error action. Shift_Count gives the number of shifts performed. If
-      --  Check_Status.Label is Error, Action.Item.Verb must be Reduce, and
-      --  Config is in the pre-reduce state.
-   end record;
-
-   package Parse_Item_Arrays is new SAL.Gen_Bounded_Definite_Vectors 
(Positive, Parse_Item, Capacity => 10);
-   --  Parse_Item_Arrays.Capacity sets maximum conflicts in one call to Parse
-
-   function Parse
-     (Super             : not null access Base.Supervisor;
-      Shared            : not null access Base.Shared;
-      Parser_Index      : in              SAL.Peek_Type;
-      Parse_Items       :    out          Parse_Item_Arrays.Vector;
-      Config            : in              Configuration;
-      Shared_Token_Goal : in              Base_Token_Index;
-      All_Conflicts     : in              Boolean;
-      Trace_Prefix      : in              String)
-     return Boolean;
-   --  Attempt to parse Config until Config.Inserted is all shifted, and
-   --  either Shared_Token_Goal = Invalid_Token_Index or
-   --  Shared_Token_Goal is shifted.
-   --
-   --  Parsed configs are in Parse_Items; there is more than one if a
-   --  conflict is encountered. Parse returns True if at least one
-   --  Parse_Item parsed successfully to the goal. In that case, the
-   --  other items are either not parsed or failed. See comment in
-   --  Parse_Item for more detail.
-   --
-   --  Raises Bad_Config if parse encounters Unknown_State.
-
-end WisiToken.Parse.LR.McKenzie_Recover.Parse;
+--  Abstract :
+--
+--  Config parsing subprograms.
+--
+--  Copyright (C) 2018 - 2019 Free Software Foundation, Inc.
+--
+--  This library is free software;  you can redistribute it and/or modify it
+--  under 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 library is distributed in the hope that it will be useful,
+--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
+--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+--  As a special exception under Section 7 of GPL version 3, you are granted
+--  additional permissions described in the GCC Runtime Library Exception,
+--  version 3.1, as published by the Free Software Foundation.
+
+pragma License (Modified_GPL);
+
+with WisiToken.Parse.LR.McKenzie_Recover.Base;
+private package WisiToken.Parse.LR.McKenzie_Recover.Parse is
+
+   function Reduce_Stack
+     (Shared          : not null access Base.Shared;
+      Stack           : in out          Recover_Stacks.Stack;
+      Action          : in              Reduce_Action_Rec;
+      Nonterm         :    out          Recover_Token;
+      Default_Virtual : in              Boolean)
+     return Semantic_Checks.Check_Status;
+   --  Reduce Stack according to Action, setting Nonterm. If
+   --  Action.Token_Count = 0, set Nonterm.Virtual := Default_Virtual.
+
+   type Parse_Item is record
+      Config      : Configuration;
+      Action      : Parse_Action_Node_Ptr;
+      Parsed      : Boolean;
+      Shift_Count : Natural := 0;
+
+      --  On return from Parse, if Parsed = False, this item was queued by a
+      --  conflict, but not parsed; it should be ignored.
+      --
+      --  Otherwise, if Config.Error_Token.ID = Invalid_Token_ID and
+      --  Config.Check_Status.Label = Ok, Config was parsed successfully to
+      --  the goal.
+      --
+      --  Otherwise, the parser failed a semantic check, or encountered an
+      --  Error action. Action gives the last action processed. Shift_Count
+      --  gives the number of shifts performed. If Check_Status.Label is
+      --  Error, Action.Item.Verb must be Reduce, and Config is in the
+      --  pre-reduce state.
+   end record;
+
+   package Parse_Item_Arrays is new SAL.Gen_Bounded_Definite_Vectors 
(Positive, Parse_Item, Capacity => 10);
+   --  Parse_Item_Arrays.Capacity sets maximum conflicts in one call to Parse
+
+   function Parse
+     (Super             : not null access Base.Supervisor;
+      Shared            : not null access Base.Shared;
+      Parser_Index      : in              SAL.Peek_Type;
+      Parse_Items       :    out          Parse_Item_Arrays.Vector;
+      Config            : in              Configuration;
+      Shared_Token_Goal : in              Base_Token_Index;
+      All_Conflicts     : in              Boolean;
+      Trace_Prefix      : in              String)
+     return Boolean;
+   --  Attempt to parse Config and any conflict configs. If not
+   --  All_Conflicts, return when Config.Insert_Delete is all processed,
+   --  and either Shared_Token_Goal = Invalid_Token_Index or
+   --  Shared_Token_Goal is shifted. If All_Conflicts, return when all
+   --  conflict configs have been parsed.
+   --
+   --  Parsed configs are in Parse_Items; there is more than one if a
+   --  conflict is encountered. Parse returns True if at least one
+   --  Parse_Item parsed successfully to the goal. In that case, the
+   --  other items are either not parsed or failed. See comment in
+   --  Parse_Item for more detail.
+   --
+   --  Raises Bad_Config if parse encounters Unknown_State.
+
+end WisiToken.Parse.LR.McKenzie_Recover.Parse;
diff --git a/wisitoken-parse-lr-mckenzie_recover.adb 
b/wisitoken-parse-lr-mckenzie_recover.adb
index 31c6285..49c2106 100644
--- a/wisitoken-parse-lr-mckenzie_recover.adb
+++ b/wisitoken-parse-lr-mckenzie_recover.adb
@@ -1,1137 +1,1252 @@
---  Abstract :
---
---  See spec
---
---  Copyright (C) 2017 - 2019 Free Software Foundation, Inc.
---
---  This library is free software;  you can redistribute it and/or modify it
---  under 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 library is distributed in the hope that it will be useful,
---  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
---  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.
---
---  As a special exception under Section 7 of GPL version 3, you are granted
---  additional permissions described in the GCC Runtime Library Exception,
---  version 3.1, as published by the Free Software Foundation.
-
-pragma License (Modified_GPL);
-
-with Ada.Characters.Handling;
-with Ada.Exceptions;
-with Ada.Task_Identification;
-with Ada.Unchecked_Deallocation;
-with GNAT.Traceback.Symbolic;
-with System.Multiprocessors;
-with WisiToken.Parse.LR.McKenzie_Recover.Base;
-with WisiToken.Parse.LR.McKenzie_Recover.Explore;
-with WisiToken.Parse.LR.Parser_Lists;
-package body WisiToken.Parse.LR.McKenzie_Recover is
-   use all type System.Multiprocessors.CPU_Range;
-
-   type Supervisor_Access is access all Base.Supervisor;
-   type Shared_Access is access all Base.Shared;
-
-   task type Worker_Task is
-      entry Start
-        (Super  : in Supervisor_Access;
-         Shared : in Shared_Access);
-      --  Start getting parser/configs to check from Config_Store. Stop when
-      --  Super reports All_Done;
-
-      entry Done;
-      --  Available after Super has reported All_Done.
-   end Worker_Task;
-
-   type Worker_Access is access Worker_Task;
-   procedure Free is new Ada.Unchecked_Deallocation (Worker_Task, 
Worker_Access);
-
-   task body Worker_Task
-   is
-      use all type Base.Config_Status;
-      Super  : Supervisor_Access;
-      Shared : Shared_Access;
-
-      Status : Base.Config_Status := Valid;
-   begin
-      loop
-         select
-            accept Start
-              (Super  : in Supervisor_Access;
-               Shared : in Shared_Access)
-
-            do
-               Worker_Task.Super  := Super;
-               Worker_Task.Shared := Shared;
-            end Start;
-         or
-            terminate;
-         end select;
-
-         loop
-            Explore.Process_One (Super, Shared, Status);
-            exit when Status = All_Done;
-         end loop;
-
-         accept Done;
-
-         Super  := null;
-         Shared := null;
-      end loop;
-
-   exception
-   when E : others =>
-      Super.Fatal (E);
-      if Debug_Mode then
-         Shared.Trace.Put_Line (GNAT.Traceback.Symbolic.Symbolic_Traceback 
(E));
-      end if;
-   end Worker_Task;
-
-   Worker_Tasks : array (1 .. System.Multiprocessors.CPU_Range'Max (1, 
System.Multiprocessors.Number_Of_CPUs - 1)) of
-     Worker_Access;
-   --  Declaring an array of tasks directly causes a circular elaboration
-   --  problem, and would mean a task that terminates due to an exception
-   --  is never restarted.
-
-   function To_Recover
-     (Parser_Stack : in Parser_Lists.Parser_Stacks.Stack;
-      Tree         : in Syntax_Trees.Tree)
-     return Recover_Stacks.Stack
-   is
-      use all type SAL.Base_Peek_Type;
-      Result : Recover_Stacks.Stack;
-      Depth  : constant SAL.Peek_Type := Parser_Stack.Depth;
-   begin
-      Result.Set_Depth (Depth);
-      for I in 1 .. Depth loop
-         declare
-            Item  : Parser_Lists.Parser_Stack_Item renames Parser_Stack (I);
-            Token : constant Recover_Token := (if I = Depth then (others => 
<>) else Tree.Recover_Token (Item.Token));
-         begin
-            Result.Set (I, Depth, (Item.State, Item.Token, Token));
-         end;
-      end loop;
-      return Result;
-   end To_Recover;
-
-   procedure Recover_Init
-     (Shared_Parser : in out LR.Parser.Parser;
-      Parser_State  : in out Parser_Lists.Parser_State)
-   is
-      use all type WisiToken.Parse.LR.Parser.Language_Fixes_Access;
-
-      Trace  : WisiToken.Trace'Class renames Shared_Parser.Trace.all;
-      Config : constant Configuration_Access := 
Parser_State.Recover.Config_Heap.Add (Configuration'(others => <>));
-      Error  : Parse_Error renames Parser_State.Errors 
(Parser_State.Errors.Last);
-   begin
-      Parser_State.Recover.Enqueue_Count := Parser_State.Recover.Enqueue_Count 
+ 1;
-
-      Config.Resume_Token_Goal := Parser_State.Shared_Token + 
Shared_Parser.Table.McKenzie_Param.Check_Limit;
-
-      if Trace_McKenzie > Outline then
-         Trace.New_Line;
-         Trace.Put_Line
-           ("parser" & Integer'Image (Parser_State.Label) &
-              ": State" & State_Index'Image (Parser_State.Stack (1).State) &
-              " Current_Token" & Parser_State.Tree.Image 
(Parser_State.Current_Token, Trace.Descriptor.all) &
-              " Resume_Token_Goal" & Token_Index'Image 
(Config.Resume_Token_Goal));
-         Trace.Put_Line (Image (Error, Parser_State.Tree, 
Trace.Descriptor.all));
-         if Trace_McKenzie > Extra then
-            Put_Line
-              (Trace, Parser_State.Label, Parser_Lists.Image
-                 (Parser_State.Stack, Trace.Descriptor.all, 
Parser_State.Tree));
-         end if;
-      end if;
-
-      --  Additional initialization of Parser_State.Recover is done in
-      --  Supervisor.Initialize.
-
-      Config.Stack := To_Recover (Parser_State.Stack, Parser_State.Tree);
-
-      --  Parser_State.Recover_Insert_Delete must be empty (else we would not 
get
-      --  here). Therefore Parser_State current token is in
-      --  Shared_Parser.Shared_Token.
-
-      Config.Current_Shared_Token := Parser_State.Shared_Token;
-
-      case Error.Label is
-      when Action =>
-         Config.Error_Token := Parser_State.Tree.Recover_Token 
(Error.Error_Token);
-         if Trace_McKenzie > Detail then
-            Put ("enqueue", Trace, Parser_State.Label, 
Shared_Parser.Terminals, Config.all,
-                 Task_ID => False);
-         end if;
-
-      when Check =>
-         if Shared_Parser.Language_Fixes = null then
-            --  The only fix is to ignore the error.
-            if Trace_McKenzie > Detail then
-               Put ("enqueue", Trace, Parser_State.Label, 
Shared_Parser.Terminals, Config.all,
-                    Task_ID => False);
-            end if;
-
-         else
-            --  Undo the reduction that encountered the error, let Process_One
-            --  enqueue possible solutions. We leave the cost at 0, since this 
is
-            --  the root config. Later logic will enqueue the 'ignore error'
-            --  solution; see McKenzie_Recover.Explore Process_One.
-
-            Config.Check_Status      := Error.Check_Status;
-            Config.Error_Token       := Config.Stack (1).Token;
-            Config.Check_Token_Count := Undo_Reduce (Config.Stack, 
Parser_State.Tree);
-
-            Config.Ops.Append ((Undo_Reduce, Config.Error_Token.ID, 
Config.Check_Token_Count));
-
-            if Trace_McKenzie > Detail then
-               Put ("undo_reduce " & Image
-                      (Config.Error_Token.ID, Trace.Descriptor.all), Trace, 
Parser_State.Label,
-                    Shared_Parser.Terminals, Config.all, Task_ID => False);
-            end if;
-         end if;
-
-      when Message =>
-         --  Last error entry should be the failure that caused us to enter
-         --  recovery.
-         raise SAL.Programmer_Error;
-      end case;
-   end Recover_Init;
-
-   function Recover (Shared_Parser : in out LR.Parser.Parser) return 
Recover_Status
-   is
-      use all type Parser.Post_Recover_Access;
-      use all type SAL.Base_Peek_Type;
-      Trace : WisiToken.Trace'Class renames Shared_Parser.Trace.all;
-
-      Parsers : Parser_Lists.List renames Shared_Parser.Parsers;
-
-      Current_Parser : Parser_Lists.Cursor;
-
-      Super : aliased Base.Supervisor
-        (Trace'Access,
-         Cost_Limit        => Shared_Parser.Table.McKenzie_Param.Cost_Limit,
-         Check_Delta_Limit => 
Shared_Parser.Table.McKenzie_Param.Check_Delta_Limit,
-         Enqueue_Limit     => Shared_Parser.Table.McKenzie_Param.Enqueue_Limit,
-         Parser_Count      => Parsers.Count);
-
-      Shared : aliased Base.Shared
-        (Shared_Parser.Trace,
-         Shared_Parser.Lexer.all'Access,
-         Shared_Parser.Table,
-         Shared_Parser.Language_Fixes,
-         Shared_Parser.Language_Use_Minimal_Complete_Actions,
-         Shared_Parser.Language_String_ID_Set,
-         Shared_Parser.Terminals'Access,
-         Shared_Parser.Line_Begin_Token'Access);
-
-      Task_Count : constant System.Multiprocessors.CPU_Range :=
-        (if Shared_Parser.Table.McKenzie_Param.Task_Count = 0
-         then Worker_Tasks'Last
-         --  Keep one CPU free for this main task, and the user.
-         else Shared_Parser.Table.McKenzie_Param.Task_Count);
-
-   begin
-      if Trace_McKenzie > Outline then
-         Trace.New_Line;
-         Trace.Put_Line (" McKenzie error recovery");
-      end if;
-
-      Super.Initialize (Parsers'Unrestricted_Access, 
Shared_Parser.Terminals'Unrestricted_Access);
-
-      for Parser_State of Parsers loop
-         Recover_Init (Shared_Parser, Parser_State);
-      end loop;
-
-      if Trace_McKenzie > Outline then
-         Trace.New_Line;
-         Trace.Put_Line (System.Multiprocessors.CPU_Range'Image 
(Worker_Tasks'Last) & " parallel tasks");
-      end if;
-
-      for I in Worker_Tasks'First .. Task_Count loop
-         if Worker_Tasks (I) = null then
-            Worker_Tasks (I) := new Worker_Task;
-            if Debug_Mode then
-               Trace.Put_Line ("new Worker_Task" & 
System.Multiprocessors.CPU_Range'Image (I));
-            end if;
-
-         elsif Worker_Tasks (I)'Terminated then
-            Free (Worker_Tasks (I));
-            Worker_Tasks (I) := new Worker_Task;
-            if Debug_Mode then
-               Trace.Put_Line ("recreated Worker_Task" & 
System.Multiprocessors.CPU_Range'Image (I));
-            end if;
-         end if;
-
-         Worker_Tasks (I).Start (Super'Unchecked_Access, 
Shared'Unchecked_Access);
-      end loop;
-
-      declare
-         use Ada.Exceptions;
-         ID      : Exception_Id;
-         Message : Ada.Strings.Unbounded.Unbounded_String;
-      begin
-         Super.Done (ID, Message); -- Wait for all parsers to fail or succeed
-
-         --  Ensure all worker tasks stop getting configs before proceeding;
-         --  otherwise local variables disappear while the task is still trying
-         --  to access them.
-         for I in Worker_Tasks'First .. Task_Count loop
-            if not Worker_Tasks (I)'Terminated then
-               Worker_Tasks (I).Done;
-            end if;
-         end loop;
-
-         if ID /= Null_Id then
-            Raise_Exception (ID, -Message);
-         end if;
-      end;
-
-      --  Adjust parser state for each successful recovery.
-      --
-      --  One option here would be to keep only the parser with the least
-      --  cost fix. However, the normal reason for having multiple parsers
-      --  is to resolve a grammar ambiguity; the least cost fix might
-      --  resolve the ambiguity the wrong way. As could any other fix, of
-      --  course.
-
-      --  Spawn new parsers for multiple solutions
-      declare
-         use Parser_Lists;
-         Cur         : Cursor             := Parsers.First;
-         Solutions   : SAL.Base_Peek_Type := 0;
-         Spawn_Limit : SAL.Base_Peek_Type := Shared_Parser.Max_Parallel; -- 
per parser
-      begin
-         for Parser of Parsers loop
-            if Parser.Recover.Success then
-               Solutions := Solutions + Parser.Recover.Results.Count;
-            end if;
-         end loop;
-
-         if Solutions > Shared_Parser.Max_Parallel and Trace_McKenzie > 
Outline then
-            Trace.Put_Line ("too many parallel parsers required in recover; 
dropping some solutions");
-            Spawn_Limit := Shared_Parser.Max_Parallel / Parsers.Count;
-         end if;
-
-         loop
-            declare
-               Data : McKenzie_Data renames State_Ref (Cur).Recover;
-            begin
-               if Data.Success then
-                  if Trace_McKenzie > Outline then
-                     Trace.Put_Line
-                       (Integer'Image (Label (Cur)) &
-                          ": succeed" & SAL.Base_Peek_Type'Image 
(Data.Results.Count) &
-                          ", enqueue" & Integer'Image (Data.Enqueue_Count) &
-                          ", check " & Integer'Image (Data.Check_Count) &
-                          ", cost: " & Integer'Image (Data.Results.Min_Key));
-                  end if;
-
-                  if Data.Results.Count > 1 then
-                     for I in 1 .. SAL.Base_Peek_Type'Min (Spawn_Limit, 
Data.Results.Count - 1) loop
-                        Parsers.Prepend_Copy (Cur); --  does not copy recover
-                        if Trace_McKenzie > Outline or Trace_Parse > Outline 
then
-                           Trace.Put_Line
-                             ("spawn parser" & Integer'Image 
(Parsers.First.Label) & " from " &
-                                Trimmed_Image (Cur.Label) & " (" & 
Trimmed_Image (Integer (Parsers.Count)) &
-                                " active)");
-                           Put ("", Trace, Parsers.First.Label, 
Shared_Parser.Terminals,
-                                Data.Results.Peek, Task_ID => False);
-                        end if;
-
-                        State_Ref (Parsers.First).Recover.Results.Add 
(Data.Results.Remove);
-                        State_Ref (Parsers.First).Recover.Success := True;
-                     end loop;
-                  end if;
-
-                  if Trace_McKenzie > Outline or Trace_Parse > Outline then
-                     Put ("", Trace, Cur.State_Ref.Label, 
Shared_Parser.Terminals, Data.Results.Peek,
-                          Task_ID => False);
-                  end if;
-               else
-                  if Trace_McKenzie > Outline then
-                     Trace.Put_Line
-                       (Integer'Image (Cur.Label) &
-                          ": fail, enqueue" & Integer'Image 
(Data.Enqueue_Count) &
-                          ", check " & Integer'Image (Data.Check_Count) &
-                          ", cost_limit: " & Integer'Image 
(Shared_Parser.Table.McKenzie_Param.Cost_Limit) &
-                          ", max shared_token " & Token_Index'Image 
(Shared_Parser.Terminals.Last_Index));
-                  end if;
-               end if;
-
-            end;
-            Next (Cur);
-            exit when Is_Done (Cur);
-         end loop;
-      end;
-
-      --  Edit Parser_State to apply solutions.
-
-      --  We don't use 'for Parser_State of Parsers loop' here,
-      --  because we might need to terminate a parser.
-      Current_Parser := Parsers.First;
-      loop
-         exit when Current_Parser.Is_Done;
-
-         if Current_Parser.State_Ref.Recover.Success then
-            begin
-               --  Can't have active 'renames State_Ref' when terminate a 
parser
-               declare
-                  use all type Syntax_Trees.Node_Index;
-                  use Parser_Lists;
-
-                  Parser_State : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref;
-
-                  Descriptor : WisiToken.Descriptor renames 
Shared_Parser.Trace.Descriptor.all;
-                  Tree       : Syntax_Trees.Tree renames Parser_State.Tree;
-                  Data       : McKenzie_Data renames Parser_State.Recover;
-                  Result     : Configuration renames Data.Results.Peek;
-
-                  Min_Op_Token_Index        : WisiToken.Token_Index := 
WisiToken.Token_Index'Last;
-                  Min_Push_Back_Token_Index : WisiToken.Token_Index := 
WisiToken.Token_Index'Last;
-
-                  Stack_Matches_Ops     : Boolean := True;
-                  Shared_Token_Changed  : Boolean := False;
-                  Current_Token_Virtual : Boolean := False;
-
-                  Sorted_Insert_Delete : Sorted_Insert_Delete_Arrays.Vector;
-
-                  procedure Apply_Prev_Token
-                  is begin
-                     loop
-                        exit when not Parser_State.Prev_Deleted.Contains 
(Parser_State.Shared_Token);
-                        Parser_State.Shared_Token := Parser_State.Shared_Token 
+ 1;
-                     end loop;
-                  end Apply_Prev_Token;
-
-               begin
-                  --  The verb will be reset by the main parser; just indicate 
the
-                  --  parser recovered from the error.
-                  Parser_State.Set_Verb (Shift);
-
-                  Parser_State.Errors (Parser_State.Errors.Last).Recover := 
Result;
-
-                  Parser_State.Resume_Token_Goal := Result.Resume_Token_Goal;
-
-                  if Trace_McKenzie > Extra then
-                     Put_Line (Trace, Parser_State.Label, "before Ops 
applied:", Task_ID => False);
-                     Put_Line
-                       (Trace, Parser_State.Label, "stack " & Image 
(Parser_State.Stack, Descriptor, Tree),
-                        Task_ID => False);
-                     Put_Line
-                       (Trace, Parser_State.Label, "Shared_Token  " & Image
-                          (Parser_State.Shared_Token, Shared_Parser.Terminals, 
Descriptor),
-                        Task_ID => False);
-                     Put_Line
-                       (Trace, Parser_State.Label, "Current_Token " & 
Parser_State.Tree.Image
-                          (Parser_State.Current_Token, Descriptor),
-                        Task_ID => False);
-                  end if;
-
-                  --  We don't apply all Ops to the parser stack here, because 
that
-                  --  requires updating the syntax tree as well, and we want 
to let the
-                  --  main parser do that, partly as a double check on the 
algorithms
-                  --  here.
-                  --
-                  --  However, the main parser can only apply Insert and 
Delete ops; we
-                  --  must apply Push_Back and Undo_Reduce here. Note that 
Fast_Forward
-                  --  ops are just for bookkeeping.
-                  --
-                  --  In order to apply Undo_Reduce, we also need to apply any 
preceding
-                  --  ops. See test_mckenzie_recover.adb Missing_Name_2 for an 
example
-                  --  of multiple Undo_Reduce. On the other hand, Push_Back 
can be
-                  --  applied without the preceding ops.
-                  --
-                  --  A Push_Back can go back past preceding ops, including 
Undo_Reduce;
-                  --  there's no point in applying ops that are later 
superceded by such
-                  --  a Push_Back. See test_mckenzie_recover.adb 
Out_Of_Order_Ops for an
-                  --  example.
-                  --
-                  --  Push_Back can also go back past a previous error 
recovery; we must
-                  --  apply Parser_State.Prev_Deleted here as well, when 
computing
-                  --  Shared_Token.
-                  --
-                  --  So first we go thru Ops to find the earliest Push_Back. 
Then we
-                  --  apply ops that are before that point, up to the first 
Insert or
-                  --  Fast_Forward. After that, we enqueue Insert and Delete 
ops on
-                  --  Parser_State.Recover_Insert_Delete, in token_index 
order, and any
-                  --  Undo_Reduce are rejected.
-                  --
-                  --  Then the main parser parses the edited input stream.
-                  --
-                  --  There's no need to modify Parser_State.Tree. Any tree 
nodes
-                  --  created by the failed parse that are pushed back are 
useful for
-                  --  error repair, and will just be ignored in future 
parsing. This
-                  --  also avoids enlarging a non-flushed branched tree, which 
saves
-                  --  time and space.
-
-                  for Op of Result.Ops loop
-                     case Op.Op is
-                     when Fast_Forward =>
-                        if Op.FF_Token_Index < Min_Op_Token_Index then
-                           Min_Op_Token_Index := Op.FF_Token_Index;
-                        end if;
-
-                     when Undo_Reduce =>
-                        null;
-
-                     when Push_Back | Insert | Delete =>
-                        if Op.Token_Index /= Invalid_Token_Index then
-                           if Op.Token_Index < Min_Op_Token_Index then
-                              Min_Op_Token_Index := Op.Token_Index;
-                           end if;
-                           if Op.Token_Index < Min_Push_Back_Token_Index then
-                              Min_Push_Back_Token_Index := Op.Token_Index;
-                           end if;
-                        end if;
-
-                     end case;
-                  end loop;
-
-                  for Op of Result.Ops loop
-                     case Op.Op is
-                     when Fast_Forward =>
-                        Stack_Matches_Ops := False;
-
-                     when Undo_Reduce =>
-                        if not Stack_Matches_Ops then
-                           if Trace_McKenzie > Outline then
-                              Put_Line
-                                (Trace, Parser_State.Label, "Undo_Reduce after 
insert or fast_forward",
-                                 Task_ID => False);
-                           end if;
-                           raise Bad_Config;
-                        end if;
-
-                        declare
-                           Item : constant Parser_Lists.Parser_Stack_Item := 
Parser_State.Stack.Pop;
-                        begin
-                           case Tree.Label (Item.Token) is
-                           when Syntax_Trees.Shared_Terminal | 
Syntax_Trees.Virtual_Terminal =>
-                              raise Bad_Config;
-
-                           when Syntax_Trees.Nonterm =>
-                              for C of Tree.Children (Item.Token) loop
-                                 Parser_State.Stack.Push ((Tree.State (C), C));
-                              end loop;
-                           end case;
-                        end;
-
-                     when Push_Back =>
-                        if Stack_Matches_Ops then
-                           Parser_State.Stack.Pop;
-                           if Op.Token_Index /= Invalid_Token_Index then
-                              Parser_State.Shared_Token := Op.Token_Index;
-                              Shared_Token_Changed      := True;
-                           end if;
-
-                        elsif Op.Token_Index = Min_Op_Token_Index then
-                           loop
-                              --  Multiple push_backs can have the same 
Op.Token_Index, so we may
-                              --  already be at the target.
-                              exit when Parser_State.Shared_Token <= 
Op.Token_Index and
-                                Tree.Min_Terminal_Index (Parser_State.Stack 
(1).Token) /= Invalid_Token_Index;
-                              --  also push back empty tokens.
-
-                              declare
-                                 Item : constant 
Parser_Lists.Parser_Stack_Item := Parser_State.Stack.Pop;
-
-                                 Min_Index : constant Base_Token_Index :=
-                                   Parser_State.Tree.Min_Terminal_Index 
(Item.Token);
-                              begin
-                                 if Min_Index /= Invalid_Token_Index then
-                                    Shared_Token_Changed := True;
-                                    Parser_State.Shared_Token := Min_Index;
-                                 end if;
-