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

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

[elpa] externals/wisi d10db37 22/35: Release ada-mode version 6.0. Relea


From: Stefan Monnier
Subject: [elpa] externals/wisi d10db37 22/35: Release ada-mode version 6.0. Release wisi version 2.0
Date: Sat, 28 Nov 2020 14:47:54 -0500 (EST)

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

    Release ada-mode version 6.0. Release wisi version 2.0
---
 NEWS                                               |   22 +-
 README                                             |    6 +-
 build-wisitoken-bnf-generate.sh                    |    9 +
 long_float_elementary_functions.ads                |   21 +
 parse_table-mode.el                                |   64 -
 sal-gen_bounded_definite_vectors-gen_image.adb     |   39 +
 sal-gen_bounded_definite_vectors-gen_image.ads     |   23 +
 sal-gen_bounded_definite_vectors-gen_image_aux.adb |   35 +
 sal-gen_bounded_definite_vectors-gen_image_aux.ads |   23 +
 sal-gen_bounded_definite_vectors-gen_sorted.adb    |   85 +
 sal-gen_bounded_definite_vectors-gen_sorted.ads    |   50 +
 sal-gen_bounded_definite_vectors.adb               |  224 ++
 sal-gen_bounded_definite_vectors.ads               |  150 +
 sal-gen_definite_doubly_linked_lists.adb           |  304 ++
 sal-gen_definite_doubly_linked_lists.ads           |  159 +
 ...finite_doubly_linked_lists_sorted-gen_image.adb |   47 +
 ...finite_doubly_linked_lists_sorted-gen_image.ads |   25 +
 sal-gen_definite_doubly_linked_lists_sorted.adb    |  542 ++++
 sal-gen_definite_doubly_linked_lists_sorted.ads    |  175 ++
 sal-gen_indefinite_doubly_linked_lists.adb         |  201 ++
 sal-gen_indefinite_doubly_linked_lists.ads         |  114 +
 sal-gen_trimmed_image.adb                          |   28 +
 sal-gen_trimmed_image.ads                          |   23 +
 sal-gen_unbounded_definite_min_heaps_fibonacci.adb |  340 +++
 sal-gen_unbounded_definite_min_heaps_fibonacci.ads |  114 +
 ...gen_unbounded_definite_queues-gen_image_aux.adb |   35 +
 ...gen_unbounded_definite_queues-gen_image_aux.ads |   23 +
 sal-gen_unbounded_definite_queues.adb              |   97 +
 sal-gen_unbounded_definite_queues.ads              |  108 +
 sal-gen_unbounded_definite_red_black_trees.adb     |  863 ++++++
 sal-gen_unbounded_definite_red_black_trees.ads     |  181 ++
 ...gen_unbounded_definite_stacks-gen_image_aux.adb |   42 +
 ...gen_unbounded_definite_stacks-gen_image_aux.ads |   27 +
 sal-gen_unbounded_definite_stacks.adb              |  178 ++
 sal-gen_unbounded_definite_stacks.ads              |  133 +
 ...n_unbounded_definite_vectors-gen_comparable.adb |   73 +
 ...n_unbounded_definite_vectors-gen_comparable.ads |   30 +
 sal-gen_unbounded_definite_vectors-gen_image.adb   |   50 +
 sal-gen_unbounded_definite_vectors-gen_image.ads   |   24 +
 ...en_unbounded_definite_vectors-gen_image_aux.adb |   36 +
 ...en_unbounded_definite_vectors-gen_image_aux.ads |   23 +
 sal-gen_unbounded_definite_vectors.adb             |  578 ++++
 sal-gen_unbounded_definite_vectors.ads             |  226 ++
 sal.adb                                            |   32 +
 sal.ads                                            |   75 +
 standard_common.gpr                                |  118 +
 wisi-compat-24.2.el                                |   34 -
 wisi-compile.el                                    |   34 +-
 wisi-elisp-lexer.el                                |  393 +++
 wisi-elisp-parse.el                                | 1682 +++++++++++
 wisi-fringe.el                                     |  146 +
 wisi-parse-common.el                               |  341 +++
 wisi-parse.el                                      |  549 ----
 wisi-process-parse.el                              |  691 +++++
 wisi.adb                                           | 1891 ++++++++++++
 wisi.ads                                           |  666 +++++
 wisi.el                                            | 2940 +++++++++----------
 wisitoken-bnf-generate.adb                         |  522 ++++
 wisitoken-bnf-generate_grammar.adb                 |   86 +
 wisitoken-bnf-generate_packrat.adb                 |  331 +++
 wisitoken-bnf-generate_utils.adb                   |  818 ++++++
 wisitoken-bnf-generate_utils.ads                   |  176 ++
 wisitoken-bnf-output_ada.adb                       |  436 +++
 wisitoken-bnf-output_ada_common.adb                | 1407 +++++++++
 wisitoken-bnf-output_ada_common.ads                |   91 +
 wisitoken-bnf-output_ada_emacs.adb                 | 1519 ++++++++++
 wisitoken-bnf-output_elisp.adb                     |  293 ++
 wisitoken-bnf-output_elisp_common.adb              |  145 +
 wisitoken-bnf-output_elisp_common.ads              |   49 +
 wisitoken-bnf-utils.adb                            |   45 +
 wisitoken-bnf-utils.ads                            |   29 +
 wisitoken-bnf.adb                                  |  337 +++
 wisitoken-bnf.ads                                  |  310 ++
 wisitoken-gen_token_enum.adb                       |  133 +
 wisitoken-gen_token_enum.ads                       |  130 +
 wisitoken-generate-lr-lalr_generate.adb            |  593 ++++
 wisitoken-generate-lr-lalr_generate.ads            |   67 +
 wisitoken-generate-lr-lr1_generate.adb             |  315 ++
 wisitoken-generate-lr-lr1_generate.ads             |   76 +
 wisitoken-generate-lr.adb                          | 1141 ++++++++
 wisitoken-generate-lr.ads                          |  176 ++
 wisitoken-generate-lr1_items.adb                   |  580 ++++
 wisitoken-generate-lr1_items.ads                   |  332 +++
 wisitoken-generate-packrat.adb                     |  247 ++
 wisitoken-generate-packrat.ads                     |   75 +
 wisitoken-generate.adb                             |  495 ++++
 wisitoken-generate.ads                             |  136 +
 wisitoken-lexer-re2c.adb                           |  244 ++
 wisitoken-lexer-re2c.ads                           |  129 +
 wisitoken-lexer-regexp.adb                         |  240 ++
 wisitoken-lexer-regexp.ads                         |  102 +
 wisitoken-lexer.adb                                |   56 +
 wisitoken-lexer.ads                                |  161 ++
 wisitoken-parse-lr-mckenzie_recover-base.adb       |  433 +++
 wisitoken-parse-lr-mckenzie_recover-base.ads       |  180 ++
 wisitoken-parse-lr-mckenzie_recover-explore.adb    | 1301 +++++++++
 wisitoken-parse-lr-mckenzie_recover-explore.ads    |   28 +
 wisitoken-parse-lr-mckenzie_recover-parse.adb      |  302 ++
 wisitoken-parse-lr-mckenzie_recover-parse.ads      |   77 +
 wisitoken-parse-lr-mckenzie_recover.adb            | 1062 +++++++
 wisitoken-parse-lr-mckenzie_recover.ads            |  220 ++
 wisitoken-parse-lr-parser.adb                      | 1105 +++++++
 wisitoken-parse-lr-parser.ads                      |  145 +
 wisitoken-parse-lr-parser_lists.adb                |  405 +++
 wisitoken-parse-lr-parser_lists.ads                |  260 ++
 wisitoken-parse-lr-parser_no_recover.adb           |  492 ++++
 wisitoken-parse-lr-parser_no_recover.ads           |   84 +
 wisitoken-parse-lr.adb                             |  856 ++++++
 wisitoken-parse-lr.ads                             |  624 ++++
 wisitoken-parse-packrat-generated.adb              |   86 +
 wisitoken-parse-packrat-generated.ads              |   70 +
 wisitoken-parse-packrat-procedural.adb             |  251 ++
 wisitoken-parse-packrat-procedural.ads             |   80 +
 wisitoken-parse-packrat.adb                        |   56 +
 wisitoken-parse-packrat.ads                        |   71 +
 wisitoken-parse.adb                                |   88 +
 wisitoken-parse.ads                                |   66 +
 wisitoken-productions.adb                          |   51 +
 wisitoken-productions.ads                          |   64 +
 wisitoken-regexp.adb                               | 1347 +++++++++
 wisitoken-regexp.ads                               |  139 +
 wisitoken-semantic_checks.adb                      |  135 +
 wisitoken-semantic_checks.ads                      |   89 +
 wisitoken-syntax_trees.adb                         | 1114 +++++++
 wisitoken-syntax_trees.ads                         |  411 +++
 wisitoken-text_io_trace.adb                        |   70 +
 wisitoken-text_io_trace.ads                        |   45 +
 wisitoken-wisi_ada.adb                             |  163 ++
 wisitoken-wisi_ada.ads                             |   82 +
 wisitoken.adb                                      |  351 +++
 wisitoken.ads                                      |  432 +++
 wisitoken.gpr                                      |   56 +
 wisitoken_grammar_actions.adb                      |  105 +
 wisitoken_grammar_actions.ads                      |  168 ++
 wisitoken_grammar_main.adb                         |  305 ++
 wisitoken_grammar_main.ads                         |   33 +
 wisitoken_grammar_re2c.c                           | 3025 ++++++++++++++++++++
 wisitoken_grammar_re2c_c.ads                       |   63 +
 wisitoken_grammar_runtime.adb                      |  610 ++++
 wisitoken_grammar_runtime.ads                      |   85 +
 140 files changed, 42135 insertions(+), 2314 deletions(-)

diff --git a/NEWS b/NEWS
index 5e837c7..f86dbe5 100644
--- a/NEWS
+++ b/NEWS
@@ -1,12 +1,32 @@
 GNU Emacs wisi NEWS -- history of user-visible changes.
 
-Copyright (C) 2014 Free Software Foundation, Inc.
+Copyright (C) 2018 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 2.0.0
+17 Nov 2018
+
+** Change indentation engine to compute indent directly in parser actions.
+
+** Add error correcting external process parser.
+
+** Factor out wisi-elisp-lexer.el from wisi.el, and
+   ada-wisi-elisp-parse.el from ada-wisi.el. Move elisp grammar
+   actions from wisi.el to wisi-elisp-parse.el; they are not used with
+   the process parser.
+
+** Add support for error corrections in parser; 'wisi-repair-error'
+   can apply the corrections to the buffer text.
+
+** Display marks in fringe at location of errors detected by
+   background parser.
+
+** include sources for wisitoken parser runtime, parser generator.
+
 * wisi 1.1.6
 3 Oct 2017
 
diff --git a/README b/README
index 648f35c..0435747 100644
--- a/README
+++ b/README
@@ -1,7 +1,7 @@
-Emacs wisi package 1.1.6
+Emacs wisi package 2.0.0
 
 The wisi package provides utilities for using generalized LALR parsers
-to do indentation, fontification, and navigation. See ada-mode for an
-example of its use.
+(in elisp or external processes) to do indentation, fontification, and
+navigation. See ada-mode for an example of its use.
 
 
diff --git a/build-wisitoken-bnf-generate.sh b/build-wisitoken-bnf-generate.sh
new file mode 100644
index 0000000..c087be6
--- /dev/null
+++ b/build-wisitoken-bnf-generate.sh
@@ -0,0 +1,9 @@
+# Build wisitoken-bnf-generate.exe, for generating code from grammar files.
+# 
+# Assumes build.sh has run.
+#
+# Instead of using this, you should consider using the complete
+# wisitoken development tree; see
+# http://stephe-leake.org/ada/wisitoken.html
+
+gprbuild -p -P wisitoken.gpr wisitoken-bnf-generate
diff --git a/long_float_elementary_functions.ads 
b/long_float_elementary_functions.ads
new file mode 100644
index 0000000..3f543b9
--- /dev/null
+++ b/long_float_elementary_functions.ads
@@ -0,0 +1,21 @@
+--  Abstract :
+--
+--  instantiation
+--
+--  Copyright (C) 2017 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);
+
+with Ada.Numerics.Generic_Elementary_Functions;
+package Long_Float_Elementary_Functions is new 
Ada.Numerics.Generic_Elementary_Functions (Long_Float);
diff --git a/parse_table-mode.el b/parse_table-mode.el
deleted file mode 100755
index d8d2651..0000000
--- a/parse_table-mode.el
+++ /dev/null
@@ -1,64 +0,0 @@
-;; parse_table-mode.el --- For navigating in a parse_table as output by 
wisi-generate. -*- lexical-binding:t -*-
-;;
-;; Copyright (C) 2017  Free Software Foundation, Inc.
-;;
-;; Author: Stephen Leake <stephen_leake@stephe-leake.org>
-;; Maintainer: Stephen Leake <stephen_leake@stephe-leake.org>
-;; Keywords: parser
-;; Version: 1.0
-;; package-requires: ((emacs "25.1"))
-;; URL: http://www.nongnu.org/ada-mode/wisi/wisi.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/>.
-
-(require 'xref)
-
-(defun parse_table--xref-backend () 'parse_table)
-
-(cl-defgeneric xref-backend-identifier-completion-table ((_backend (eql 
parse_table)))
-  ;; could complete on nonterms, find productions
-  nil)
-
-(cl-defmethod xref-backend-identifier-at-point ((_backend (eql parse_table)))
-  ;; assume we are on one of:
-  ;; - ’goto state nnn’ in a state action
-  ;; - ’=> State nnn’ in the debug kernels list
-  ;; - ’( nnn)’ in the unknown conflicts list
-  (save-excursion
-    (end-of-line)
-    (when (or (looking-back "[Ss]tate \\([0-9]+\\),?" 
(line-beginning-position))
-             (looking-back "( \\([0-9]+\\))" (line-beginning-position)))
-      (match-string 1))))
-
-(cl-defgeneric xref-backend-definitions ((_backend (eql parse_table)) 
identifier)
-  ;; state tables are self-contained; IDENTIFIER must be a state number
-  (save-excursion
-    (goto-char (point-min))
-    (search-forward-regexp (concat "^State " identifier ":$"))
-    (list (xref-make identifier (xref-make-buffer-location (current-buffer) 
(match-beginning 0))))))
-
-(define-minor-mode parse_table-mode
-  "Provides navigation in wisi-generate parse table output."
-  nil ":parse_table" nil
-  (add-hook 'xref-backend-functions #'parse_table--xref-backend nil t)
-
-  (if parse_table-mode
-      (read-only-mode 0)
-    (read-only-mode 1)
-  ))
-
-(provide 'parse_table-mode)
-;; end of file
diff --git a/sal-gen_bounded_definite_vectors-gen_image.adb 
b/sal-gen_bounded_definite_vectors-gen_image.adb
new file mode 100644
index 0000000..893b178
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors-gen_image.adb
@@ -0,0 +1,39 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+function SAL.Gen_Bounded_Definite_Vectors.Gen_Image (Item : in Vector) return 
String
+is
+   use all type SAL.Base_Peek_Type;
+   use Ada.Strings;
+   use Ada.Strings.Unbounded;
+   Result : Unbounded_String := To_Unbounded_String ("(");
+   Last   : Base_Peek_Type   := To_Peek_Index (Item.Last);
+begin
+   for I in Item.Elements (1 .. Last) loop
+      Result := Result &
+        ((if Trim
+          then Fixed.Trim (Element_Image (Item.Elements (I)), Left)
+          else Element_Image (Item.Elements (I)));
+      if I /= Last then
+         Result := Result & ", ";
+      end if;
+   end loop;
+   Result := Result & ")";
+   return To_String (Result);
+end SAL.Gen_Bounded_Definite_Vectors.Gen_Image;
diff --git a/sal-gen_bounded_definite_vectors-gen_image.ads 
b/sal-gen_bounded_definite_vectors-gen_image.ads
new file mode 100644
index 0000000..6202be0
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors-gen_image.ads
@@ -0,0 +1,23 @@
+--  Abstract :
+--
+--  Image 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
+   with function Element_Image (Item : in Element_Type) return String;
+   Trim : in Boolean;
+function SAL.Gen_Bounded_Definite_Vectors.Gen_Image (Item : in Vector) return 
String;
diff --git a/sal-gen_bounded_definite_vectors-gen_image_aux.adb 
b/sal-gen_bounded_definite_vectors-gen_image_aux.adb
new file mode 100644
index 0000000..39ecd46
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors-gen_image_aux.adb
@@ -0,0 +1,35 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings.Unbounded;
+function SAL.Gen_Bounded_Definite_Vectors.Gen_Image_Aux (Item : in Vector; Aux 
: in Aux_Data) return String
+is
+   use Ada.Strings.Unbounded;
+   Result : Unbounded_String        := To_Unbounded_String ("(");
+   Last   : constant Base_Peek_Type := To_Peek_Index (Item.Last);
+begin
+   for I in 1 .. Last loop
+      Result := Result & Element_Image (Item.Elements (I), Aux);
+      if I /= Last then
+         Result := Result & ", ";
+      end if;
+   end loop;
+   Result := Result & ")";
+   return To_String (Result);
+end SAL.Gen_Bounded_Definite_Vectors.Gen_Image_Aux;
diff --git a/sal-gen_bounded_definite_vectors-gen_image_aux.ads 
b/sal-gen_bounded_definite_vectors-gen_image_aux.ads
new file mode 100644
index 0000000..c72f8ee
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors-gen_image_aux.ads
@@ -0,0 +1,23 @@
+--  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_Bounded_Definite_Vectors.Gen_Image_Aux (Item : in Vector; Aux 
: in Aux_Data) return String;
diff --git a/sal-gen_bounded_definite_vectors-gen_sorted.adb 
b/sal-gen_bounded_definite_vectors-gen_sorted.adb
new file mode 100644
index 0000000..088b807
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors-gen_sorted.adb
@@ -0,0 +1,85 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted is
+
+   overriding procedure Append (Container : in out Vector; New_Item : in 
Element_Type)
+   is begin
+      raise Programmer_Error;
+   end Append;
+
+   overriding procedure Prepend (Container : in out Vector; New_Item : in 
Element_Type)
+   is begin
+      raise Programmer_Error;
+   end Prepend;
+
+   overriding
+   procedure Insert
+     (Container : in out Vector;
+      New_Item  : in     Element_Type;
+      Before    : in     Extended_Index)
+   is begin
+      raise Programmer_Error;
+   end Insert;
+
+   procedure Insert
+     (Container : in out Vector;
+      New_Item  : in     Element_Type)
+   is
+      K : constant Base_Peek_Type := To_Peek_Index (Container.Last);
+      J : Base_Peek_Type := K;
+   begin
+      if K + 1 > Container.Elements'Last then
+         raise Container_Full;
+
+      elsif K = 0 then
+         --  Container empty
+         Container.Last := Container.Last + 1;
+         Container.Elements (1) := New_Item;
+         return;
+      end if;
+
+      loop
+         exit when J < 1;
+
+         case Element_Compare (New_Item, Container.Elements (J)) is
+         when Less =>
+            J := J - 1;
+         when Equal =>
+            --  Insert after J
+            exit;
+         when Greater =>
+            --  Insert after J
+            exit;
+         end case;
+      end loop;
+
+      if J = 0 then
+         --  Insert before all
+         Container.Elements (2 .. K + 1) := Container.Elements (1 .. K);
+         Container.Elements (1) := New_Item;
+      else
+         --  Insert after J
+         Container.Elements (J + 2 .. K + 1) := Container.Elements (J + 1 .. 
K);
+         Container.Elements (J + 1) := New_Item;
+      end if;
+      Container.Last := Container.Last + 1;
+   end Insert;
+
+end SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted;
diff --git a/sal-gen_bounded_definite_vectors-gen_sorted.ads 
b/sal-gen_bounded_definite_vectors-gen_sorted.ads
new file mode 100644
index 0000000..d210c0b
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors-gen_sorted.ads
@@ -0,0 +1,50 @@
+--  Abstract :
+--
+--  Add sorted behavior to 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
+   with function Element_Compare (Left, Right : in Element_Type) return 
Compare_Result;
+package SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted is
+
+   type Vector is new SAL.Gen_Bounded_Definite_Vectors.Vector with null record;
+
+   overriding procedure Append (Container : in out Vector; New_Item : in 
Element_Type)
+   with Inline => True;
+   --  Raises Programmer_Error
+
+   overriding procedure Prepend (Container : in out Vector; New_Item : in 
Element_Type)
+   with Inline => True;
+   --  Raises Programmer_Error
+
+   overriding
+   procedure Insert
+     (Container : in out Vector;
+      New_Item  : in     Element_Type;
+      Before    : in     Extended_Index)
+   with Inline => True;
+   --  Raises Programmer_Error
+
+   not overriding
+   procedure Insert
+     (Container : in out Vector;
+      New_Item  : in     Element_Type);
+   --  Insert New_Item in sorted position. Items are sorted in increasing
+   --  order according to Element_Compare. New_Item is inserted after
+   --  Equal items.
+
+end SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted;
diff --git a/sal-gen_bounded_definite_vectors.adb 
b/sal-gen_bounded_definite_vectors.adb
new file mode 100644
index 0000000..75b4016
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors.adb
@@ -0,0 +1,224 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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);
+
+package body SAL.Gen_Bounded_Definite_Vectors is
+
+   function Length (Container : in Vector) return Ada.Containers.Count_Type
+   is begin
+      --  We assume the type ranges are sensible, so no exceptions occur
+      --  here.
+      return Ada.Containers.Count_Type (Container.Last - Index_Type'First + 1);
+   end Length;
+
+   function Is_Full (Container : in Vector) return Boolean
+   is begin
+      return To_Peek_Index (Container.Last) = Peek_Type (Capacity);
+   end Is_Full;
+
+   procedure Clear (Container : in out Vector)
+   is begin
+      Container.Last := No_Index;
+   end Clear;
+
+   function Element (Container : Vector; Index : Index_Type) return 
Element_Type
+   is begin
+      return Container.Elements (Peek_Type (Index - Index_Type'First + 1));
+   end Element;
+
+   function Last_Index (Container : Vector) return Extended_Index
+   is begin
+      return Container.Last;
+   end Last_Index;
+
+   procedure Set_Last (Container : in out Vector; Last : in Index_Type)
+   is begin
+      Container.Last := Last;
+   end Set_Last;
+
+   procedure Append (Container : in out Vector; New_Item : in Element_Type)
+   is
+      J : constant Peek_Type := To_Peek_Index (Container.Last + 1);
+   begin
+      if J > Container.Elements'Last then
+         raise Container_Full;
+      end if;
+      Container.Elements (J) := New_Item;
+      Container.Last := Container.Last + 1;
+   end Append;
+
+   procedure Prepend (Container : in out Vector; New_Item : in Element_Type)
+   is
+      J : constant Peek_Type := Peek_Type (Container.Last + 1 - 
Index_Type'First + 1);
+   begin
+      if J > Container.Elements'Last then
+         raise Container_Full;
+      end if;
+
+      Container.Elements (2 .. J) := Container.Elements (1 .. J - 1);
+      Container.Elements (1) := New_Item;
+      Container.Last := Container.Last + 1;
+   end Prepend;
+
+   procedure Insert
+     (Container : in out Vector;
+      New_Item  : in     Element_Type;
+      Before    : in     Extended_Index)
+   is
+      J : constant Peek_Type := To_Peek_Index ((if Before = No_Index then 
Container.Last + 1 else Before));
+      K : constant Base_Peek_Type := To_Peek_Index (Container.Last);
+   begin
+      if K + 1 > Container.Elements'Last then
+         raise Container_Full;
+      end if;
+
+      Container.Elements (J + 1 .. K + 1) := Container.Elements (J .. K);
+      Container.Elements (J) := New_Item;
+      Container.Last := Container.Last + 1;
+   end Insert;
+
+   function "+" (Item : in Element_Type) return Vector
+   is begin
+      return Result : Vector do
+         Result.Append (Item);
+      end return;
+   end "+";
+
+   function "&" (Left : in Vector; Right : in Element_Type) return Vector
+   is begin
+      return Result : Vector := Left do
+         Result.Append (Right);
+      end return;
+   end "&";
+
+   procedure Delete_First (Container : in out Vector; Count : in Index_Type := 
1)
+   is
+      J : constant Peek_Type := Peek_Type (Container.Last - Index_Type'First + 
Count);
+   begin
+      if Count > Container.Last then
+         raise Container_Empty;
+      end if;
+      Container.Elements (1 .. J - 1) := Container.Elements (2 .. J);
+      Container.Last := Container.Last - Count;
+   end Delete_First;
+
+   function Constant_Reference (Container : aliased Vector; Index : in 
Index_Type) return Constant_Reference_Type
+   is
+      J : constant Peek_Type := Peek_Type (Index - Index_Type'First + 1);
+   begin
+      if Index > Container.Last then
+         raise Constraint_Error;
+      end if;
+      return (Element => Container.Elements (J)'Access);
+   end Constant_Reference;
+
+   function Variable_Reference
+     (Container : aliased in out Vector;
+      Index     :         in     Index_Type)
+     return Variable_Reference_Type
+   is
+      J : constant Peek_Type := Peek_Type (Index - Index_Type'First + 1);
+   begin
+      if Index > Container.Last then
+         raise Constraint_Error;
+      end if;
+      return (Element => Container.Elements (J)'Access);
+   end Variable_Reference;
+
+   function Has_Element (Position : Cursor) return Boolean is
+   begin
+      if Position.Container = null then
+         return False;
+      end if;
+
+      return Position.Index <= Position.Container.Last;
+   end Has_Element;
+
+   overriding function First (Object : Iterator) return Cursor
+   is begin
+      if Object.Container.Last = No_Index then
+         return (null, Index_Type'First);
+      else
+         return (Object.Container, Object.Container.First_Index);
+      end if;
+   end First;
+
+   overriding function Last  (Object : Iterator) return Cursor
+   is begin
+      if Object.Container.Last = No_Index then
+         return (null, Index_Type'First);
+      else
+         return (Object.Container, Object.Container.Last_Index);
+      end if;
+   end Last;
+
+   overriding function Next
+     (Object   : Iterator;
+      Position : Cursor) return Cursor
+   is begin
+      if Position.Index = Object.Container.Last then
+         return (null, Index_Type'First);
+      else
+         return (Object.Container, Position.Index + 1);
+      end if;
+   end Next;
+
+   overriding function Previous
+     (Object   : Iterator;
+      Position : Cursor) return Cursor
+   is begin
+      if Position.Index = Index_Type'First then
+         return (null, Index_Type'First);
+      else
+         return (Object.Container, Position.Index - 1);
+      end if;
+   end Previous;
+
+   function Iterate (Container : Vector) return 
Vector_Iterator_Interfaces.Reversible_Iterator'Class
+   is begin
+      return Iterator'
+        (Container => Container'Unrestricted_Access,
+         Index     => No_Index);
+   end Iterate;
+
+   function Constant_Reference (Container : aliased Vector; Position : in 
Cursor) return Constant_Reference_Type
+   is
+      J : constant Peek_Type := Peek_Type (Position.Index - Index_Type'First + 
1);
+   begin
+      return (Element => Container.Elements (J)'Access);
+   end Constant_Reference;
+
+   function Variable_Reference
+     (Container : aliased in out Vector;
+      Position  :         in     Cursor)
+     return Variable_Reference_Type
+   is
+      J : constant Peek_Type := Peek_Type (Position.Index - Index_Type'First + 
1);
+   begin
+      return (Element => Container.Elements (J)'Access);
+   end Variable_Reference;
+
+   ----------
+   --  Spec private functions
+
+   function To_Peek_Index (Index : in Extended_Index) return Base_Peek_Type
+   is begin
+      return Base_Peek_Type (Index - Index_Type'First + 1);
+   end To_Peek_Index;
+
+end SAL.Gen_Bounded_Definite_Vectors;
diff --git a/sal-gen_bounded_definite_vectors.ads 
b/sal-gen_bounded_definite_vectors.ads
new file mode 100644
index 0000000..d4c808d
--- /dev/null
+++ b/sal-gen_bounded_definite_vectors.ads
@@ -0,0 +1,150 @@
+--  Abstract :
+--
+--  A simple bounded vector of definite items, intended to be faster
+--  than Ada.Containers.Bounded_Definite_Vectors.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Iterator_Interfaces;
+generic
+   type Index_Type is range <>;
+   type Element_Type is private;
+   Capacity : in Ada.Containers.Count_Type;
+package SAL.Gen_Bounded_Definite_Vectors is
+
+   subtype Extended_Index is Index_Type'Base
+     range Index_Type'First - 1 ..
+           Index_Type'Min (Index_Type'Base'Last - 1, Index_Type'Last) + 1;
+
+   No_Index : constant Extended_Index := Extended_Index'First;
+
+   type Vector is tagged private with
+      Constant_Indexing => Constant_Reference,
+      Variable_Indexing => Variable_Reference,
+      Default_Iterator  => Iterate,
+      Iterator_Element  => Element_Type;
+
+   Empty_Vector : constant Vector;
+
+   function Length (Container : in Vector) return Ada.Containers.Count_Type;
+
+   function Is_Full (Container : in Vector) return Boolean;
+
+   procedure Clear (Container : in out Vector);
+
+   function First_Index (Container : in Vector) return Index_Type is 
(Index_Type'First);
+
+   function Last_Index (Container : in Vector) return Extended_Index;
+   --  No_Index when Container is empty.
+
+   procedure Set_Last (Container : in out Vector; Last : in Index_Type);
+   --  Elements with indices < Last that have not been set are undefined.
+
+   function Element (Container : Vector; Index : Index_Type) return 
Element_Type;
+   --  Index of first element in vector is Index_Type'First.
+
+   procedure Append (Container : in out Vector; New_Item : in Element_Type);
+   --  Raises Container_Full if full (more useful than a precondition failure).
+
+   procedure Prepend (Container : in out Vector; New_Item : in Element_Type);
+   --  Insert New_Item at beginning of Container; current elements slide right.
+
+   procedure Insert
+     (Container : in out Vector;
+      New_Item  : in     Element_Type;
+      Before    : in     Extended_Index);
+   --  Insert New_Item before Before, or after Last_Index if Before is
+   --  No_Index. Current elements at Before and after slide right.
+   --  New_Item then has index Before.
+
+   function "+" (Item : in Element_Type) return Vector;
+   function "&" (Left : in Vector; Right : in Element_Type) return Vector;
+
+   procedure Delete_First (Container : in out Vector; Count : in Index_Type := 
1);
+   --  Remaining elements slide down.
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Constant_Reference (Container : aliased Vector; Index : in 
Index_Type) return Constant_Reference_Type;
+
+   type Variable_Reference_Type (Element : not null access Element_Type) is 
null record
+   with Implicit_Dereference => Element;
+
+   function Variable_Reference
+     (Container : aliased in out Vector;
+      Index     :         in     Index_Type)
+     return Variable_Reference_Type;
+
+   type Cursor is private;
+
+   function Has_Element (Position : Cursor) return Boolean;
+
+   package Vector_Iterator_Interfaces is new Ada.Iterator_Interfaces (Cursor, 
Has_Element);
+
+   function Iterate (Container : Vector) return 
Vector_Iterator_Interfaces.Reversible_Iterator'Class;
+
+   function Constant_Reference (Container : aliased Vector; Position : in 
Cursor) return Constant_Reference_Type;
+
+   function Variable_Reference
+     (Container : aliased in out Vector;
+      Position  :         in     Cursor)
+     return Variable_Reference_Type;
+
+private
+
+   type Array_Type is array (Peek_Type range 1 .. Peek_Type (Capacity)) of 
aliased Element_Type;
+
+   type Vector is tagged
+   record
+      Elements : Array_Type := (others => <>);
+      Last     : Extended_Index := No_Index;
+   end record;
+
+   type Vector_Access is access all Vector;
+   for Vector_Access'Storage_Size use 0;
+
+   type Cursor is record
+      Container : Vector_Access;
+      Index     : Index_Type := Index_Type'First;
+   end record;
+
+   type Iterator is new Vector_Iterator_Interfaces.Reversible_Iterator with
+   record
+      Container : Vector_Access;
+      Index     : Index_Type'Base;
+   end record;
+
+   overriding function First (Object : Iterator) return Cursor;
+   overriding function Last  (Object : Iterator) return Cursor;
+
+   overriding function Next
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+   overriding function Previous
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+   Empty_Vector : constant Vector := (others => <>);
+
+   ----------
+   --  For child units
+
+   function To_Peek_Index (Index : in Extended_Index) return Base_Peek_Type
+   with Inline;
+
+end SAL.Gen_Bounded_Definite_Vectors;
diff --git a/sal-gen_definite_doubly_linked_lists.adb 
b/sal-gen_definite_doubly_linked_lists.adb
new file mode 100644
index 0000000..3855ae6
--- /dev/null
+++ b/sal-gen_definite_doubly_linked_lists.adb
@@ -0,0 +1,304 @@
+--  Abstract :
+--
+--  see spec
+--
+--  Copyright (C) 2017, 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 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 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_Definite_Doubly_Linked_Lists is
+
+   ---------
+   --  Public operations, declaration order.
+
+   overriding
+   procedure Adjust (Container : in out List)
+   is
+      Next_Source : Node_Access := Container.Head;
+      New_Node    : Node_Access;
+   begin
+      if Next_Source = null then
+         return;
+      end if;
+
+      Container.Tail := null;
+
+      loop
+         New_Node := new Node_Type'
+           (Element => Next_Source.Element,
+            Next    => null,
+            Prev    => Container.Tail);
+         if Container.Tail = null then
+            Container.Head := New_Node;
+            Container.Tail := New_Node;
+         else
+            Container.Tail.Next := New_Node;
+            Container.Tail      := New_Node;
+         end if;
+         Next_Source := Next_Source.Next;
+         exit when Next_Source = null;
+      end loop;
+   end Adjust;
+
+   overriding
+   procedure Finalize (Container : in out List)
+   is
+      Next : Node_Access := Container.Head;
+   begin
+      loop
+         exit when Next = null;
+         Next := Container.Head.Next;
+         Free (Container.Head);
+         Container.Head := Next;
+      end loop;
+      Container.Tail := null;
+   end Finalize;
+
+   function Length (Container : in List) return Ada.Containers.Count_Type
+   is begin
+      return Container.Count;
+   end Length;
+
+   procedure Append (Container : in out List; Element : in Element_Type)
+   is
+      use all type Ada.Containers.Count_Type;
+      New_Node : constant Node_Access := new Node_Type'
+        (Element => Element,
+         Prev    => Container.Tail,
+         Next    => null);
+   begin
+      if Container.Tail = null then
+         Container.Head := New_Node;
+         Container.Tail := New_Node;
+      else
+         Container.Tail.Next := New_Node;
+         Container.Tail      := New_Node;
+      end if;
+      Container.Count := Container.Count + 1;
+   end Append;
+
+   procedure Prepend (Container : in out List; Element : in Element_Type)
+   is
+      use all type Ada.Containers.Count_Type;
+      New_Node : constant Node_Access := new Node_Type'
+        (Element => Element,
+         Prev    => null,
+         Next    => Container.Head);
+   begin
+      if Container.Tail = null then
+         Container.Head := New_Node;
+         Container.Tail := New_Node;
+      else
+         Container.Head.Prev := New_Node;
+         Container.Head      := New_Node;
+      end if;
+      Container.Count := Container.Count + 1;
+   end Prepend;
+
+   function To_List (Element : in Element_Type) return List
+   is begin
+      return Result : List do
+         Result.Append (Element);
+      end return;
+   end To_List;
+
+   function Has_Element (Position : in Cursor) return Boolean
+   is begin
+      return Position.Ptr /= null;
+   end Has_Element;
+
+   function First (Container : in List) return Cursor
+   is begin
+      if Container.Head = null then
+         return No_Element;
+      else
+         return (Container'Unrestricted_Access, Container.Head);
+      end if;
+   end First;
+
+   function Last (Container : in List) return Cursor
+   is begin
+      if Container.Tail = null then
+         return No_Element;
+      else
+         return (Container'Unrestricted_Access, Container.Tail);
+      end if;
+   end Last;
+
+   procedure Next (Position : in out Cursor)
+   is begin
+      if Position.Ptr /= null then
+         if Position.Ptr.Next = null then
+            Position := No_Element;
+         else
+            Position.Ptr := Position.Ptr.Next;
+         end if;
+      end if;
+   end Next;
+
+   function Next (Position : in Cursor) return Cursor
+   is begin
+      if Position.Ptr = null then
+         return Position;
+      else
+         if Position.Ptr.Next = null then
+            return No_Element;
+         else
+            return (Position.Container, Position.Ptr.Next);
+         end if;
+      end if;
+   end Next;
+
+   function Previous (Position : in Cursor) return Cursor
+   is begin
+      if Position.Ptr = null then
+         return Position;
+      else
+         if Position.Ptr.Prev = null then
+            return No_Element;
+         else
+            return (Position.Container, Position.Ptr.Prev);
+         end if;
+      end if;
+   end Previous;
+
+   function Element (Position : in Cursor) return Element_Type
+   is begin
+      return Position.Ptr.Element;
+   end Element;
+
+   procedure Delete (Container : in out List; Position : in out Cursor)
+   is
+      use all type Ada.Containers.Count_Type;
+      Node : Node_Access renames Position.Ptr;
+   begin
+      if Node.Next = null then
+         Container.Tail := Node.Prev;
+      else
+         Node.Next.Prev := Node.Prev;
+      end if;
+      if Node.Prev = null then
+         Container.Head := Node.Next;
+      else
+         Node.Prev.Next := Node.Next;
+      end if;
+      Free (Node);
+      Position        := No_Element;
+      Container.Count := Container.Count - 1;
+   end Delete;
+
+   procedure Insert
+     (Container : in out List;
+      Before    : in     Cursor;
+      Element   : in     Element_Type)
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Before = No_Element then
+         Container.Append (Element);
+      else
+         if Before.Ptr = Container.Head then
+            declare
+               --  old list: before ...
+               --  newlist:  new  before ...
+               New_Node : constant Node_Access := new Node_Type'
+                 (Element => Element,
+                  Prev    => null,
+                  Next    => Before.Ptr);
+            begin
+               Before.Ptr.Prev := New_Node;
+               Container.Head  := New_Node;
+            end;
+         else
+            declare
+               --  old list: ... prev  before ...
+               --  newlist:  ... prev  new  before ...
+               New_Node : constant Node_Access := new Node_Type'
+                 (Element => Element,
+                  Prev    => Before.Ptr.Prev,
+                  Next    => Before.Ptr);
+            begin
+               Before.Ptr.Prev.Next := New_Node;
+               Before.Ptr.Prev      := New_Node;
+
+            end;
+         end if;
+         Container.Count := Container.Count + 1;
+      end if;
+   end Insert;
+
+   function Persistent_Ref (Position : in Cursor) return access Element_Type
+   is begin
+      return Position.Ptr.Element'Access;
+   end Persistent_Ref;
+
+   function Constant_Reference (Container : in List; Position : in Cursor) 
return Constant_Reference_Type
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Constant_Reference;
+
+   function Constant_Ref (Position : in Cursor) return Constant_Reference_Type
+   is begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Constant_Ref;
+
+   function Reference (Container : in List; Position : in Cursor) return 
Reference_Type
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Reference;
+
+   function Ref (Position : in Cursor) return Reference_Type
+   is begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Ref;
+
+   function Iterate (Container : aliased in List) return 
Iterator_Interfaces.Reversible_Iterator'Class
+   is begin
+      return Iterator'(Container => Container'Unrestricted_Access);
+   end Iterate;
+
+   overriding function First (Object : Iterator) return Cursor
+   is begin
+      return First (Object.Container.all);
+   end First;
+
+   overriding function Last  (Object : Iterator) return Cursor
+   is begin
+      return Last (Object.Container.all);
+   end Last;
+
+   overriding function Next (Object : in Iterator; Position : in Cursor) 
return Cursor
+   is
+      pragma Unreferenced (Object);
+   begin
+      return Next (Position);
+   end Next;
+
+   overriding function Previous (Object : in Iterator; Position : in Cursor) 
return Cursor
+   is
+      pragma Unreferenced (Object);
+   begin
+      return Previous (Position);
+   end Previous;
+
+end SAL.Gen_Definite_Doubly_Linked_Lists;
diff --git a/sal-gen_definite_doubly_linked_lists.ads 
b/sal-gen_definite_doubly_linked_lists.ads
new file mode 100644
index 0000000..203f9ad
--- /dev/null
+++ b/sal-gen_definite_doubly_linked_lists.ads
@@ -0,0 +1,159 @@
+--  Abstract :
+--
+--  A generic doubly linked list with definite elements, allowing
+--  permanent references to elements.
+--
+--  Copyright (C) 2017, 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 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 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.Finalization;
+with Ada.Iterator_Interfaces;
+with Ada.Unchecked_Deallocation;
+generic
+   type Element_Type is private;
+package SAL.Gen_Definite_Doubly_Linked_Lists is
+
+   type List is new Ada.Finalization.Controlled with private
+   with
+      Constant_Indexing => Constant_Reference,
+      Variable_Indexing => Reference,
+      Default_Iterator  => Iterate,
+      Iterator_Element  => Element_Type;
+
+   type List_Access_Constant is access constant List;
+   for List_Access_Constant'Storage_Size use 0;
+
+   type List_Access is access all List;
+   for List_Access'Storage_Size use 0;
+
+   Empty_List : constant List;
+
+   overriding procedure Adjust (Container : in out List);
+   --  Deep copy.
+
+   overriding procedure Finalize (Container : in out List);
+   --  Free all items in List.
+
+   function Length (Container : in List) return Ada.Containers.Count_Type;
+
+   procedure Append (Container : in out List; Element : in Element_Type);
+
+   procedure Prepend (Container : in out List; Element : in Element_Type);
+
+   function To_List (Element : in Element_Type) return List;
+
+   type Cursor is private;
+
+   No_Element : constant Cursor;
+
+   function Has_Element (Position : in Cursor) return Boolean;
+
+   function First (Container : in List) return Cursor;
+   function Last (Container : in List) return Cursor;
+
+   procedure Next (Position : in out Cursor)
+   with Pre => Position /= No_Element;
+
+   function Next (Position : in Cursor) return Cursor
+   with Pre => Position /= No_Element;
+   function Previous (Position : in Cursor) return Cursor
+   with Pre => Position /= No_Element;
+
+   function Element (Position : in Cursor) return Element_Type
+   with Pre => Position /= No_Element;
+
+   procedure Delete (Container : in out List; Position : in out Cursor)
+   with Pre => Position /= No_Element;
+
+   procedure Insert
+     (Container : in out List;
+      Before    : in     Cursor;
+      Element   : in     Element_Type);
+   --  If Before is No_Element, insert after Last.
+
+   function Persistent_Ref (Position : in Cursor) return access Element_Type
+   with Pre => Position /= No_Element;
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Constant_Reference (Container : in List; Position : in Cursor) 
return Constant_Reference_Type
+   with Pre => Position /= No_Element;
+   function Constant_Ref (Position : in Cursor) return Constant_Reference_Type
+   with Pre => Position /= No_Element;
+
+   type Reference_Type (Element : not null access Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Reference (Container : in List; Position : in Cursor) return 
Reference_Type
+   with Pre => Position /= No_Element;
+   function Ref (Position : in Cursor) return Reference_Type
+   with Pre => Position /= No_Element;
+
+   package Iterator_Interfaces is new Ada.Iterator_Interfaces (Cursor, 
Has_Element);
+
+   function Iterate (Container : aliased in List) return 
Iterator_Interfaces.Reversible_Iterator'Class;
+
+private
+   type Node_Type;
+
+   type Node_Access is access Node_Type;
+
+   type Node_Type is record
+      Element : aliased Element_Type;
+      Prev    : Node_Access;
+      Next    : Node_Access;
+   end record;
+
+   procedure Free is new Ada.Unchecked_Deallocation (Node_Type, Node_Access);
+
+   type List is new Ada.Finalization.Controlled with record
+      Head  : Node_Access               := null;
+      Tail  : Node_Access               := null;
+      Count : Ada.Containers.Count_Type := 0;
+   end record;
+
+   type Cursor is record
+      Container : List_Access;
+      Ptr       : Node_Access;
+   end record;
+
+   Empty_List : constant List := (Ada.Finalization.Controlled with null, null, 
0);
+
+   No_Element : constant Cursor := (null, null);
+
+   type Iterator is new Iterator_Interfaces.Reversible_Iterator with
+   record
+      Container : List_Access;
+   end record;
+
+   overriding function First (Object : Iterator) return Cursor;
+   overriding function Last  (Object : Iterator) return Cursor;
+
+   overriding function Next
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+   overriding function Previous
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+end SAL.Gen_Definite_Doubly_Linked_Lists;
diff --git a/sal-gen_definite_doubly_linked_lists_sorted-gen_image.adb 
b/sal-gen_definite_doubly_linked_lists_sorted-gen_image.adb
new file mode 100644
index 0000000..898a588
--- /dev/null
+++ b/sal-gen_definite_doubly_linked_lists_sorted-gen_image.adb
@@ -0,0 +1,47 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings.Unbounded;
+function SAL.Gen_Definite_Doubly_Linked_Lists_Sorted.Gen_Image
+  (Item : in List; Strict : in Boolean := False) return String
+is
+   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 (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.Gen_Definite_Doubly_Linked_Lists_Sorted.Gen_Image;
diff --git a/sal-gen_definite_doubly_linked_lists_sorted-gen_image.ads 
b/sal-gen_definite_doubly_linked_lists_sorted-gen_image.ads
new file mode 100644
index 0000000..2743f4e
--- /dev/null
+++ b/sal-gen_definite_doubly_linked_lists_sorted-gen_image.ads
@@ -0,0 +1,25 @@
+--  Abstract :
+--
+--  Image 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
+   with function Element_Image (Item : in Element_Type) return String;
+function SAL.Gen_Definite_Doubly_Linked_Lists_Sorted.Gen_Image
+  (Item : in List; Strict : in Boolean := False) return String;
+--  Image of Item, in Ada aggregate syntax. If Strict, use correct
+--  syntax for 0 and 1 item; otherwise, use () and (item).
diff --git a/sal-gen_definite_doubly_linked_lists_sorted.adb 
b/sal-gen_definite_doubly_linked_lists_sorted.adb
new file mode 100644
index 0000000..da99b3e
--- /dev/null
+++ b/sal-gen_definite_doubly_linked_lists_sorted.adb
@@ -0,0 +1,542 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body SAL.Gen_Definite_Doubly_Linked_Lists_Sorted is
+
+   ----------
+   --  Body subprograms, alphabetical
+
+   procedure Find
+     (Container     : in     List;
+      Element       : in     Element_Type;
+      Found         :    out Node_Access;
+      Found_Compare :    out Compare_Result)
+   is
+      --  Return pointer to first item in Container for which Compare (item,
+      --  element) returns True or Greater. If no such element exists, Found
+      --  is null, Found_Compare is Less.
+      use Ada.Containers;
+   begin
+      if Container.Head = null then
+         Found         := null;
+         Found_Compare := Less;
+         return;
+      end if;
+
+      declare
+         Low_Index  : Count_Type  := 1;
+         High_Index : Count_Type  := Container.Count;
+         Next_Node  : Node_Access := Container.Head;
+         Next_Index : Count_Type  := Low_Index;
+         Old_Index  : Count_Type;
+      begin
+         loop
+            Old_Index  := Next_Index;
+            Next_Index := (Low_Index + High_Index) / 2;
+
+            if Next_Index > Old_Index then
+               for I in Old_Index + 1 .. Next_Index loop
+                  Next_Node := Next_Node.Next;
+               end loop;
+            elsif Next_Index < Old_Index then
+               for I in Next_Index .. Old_Index - 1 loop
+                  Next_Node := Next_Node.Prev;
+               end loop;
+            end if;
+
+            case Element_Compare (Next_Node.Element, Element) is
+            when Less =>
+               if Next_Index = High_Index then
+                  --  no more nodes to check
+                  Found         := null;
+                  Found_Compare := Less;
+                  return;
+               elsif Next_Index = Low_Index then
+                  --  force check of high_index
+                  Low_Index := High_Index;
+               else
+                  Low_Index := Next_Index;
+               end if;
+
+            when Equal =>
+               Found         := Next_Node;
+               Found_Compare := Equal;
+               return;
+
+            when Greater =>
+               if Low_Index = Next_Index then
+                  --  no more nodes to check
+                  Found         := Next_Node;
+                  Found_Compare := Greater;
+                  return;
+               elsif High_Index = Next_Index then
+                  --  Desired result is either high_index or low_index
+                  pragma Assert (Low_Index + 1 = High_Index);
+                  case Element_Compare (Next_Node.Prev.Element, Element) is
+                  when Less =>
+                     Found         := Next_Node;
+                     Found_Compare := Greater;
+                     return;
+                  when Equal =>
+                     Found         := Next_Node.Prev;
+                     Found_Compare := Equal;
+                     return;
+                  when Greater =>
+                     Found         := Next_Node.Prev;
+                     Found_Compare := Greater;
+                     return;
+                  end case;
+               else
+                  High_Index := Next_Index;
+               end if;
+            end case;
+         end loop;
+      end;
+   end Find;
+
+   procedure Insert_Before
+     (Container : in out List;
+      Before    : in     Node_Access;
+      Element   : in     Element_Type)
+   is
+      New_Node : constant Node_Access := new Node_Type'
+        (Element => Element,
+         Prev    => Before.Prev,
+         Next    => Before);
+   begin
+      if Before = Container.Head then
+         Before.Prev    := New_Node;
+         Container.Head := New_Node;
+      else
+         Before.Prev.Next := New_Node;
+         Before.Prev      := New_Node;
+      end if;
+   end Insert_Before;
+
+   procedure Insert_After_Tail
+     (Container : in out List;
+      Element   : in     Element_Type)
+   is
+      New_Node : constant Node_Access := new Node_Type'
+        (Element => Element,
+         Prev    => Container.Tail,
+         Next    => null);
+   begin
+      Container.Tail.Next := New_Node;
+      Container.Tail      := New_Node;
+   end Insert_After_Tail;
+
+   ---------
+   --  Public operations, declaration order.
+
+   overriding
+   procedure Adjust (Container : in out List)
+   is
+      Next_Source : Node_Access := Container.Head;
+      New_Node    : Node_Access;
+   begin
+      if Next_Source = null then
+         return;
+      end if;
+
+      Container.Tail := null;
+
+      loop
+         New_Node := new Node_Type'
+           (Element => Next_Source.Element,
+            Next    => null,
+            Prev    => Container.Tail);
+         if Container.Tail = null then
+            Container.Head := New_Node;
+            Container.Tail := New_Node;
+         else
+            Container.Tail.Next := New_Node;
+            Container.Tail      := New_Node;
+         end if;
+         Next_Source := Next_Source.Next;
+         exit when Next_Source = null;
+      end loop;
+   end Adjust;
+
+   overriding
+   procedure Finalize (Container : in out List)
+   is
+      Next : Node_Access := Container.Head;
+   begin
+      loop
+         exit when Next = null;
+         Next := Container.Head.Next;
+         Free (Container.Head);
+         Container.Head := Next;
+      end loop;
+      Container.Tail := null;
+   end Finalize;
+
+   overriding function "=" (Left, Right : in List) return Boolean
+   is
+      Left_I  : Node_Access := Left.Head;
+      Right_I : Node_Access := Right.Head;
+   begin
+      loop
+         exit when Left_I = null;
+
+         if Right_I = null then
+            return False;
+         elsif Left_I.Element /= Right_I.Element then
+            return False;
+         end if;
+
+         Left_I  := Left_I.Next;
+         Right_I := Right_I.Next;
+      end loop;
+      return Right_I = null;
+   end "=";
+
+   function Length (Container : in List) return Ada.Containers.Count_Type
+   is begin
+      return Container.Count;
+   end Length;
+
+   function To_List (Element : in Element_Type) return List
+   is
+      New_Node : constant Node_Access := new Node_Type'
+        (Element => Element,
+         Prev    => null,
+         Next    => null);
+   begin
+      return Result : constant List :=
+        (Ada.Finalization.Controlled with
+         Head  => New_Node,
+         Tail  => New_Node,
+         Count => 1);
+   end To_List;
+
+   procedure Insert (Container : in out List; Element : in Element_Type)
+   is
+      Node    : Node_Access := Container.Head;
+      Compare : Compare_Result;
+   begin
+      if Node = null then
+         Container := To_List (Element);
+      else
+         Find (Container, Element, Node, Compare);
+
+         Container.Count := Container.Count + 1;
+
+         if Node = null then
+            Insert_After_Tail (Container, Element);
+         else
+            Insert_Before (Container, Node, Element);
+         end if;
+      end if;
+   end Insert;
+
+   function Contains (Container : in List; Element : in Element_Type) return 
Boolean
+   is
+      Node    : Node_Access := Container.Head;
+      Compare : Compare_Result;
+   begin
+      Find (Container, Element, Node, Compare);
+      return Compare = Equal;
+   end Contains;
+
+   procedure Merge
+     (Target : in out List;
+      Source : in     List;
+      Added  :    out Boolean)
+   is
+      Target_I : Node_Access := Target.Head;
+      Source_I : Node_Access := Source.Head;
+   begin
+      if Target_I = null then
+         if Source_I = null then
+            Added := False;
+         else
+            Target.Head  := Source.Head;
+            Target.Tail  := Source.Tail;
+            Target.Count := Source.Count;
+            Adjust (Target);
+
+            Added := True;
+         end if;
+
+      elsif Source_I = null then
+         Added := False;
+
+      else
+         Added := False;
+         loop
+            exit when Source_I = null;
+
+            if Target_I = null then
+               Added := True;
+               Target.Count := Target.Count + 1;
+               Insert_After_Tail (Target, Source_I.Element);
+               Source_I := Source_I.Next;
+
+            else
+               case Element_Compare (Target_I.Element, Source_I.Element) is
+               when Greater =>
+                  Added := True;
+                  Target.Count := Target.Count + 1;
+                  Insert_Before (Target, Target_I, Source_I.Element);
+                  Source_I := Source_I.Next;
+
+               when Equal =>
+                  Target_I := Target_I.Next;
+                  Source_I := Source_I.Next;
+
+               when Less =>
+                  Target_I := Target_I.Next;
+               end case;
+            end if;
+         end loop;
+      end if;
+   end Merge;
+
+   procedure Merge
+     (Target  : in out List;
+      Source  : in     List;
+      Added   :    out Boolean;
+      Exclude : in     Element_Type)
+   is
+      Target_I : Node_Access := Target.Head;
+      Source_I : Node_Access := Source.Head;
+   begin
+      Added := False;
+
+      if Target_I = null then
+         if Source_I = null then
+            return;
+         else
+            loop
+               if Source_I = null then
+                  return;
+               end if;
+               exit when Source_I.Element /= Exclude;
+               Source_I := Source_I.Next;
+            end loop;
+
+            Added    := True;
+            Target   := To_List (Source_I.Element);
+            Source_I := Source_I.Next;
+         end if;
+      end if;
+
+      loop
+         exit when Source_I = null;
+
+         if Source_I.Element = Exclude then
+            Source_I := Source_I.Next;
+
+         elsif Target_I = null then
+            Added := True;
+            Target.Count := Target.Count + 1;
+            Insert_After_Tail (Target, Source_I.Element);
+            Source_I := Source_I.Next;
+
+         else
+            case Element_Compare (Target_I.Element, Source_I.Element) is
+            when Greater =>
+               Added := True;
+               Target.Count := Target.Count + 1;
+               Insert_Before (Target, Target_I, Source_I.Element);
+               Source_I := Source_I.Next;
+
+            when Equal =>
+               Target_I := Target_I.Next;
+               Source_I := Source_I.Next;
+
+            when Less =>
+               Target_I := Target_I.Next;
+            end case;
+         end if;
+      end loop;
+   end Merge;
+
+   function Has_Element (Position : in Cursor) return Boolean
+   is begin
+      return Position.Ptr /= null;
+   end Has_Element;
+
+   function First (Container : in List) return Cursor
+   is begin
+      if Container.Head = null then
+         return No_Element;
+      else
+         return (Container'Unrestricted_Access, Container.Head);
+      end if;
+   end First;
+
+   function Last (Container : in List) return Cursor
+   is begin
+      if Container.Tail = null then
+         return No_Element;
+      else
+         return (Container'Unrestricted_Access, Container.Tail);
+      end if;
+   end Last;
+
+   function Find (Container : in List; Element : in Element_Type) return Cursor
+   is
+      Node    : Node_Access;
+      Compare : Compare_Result;
+   begin
+      Find (Container, Element, Node, Compare);
+
+      if Node = null then
+         return No_Element;
+      elsif Compare = Equal then
+         return (Container'Unrestricted_Access, Node);
+      else
+         return No_Element;
+      end if;
+   end Find;
+
+   procedure Next (Position : in out Cursor)
+   is begin
+      if Position.Ptr /= null then
+         if Position.Ptr.Next = null then
+            Position := No_Element;
+         else
+            Position.Ptr := Position.Ptr.Next;
+         end if;
+      end if;
+   end Next;
+
+   function Next (Position : in Cursor) return Cursor
+   is begin
+      if Position.Ptr = null then
+         return Position;
+      else
+         if Position.Ptr.Next = null then
+            return No_Element;
+         else
+            return (Position.Container, Position.Ptr.Next);
+         end if;
+      end if;
+   end Next;
+
+   function Previous (Position : in Cursor) return Cursor
+   is begin
+      if Position.Ptr = null then
+         return Position;
+      else
+         if Position.Ptr.Prev = null then
+            return No_Element;
+         else
+            return (Position.Container, Position.Ptr.Prev);
+         end if;
+      end if;
+   end Previous;
+
+   function Element (Position : in Cursor) return Element_Type
+   is begin
+      return Position.Ptr.Element;
+   end Element;
+
+   procedure Delete (Container : in out List; Position : in out Cursor)
+   is
+      Node : Node_Access renames Position.Ptr;
+   begin
+      if Node.Next = null then
+         Container.Tail := Node.Prev;
+      else
+         Node.Next.Prev := Node.Prev;
+      end if;
+      if Node.Prev = null then
+         Container.Head := Node.Next;
+      else
+         Node.Prev.Next := Node.Next;
+      end if;
+      Free (Node);
+      Position        := No_Element;
+      Container.Count := Container.Count - 1;
+   end Delete;
+
+   function Pop (Container : in out List) return Element_Type
+   is
+      Node : Node_Access := Container.Head;
+   begin
+      return Result : constant Element_Type := Container.Head.Element do
+         Container.Head := Node.Next;
+         if Node.Next = null then
+            Container.Tail := null;
+         else
+            Node.Next.Prev := null;
+         end if;
+         Free (Node);
+         Container.Count := Container.Count - 1;
+      end return;
+   end Pop;
+
+   function Constant_Reference (Container : in List; Position : in Cursor) 
return Constant_Reference_Type
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Constant_Reference;
+
+   function Constant_Ref (Position : in Cursor) return Constant_Reference_Type
+   is begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Constant_Ref;
+
+   function Reference (Container : in List; Position : in Cursor) return 
Reference_Type
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Reference;
+
+   function Ref (Position : in Cursor) return Reference_Type
+   is begin
+      return (Element => Position.Ptr.all.Element'Access);
+   end Ref;
+
+   function Iterate (Container : aliased in List) return 
Iterator_Interfaces.Reversible_Iterator'Class
+   is begin
+      return Iterator'(Container => Container'Unrestricted_Access);
+   end Iterate;
+
+   overriding function First (Object : Iterator) return Cursor
+   is begin
+      return First (Object.Container.all);
+   end First;
+
+   overriding function Last  (Object : Iterator) return Cursor
+   is begin
+      return Last (Object.Container.all);
+   end Last;
+
+   overriding function Next (Object : in Iterator; Position : in Cursor) 
return Cursor
+   is
+      pragma Unreferenced (Object);
+   begin
+      return Next (Position);
+   end Next;
+
+   overriding function Previous (Object : in Iterator; Position : in Cursor) 
return Cursor
+   is
+      pragma Unreferenced (Object);
+   begin
+      return Previous (Position);
+   end Previous;
+
+end SAL.Gen_Definite_Doubly_Linked_Lists_Sorted;
diff --git a/sal-gen_definite_doubly_linked_lists_sorted.ads 
b/sal-gen_definite_doubly_linked_lists_sorted.ads
new file mode 100644
index 0000000..464ff86
--- /dev/null
+++ b/sal-gen_definite_doubly_linked_lists_sorted.ads
@@ -0,0 +1,175 @@
+--  Abstract :
+--
+--  A generic sorted doubly linked list with definite elements.
+--
+--  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);
+
+with Ada.Finalization;
+with Ada.Iterator_Interfaces;
+with Ada.Unchecked_Deallocation;
+generic
+   type Element_Type is private;
+   with function Element_Compare (Left, Right : in Element_Type) return 
Compare_Result;
+package SAL.Gen_Definite_Doubly_Linked_Lists_Sorted is
+   use all type Ada.Containers.Count_Type;
+
+   type List is new Ada.Finalization.Controlled with private
+   with
+      Constant_Indexing => Constant_Reference,
+      Variable_Indexing => Reference,
+      Default_Iterator  => Iterate,
+      Iterator_Element  => Element_Type;
+
+   --  If user uses Variable_Indexing, they must not change the sort
+   --  order of the elements.
+
+   type List_Access is access all List;
+   for List_Access'Storage_Size use 0;
+
+   Empty_List : constant List;
+
+   overriding procedure Adjust (Container : in out List);
+   --  Deep copy.
+
+   overriding procedure Finalize (Container : in out List);
+   --  Free all items in List.
+
+   overriding function "=" (Left, Right : in List) return Boolean;
+   --  True if contents are the same.
+
+   function Length (Container : in List) return Ada.Containers.Count_Type;
+
+   function To_List (Element : in Element_Type) return List;
+
+   procedure Insert (Container : in out List; Element : in Element_Type);
+   --  Insert Element before first item for which Element_Order (item,
+   --  element) returns True.
+
+   function Contains (Container : in List; Element : in Element_Type) return 
Boolean;
+
+   procedure Merge
+     (Target : in out List;
+      Source : in     List;
+      Added  :    out Boolean);
+   --  Add all elements of Source to Target, if they are not already
+   --  present.
+   --
+   --  Added is True if any element was not already present.
+
+   procedure Merge
+     (Target  : in out List;
+      Source  : in     List;
+      Added   :    out Boolean;
+      Exclude : in     Element_Type);
+   --  Add all elements of Source to Target, if they are not already
+   --  present, and are not equal to Exclude.
+   --
+   --  Added is True if any element was not already present.
+
+   type Cursor is private;
+
+   No_Element : constant Cursor;
+
+   function Has_Element (Position : in Cursor) return Boolean;
+
+   function First (Container : in List) return Cursor;
+   function Last (Container : in List) return Cursor;
+
+   function Find (Container : in List; Element : in Element_Type) return 
Cursor;
+   --  No_Element if Element not found.
+
+   procedure Next (Position : in out Cursor)
+   with Pre => Position /= No_Element;
+
+   function Next (Position : in Cursor) return Cursor
+   with Pre => Position /= No_Element;
+   function Previous (Position : in Cursor) return Cursor
+   with Pre => Position /= No_Element;
+
+   function Element (Position : in Cursor) return Element_Type
+   with Pre => Position /= No_Element;
+
+   procedure Delete (Container : in out List; Position : in out Cursor)
+   with Pre => Position /= No_Element;
+
+   function Pop (Container : in out List) return Element_Type
+   with Pre => Container.Length > 0;
+   --  Return Container.First, delete it from Container.
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Constant_Reference (Container : in List; Position : in Cursor) 
return Constant_Reference_Type;
+   function Constant_Ref (Position : in Cursor) return Constant_Reference_Type;
+
+   type Reference_Type (Element : not null access Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Reference (Container : in List; Position : in Cursor) return 
Reference_Type
+   with Pre => Position /= No_Element;
+   function Ref (Position : in Cursor) return Reference_Type
+   with Pre => Position /= No_Element;
+   --  User must not change the element in a way that affects the sort order.
+
+   package Iterator_Interfaces is new Ada.Iterator_Interfaces (Cursor, 
Has_Element);
+
+   function Iterate (Container : aliased in List) return 
Iterator_Interfaces.Reversible_Iterator'Class;
+
+private
+   type Node_Type;
+
+   type Node_Access is access Node_Type;
+
+   type Node_Type is record
+      Element : aliased Element_Type;
+      Prev    : Node_Access;
+      Next    : Node_Access;
+   end record;
+
+   procedure Free is new Ada.Unchecked_Deallocation (Node_Type, Node_Access);
+
+   type List is new Ada.Finalization.Controlled with record
+      Head  : Node_Access               := null;
+      Tail  : Node_Access               := null;
+      Count : Ada.Containers.Count_Type := 0;
+   end record;
+
+   type Cursor is record
+      Container : List_Access;
+      Ptr       : Node_Access;
+   end record;
+
+   Empty_List : constant List := (Ada.Finalization.Controlled with null, null, 
0);
+
+   No_Element : constant Cursor := (null, null);
+
+   type Iterator is new Iterator_Interfaces.Reversible_Iterator with
+   record
+      Container : List_Access;
+   end record;
+
+   overriding function First (Object : Iterator) return Cursor;
+   overriding function Last  (Object : Iterator) return Cursor;
+
+   overriding function Next
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+   overriding function Previous
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+end SAL.Gen_Definite_Doubly_Linked_Lists_Sorted;
diff --git a/sal-gen_indefinite_doubly_linked_lists.adb 
b/sal-gen_indefinite_doubly_linked_lists.adb
new file mode 100644
index 0000000..079e736
--- /dev/null
+++ b/sal-gen_indefinite_doubly_linked_lists.adb
@@ -0,0 +1,201 @@
+--  Abstract :
+--
+--  see spec
+--
+--  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 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 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_Indefinite_Doubly_Linked_Lists is
+
+   ---------
+   --  Public operations, declaration order.
+
+   overriding
+   procedure Adjust (Container : in out List)
+   is
+      Source   : Node_Access := Container.Head;
+      New_Node : Node_Access;
+   begin
+      if Source = null then
+         return;
+      end if;
+
+      Container.Tail := null;
+
+      loop
+         New_Node := new Node_Type'
+           (Element => new Element_Type'(Source.Element.all),
+            Next    => null,
+            Prev    => Container.Tail);
+         if Container.Tail = null then
+            Container.Head := New_Node;
+            Container.Tail := New_Node;
+         else
+            Container.Tail.Next := New_Node;
+            Container.Tail      := New_Node;
+         end if;
+         Source := Source.Next;
+         exit when Source = null;
+      end loop;
+   end Adjust;
+
+   overriding
+   procedure Finalize (Container : in out List)
+   is
+      Next : Node_Access := Container.Head;
+   begin
+      loop
+         exit when Next = null;
+         Next := Container.Head.Next;
+         Free (Container.Head.Element);
+         Free (Container.Head);
+         Container.Head := Next;
+      end loop;
+      Container.Tail := null;
+   end Finalize;
+
+   function Length (Container : in List) return SAL.Base_Peek_Type
+   is begin
+      return Container.Count;
+   end Length;
+
+   procedure Append (Container : in out List; Element : in Element_Type)
+   is
+      New_Node : constant Node_Access := new Node_Type'
+        (Element => new Element_Type'(Element),
+         Prev    => Container.Tail,
+         Next    => null);
+   begin
+      if Container.Tail = null then
+         Container.Head := New_Node;
+         Container.Tail := New_Node;
+      else
+         Container.Tail.Next := New_Node;
+         Container.Tail      := New_Node;
+      end if;
+      Container.Count := Container.Count + 1;
+   end Append;
+
+   procedure Prepend (Container : in out List; Element : in Element_Type)
+   is
+      New_Node : constant Node_Access := new Node_Type'
+        (Element => new Element_Type'(Element),
+         Prev    => null,
+         Next    => Container.Head);
+   begin
+      if Container.Tail = null then
+         Container.Head := New_Node;
+         Container.Tail := New_Node;
+      else
+         Container.Head.Prev := New_Node;
+         Container.Head      := New_Node;
+      end if;
+      Container.Count := Container.Count + 1;
+   end Prepend;
+
+   function Has_Element (Position : in Cursor) return Boolean
+   is begin
+      return Position.Ptr /= null;
+   end Has_Element;
+
+   function First (Container : in List) return Cursor
+   is begin
+      if Container.Head = null then
+         return No_Element;
+      else
+         return (Container'Unrestricted_Access, Container.Head);
+      end if;
+   end First;
+
+   procedure Next (Position : in out Cursor)
+   is begin
+      if Position.Ptr /= null then
+         if Position.Ptr.Next = null then
+            Position := No_Element;
+         else
+            Position.Ptr := Position.Ptr.Next;
+         end if;
+      end if;
+   end Next;
+
+   function Next (Position : in Cursor) return Cursor
+   is begin
+      if Position.Ptr = null then
+         return Position;
+      else
+         if Position.Ptr.Next = null then
+            return No_Element;
+         else
+            return (Position.Container, Position.Ptr.Next);
+         end if;
+      end if;
+   end Next;
+
+   function Element (Position : in Cursor) return Element_Type
+   is begin
+      return Position.Ptr.Element.all;
+   end Element;
+
+   procedure Delete (Container : in out List; Position : in out Cursor)
+   is
+      Node : Node_Access renames Position.Ptr;
+   begin
+      if Node.Next = null then
+         Container.Tail := Node.Prev;
+      else
+         Node.Next.Prev := Node.Prev;
+      end if;
+      if Node.Prev = null then
+         Container.Head := Node.Next;
+      else
+         Node.Prev.Next := Node.Next;
+      end if;
+      Free (Node.Element);
+      Free (Node);
+      Position        := No_Element;
+      Container.Count := Container.Count - 1;
+   end Delete;
+
+   function Persistent_Ref (Position : in Cursor) return access Element_Type
+   is begin
+      return Position.Ptr.Element;
+   end Persistent_Ref;
+
+   function Constant_Reference (Position : in Cursor) return 
Constant_Reference_Type
+   is begin
+      return (Element => Position.Ptr.all.Element);
+   end Constant_Reference;
+
+   function Constant_Ref (Container : in List'Class; Position : in Peek_Type) 
return Constant_Reference_Type
+   is
+      Ptr : Node_Access := Container.Head;
+   begin
+      for I in 2 .. Position loop
+         Ptr := Ptr.Next;
+      end loop;
+      return (Element => Ptr.all.Element);
+   end Constant_Ref;
+
+   function Reference (Position : in Cursor) return Reference_Type
+   is begin
+      return (Element => Position.Ptr.all.Element);
+   end Reference;
+
+end SAL.Gen_Indefinite_Doubly_Linked_Lists;
diff --git a/sal-gen_indefinite_doubly_linked_lists.ads 
b/sal-gen_indefinite_doubly_linked_lists.ads
new file mode 100644
index 0000000..f477f2d
--- /dev/null
+++ b/sal-gen_indefinite_doubly_linked_lists.ads
@@ -0,0 +1,114 @@
+--  Abstract :
+--
+--  A generic doubly linked list with indefinite elements, allowing
+--  permanent references to elements.
+--
+--  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 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 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.Finalization;
+with Ada.Unchecked_Deallocation;
+generic
+   type Element_Type (<>) is private;
+package SAL.Gen_Indefinite_Doubly_Linked_Lists is
+
+   type List is new Ada.Finalization.Controlled with private;
+
+   Empty_List : constant List;
+
+   overriding procedure Adjust (Container : in out List);
+   --  Deep copy.
+
+   overriding procedure Finalize (Container : in out List);
+   --  Free all items in List.
+
+   function Length (Container : in List) return Base_Peek_Type;
+
+   procedure Append (Container : in out List; Element : in Element_Type);
+
+   procedure Prepend (Container : in out List; Element : in Element_Type);
+
+   type Cursor is private;
+
+   No_Element : constant Cursor;
+
+   function Has_Element (Position : in Cursor) return Boolean;
+
+   function First (Container : in List) return Cursor;
+
+   procedure Next (Position : in out Cursor);
+
+   function Next (Position : in Cursor) return Cursor;
+
+   function Element (Position : in Cursor) return Element_Type
+   with Pre => Has_Element (Position);
+
+   procedure Delete (Container : in out List; Position : in out Cursor)
+   with Pre => Has_Element (Position);
+
+   function Persistent_Ref (Position : in Cursor) return access Element_Type
+   with Pre => Has_Element (Position);
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Constant_Reference (Position : in Cursor) return 
Constant_Reference_Type
+   with Pre => Has_Element (Position);
+
+   function Constant_Ref (Container : in List'Class; Position : in Peek_Type) 
return Constant_Reference_Type
+   with Pre => Position <= Container.Length;
+
+   type Reference_Type (Element : not null access Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Reference (Position : in Cursor) return Reference_Type
+   with Pre => Has_Element (Position);
+
+private
+   type Node_Type;
+   type Node_Access is access Node_Type;
+   type Element_Access is access Element_Type;
+
+
+   type Node_Type is record
+      Element : Element_Access;
+      Prev    : Node_Access;
+      Next    : Node_Access;
+   end record;
+
+   procedure Free is new Ada.Unchecked_Deallocation (Node_Type, Node_Access);
+   procedure Free is new Ada.Unchecked_Deallocation (Element_Type, 
Element_Access);
+
+   type List is new Ada.Finalization.Controlled with record
+      Head  : Node_Access        := null;
+      Tail  : Node_Access        := null;
+      Count : SAL.Base_Peek_Type := 0;
+   end record;
+
+   type Cursor is record
+      Container : access List;
+      Ptr       : Node_Access;
+   end record;
+
+   Empty_List : constant List := (Ada.Finalization.Controlled with null, null, 
0);
+
+   No_Element : constant Cursor := (null, null);
+
+end SAL.Gen_Indefinite_Doubly_Linked_Lists;
diff --git a/sal-gen_trimmed_image.adb b/sal-gen_trimmed_image.adb
new file mode 100644
index 0000000..41fb042
--- /dev/null
+++ b/sal-gen_trimmed_image.adb
@@ -0,0 +1,28 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings;
+with Ada.Strings.Fixed;
+function SAL.Gen_Trimmed_Image (Item : in Element_Type) return String
+is
+   use Ada.Strings;
+   use Ada.Strings.Fixed;
+begin
+   return Trim (Element_Type'Image (Item), Both);
+end SAL.Gen_Trimmed_Image;
diff --git a/sal-gen_trimmed_image.ads b/sal-gen_trimmed_image.ads
new file mode 100644
index 0000000..faf782f
--- /dev/null
+++ b/sal-gen_trimmed_image.ads
@@ -0,0 +1,23 @@
+--  Abstract :
+--
+--  Generic trimmed image.
+--
+--  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 Element_Type is range <>;
+function SAL.Gen_Trimmed_Image (Item : in Element_Type) return String;
+--  Return image of Item with no leading space.
diff --git a/sal-gen_unbounded_definite_min_heaps_fibonacci.adb 
b/sal-gen_unbounded_definite_min_heaps_fibonacci.adb
new file mode 100644
index 0000000..75b9478
--- /dev/null
+++ b/sal-gen_unbounded_definite_min_heaps_fibonacci.adb
@@ -0,0 +1,340 @@
+--  Abstract:
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Unchecked_Deallocation;
+with Long_Float_Elementary_Functions;
+package body SAL.Gen_Unbounded_Definite_Min_Heaps_Fibonacci is
+
+   ----------
+   --  local subprogram specs (as needed), alphabetical order
+
+   procedure Insert_Into_Root_List (Heap : in out Heap_Type; X : in 
Node_Access);
+
+   procedure Link (Y, X : in Node_Access);
+
+   procedure Remove_From_List (X : in Node_Access);
+
+   procedure Swap (A, B : in out Node_Access);
+
+   ----------
+   --  local subprogram bodies, alphabetical order
+
+   function Add (Heap : in out Heap_Type; Item : in Element_Type) return 
Node_Access
+   is
+      X : constant Node_Access := new Node'(Item, null, null, null, null, 0, 
False);
+   begin
+      --  [1] 19.2 FIB-HEAP-INSERT
+      if Heap.Min = null then
+         Heap.Min       := X;
+         Heap.Min.Left  := Heap.Min;
+         Heap.Min.Right := Heap.Min;
+      else
+         Insert_Into_Root_List (Heap, X);
+
+         if Key (Item) < Key (Heap.Min.Element) then
+            Heap.Min := X;
+         end if;
+      end if;
+      Heap.Count := Heap.Count + 1;
+
+      return X;
+   end Add;
+
+   procedure Consolidate (Heap : in out Heap_Type)
+   is
+      --  [1] 19.4 max degree of Fibonacci heap
+      Phi : constant := 1.61803398874989484820458683436563811772; -- 
https://oeis.org/A001622/constant
+      Max_Degree : constant Integer := Integer
+        (Long_Float_Elementary_Functions.Log (Long_Float (Heap.Count), Base => 
Phi));
+
+      --  [1] 19.2 CONSOLIDATE
+      A : array (0 .. Max_Degree) of Node_Access := (others => null);
+
+      W    : Node_Access := Heap.Min;
+      Last : Node_Access := Heap.Min;
+      X, Y : Node_Access;
+      D    : Integer;
+
+      Min_Key : Key_Type;
+   begin
+      loop
+         X := W;
+         W := W.Right;
+
+         D := X.Degree;
+
+         loop
+            exit when A (D) = null;
+
+            Y := A (D);
+            if Key (Y.Element) < Key (X.Element) then
+               Swap (X, Y);
+            end if;
+            if Y = Last and W /= Last then
+               Last := Y.Right;
+            end if;
+            Link (Y, X);
+            A (D) := null;
+            D := D + 1;
+            exit when D = A'Last;
+         end loop;
+
+         A (D) := X;
+
+         exit when W = Last;
+      end loop;
+
+      Heap.Min := null;
+      for I in A'Range loop
+         if A (I) /= null then
+            if Heap.Min = null then
+               Heap.Min       := A (I);
+               Heap.Min.Left  := Heap.Min;
+               Heap.Min.Right := Heap.Min;
+               Min_Key        := Key (Heap.Min.Element);
+            else
+               Insert_Into_Root_List (Heap, A (I));
+               if Key (A (I).Element) < Min_Key then
+                  Heap.Min := A (I);
+                  Min_Key := Key (A (I).Element);
+               end if;
+            end if;
+         end if;
+      end loop;
+   end Consolidate;
+
+   procedure Copy_Node (Old_Obj : in Node_Access; New_Obj : in out Heap_Type)
+   is
+      Child : Node_Access;
+   begin
+      if Old_Obj = null then
+         return;
+      end if;
+
+      if Old_Obj.Child /= null then
+         Child := Old_Obj.Child;
+
+         loop
+            Add (New_Obj, Child.Element);
+            Child := Child.Right;
+            exit when Child = Old_Obj.Child;
+         end loop;
+      end if;
+
+      Add (New_Obj, Old_Obj.Element);
+   end Copy_Node;
+
+   procedure Free is new Ada.Unchecked_Deallocation (Node, Node_Access);
+
+   procedure Free_Node (Item : in out Node_Access)
+   is
+      Child : Node_Access;
+      Temp  : Node_Access;
+   begin
+      if Item = null then
+         return;
+      end if;
+
+      --  Parent has already been free'd
+      --  Siblings are freed by caller
+
+      --  Free children
+      if Item.Child /= null then
+         Child := Item.Child;
+
+         loop
+            Temp  := Child;
+            Child := Child.Right;
+            Free_Node (Temp);
+            exit when Child = Item.Child;
+         end loop;
+      end if;
+      Free (Item);
+   end Free_Node;
+
+   procedure Insert_Into_Root_List (Heap : in out Heap_Type; X : in 
Node_Access)
+   is begin
+      --  match [1] fig 19.3
+      X.Right             := Heap.Min;
+      X.Left              := Heap.Min.Left;
+      Heap.Min.Left.Right := X;
+      Heap.Min.Left       := X;
+   end Insert_Into_Root_List;
+
+   procedure Link (Y, X : in Node_Access)
+   is begin
+      --  [1] 19.2 FIB-HEAP-LINK
+      Remove_From_List (Y);
+      Y.Parent := X;
+      X.Degree := X.Degree + 1;
+      if X.Child = null then
+         X.Child := Y;
+         Y.Right := Y;
+         Y.Left  := Y;
+      else
+         --  Insert Y into X child list
+         Y.Right            := X.Child;
+         Y.Left             := X.Child.Left;
+         X.Child.Left.Right := Y;
+         X.Child.Left       := Y;
+      end if;
+      Y.Mark := False;
+   end Link;
+
+   procedure Remove_From_List (X : in Node_Access)
+   is begin
+      X.Left.Right := X.Right;
+      X.Right.Left := X.Left;
+   end Remove_From_List;
+
+   procedure Swap (A, B : in out Node_Access)
+   is
+      C : constant Node_Access := A;
+   begin
+      A := B;
+      B := C;
+   end Swap;
+
+   ----------
+   --  Visible operations
+
+   overriding
+   procedure Initialize (Object : in out Heap_Type)
+   is begin
+      --  Min is null by default.
+      Object.Count := 0;
+   end Initialize;
+
+   overriding
+   procedure Finalize (Object : in out Heap_Type)
+   is
+      Next : Node_Access := Object.Min;
+      Temp : Node_Access;
+   begin
+      if Next = null then
+         return;
+      end if;
+
+      loop
+         Temp := Next;
+         Next := Next.Right;
+         Free_Node (Temp);
+         exit when Next = Object.Min;
+      end loop;
+      Object.Min   := null;
+      Object.Count := 0;
+   end Finalize;
+
+   overriding
+   procedure Adjust (Object : in out Heap_Type)
+   is
+      Old_Obj : Node_Access := Object.Min;
+      Last : constant Node_Access := Old_Obj;
+   begin
+      if Old_Obj = null then
+         return;
+      end if;
+
+      Object.Min := null;
+      Object.Count := 0;
+
+      loop
+         Copy_Node (Old_Obj, Object);
+         Old_Obj := Old_Obj.Right;
+         exit when Old_Obj = Last;
+      end loop;
+   end Adjust;
+
+   procedure Clear (Heap : in out Heap_Type)
+   is begin
+      Finalize (Heap);
+   end Clear;
+
+   function Count (Heap : in Heap_Type) return Base_Peek_Type
+   is begin
+      return Heap.Count;
+   end Count;
+
+   function Remove (Heap : in out Heap_Type) return Element_Type
+   is
+      Z           : Node_Access := Heap.Min;
+      Child, Temp : Node_Access;
+   begin
+      if Heap.Count = 0 then
+         raise Container_Empty;
+      end if;
+
+      --  [1] 19.2 FIB-HEAP-EXTRACT-MIN
+      Child := Z.Child;
+      for I in 1 .. Z.Degree loop
+         Temp        := Child;
+         Child       := Child.Right;
+         Temp.Parent := null;
+         Insert_Into_Root_List (Heap, Temp);
+      end loop;
+
+      Remove_From_List (Z);
+
+      if Z.Right = Z then
+         Heap.Min := null;
+      else
+         Heap.Min := Z.Right;
+         Consolidate (Heap);
+      end if;
+      Heap.Count := Heap.Count - 1;
+
+      return Result : constant Element_Type := Z.Element do
+         Free (Z);
+      end return;
+   end Remove;
+
+   function Min_Key (Heap : in out Heap_Type) return Key_Type
+   is begin
+      return Key (Heap.Min.Element);
+   end Min_Key;
+
+   procedure Drop (Heap : in out Heap_Type)
+   is
+      Junk : Element_Type := Remove (Heap);
+      pragma Unreferenced (Junk);
+   begin
+      null;
+   end Drop;
+
+   procedure Add (Heap : in out Heap_Type; Item : in Element_Type)
+   is
+      X : constant Node_Access := Add (Heap, Item);
+      pragma Unreferenced (X);
+   begin
+      null;
+   end Add;
+
+   function Add (Heap : in out Heap_Type; Item : in Element_Type) return 
Element_Access
+   is
+      X : constant Node_Access := Add (Heap, Item);
+   begin
+      return X.all.Element'Access;
+   end Add;
+
+   function Peek (Heap : in Heap_Type) return Constant_Reference_Type
+   is begin
+      return (Element => Heap.Min.all.Element'Access);
+   end Peek;
+
+end SAL.Gen_Unbounded_Definite_Min_Heaps_Fibonacci;
diff --git a/sal-gen_unbounded_definite_min_heaps_fibonacci.ads 
b/sal-gen_unbounded_definite_min_heaps_fibonacci.ads
new file mode 100644
index 0000000..381bd35
--- /dev/null
+++ b/sal-gen_unbounded_definite_min_heaps_fibonacci.ads
@@ -0,0 +1,114 @@
+--  Abstract:
+--
+--  An unbounded minimum Fibonacci heap of definite non-limited elements.
+--
+--  References:
+--
+--  [1] Introduction to Algorithms, Third Edition. Thomas H. Cormen,
+--  Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Chapter 19.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Finalization;
+generic
+   type Element_Type is private;
+   type Element_Access is access all Element_Type;
+   type Key_Type is private;
+   with function Key (Item : in Element_Type) return Key_Type;
+   with procedure Set_Key (Item : in out Element_Type; Key : in Key_Type);
+   pragma Unreferenced (Set_Key); -- needed for Decrease_Key
+   with function "<" (Left, Right : in Key_Type) return Boolean is <>;
+package SAL.Gen_Unbounded_Definite_Min_Heaps_Fibonacci is
+
+   type Heap_Type is new Ada.Finalization.Controlled with private;
+
+   Empty_Heap : constant Heap_Type;
+
+   overriding
+   procedure Initialize (Object : in out Heap_Type);
+
+   overriding
+   procedure Finalize (Object : in out Heap_Type);
+
+   overriding
+   procedure Adjust (Object : in out Heap_Type);
+
+   procedure Clear (Heap : in out Heap_Type);
+   --  Empty Heap.
+
+   function Count (Heap : in Heap_Type) return Base_Peek_Type;
+   --  Return count of elements in Heap.
+
+   function Remove (Heap : in out Heap_Type) return Element_Type;
+   --  Remove minimum element in Heap, return it.
+
+   function Min_Key (Heap : in out Heap_Type) return Key_Type;
+   --  Return a copy of the minimum key value.
+
+   function Get (Heap : in out Heap_Type) return Element_Type renames Remove;
+
+   procedure Drop (Heap : in out Heap_Type);
+   --  Remove minimum element in Heap, discard it.
+
+   procedure Add (Heap : in out Heap_Type; Item : in Element_Type);
+   --  Add Item to Heap.
+
+   procedure Insert (Heap : in out Heap_Type; Item : in Element_Type) renames 
Add;
+
+   function Add (Heap : in out Heap_Type; Item : in Element_Type) return 
Element_Access;
+   --  Add Item to Heap, return a pointer to it. This avoids extra
+   --  copying of Item.
+   --
+   --  Result is valid at least until next Get.
+
+   --  Despite being called a "mergeable heap" in [1], there is no
+   --  algorithm for merging two Fibonacci heaps. And the naive method of
+   --  simply splicing the root lists apparently breaks the consolidate
+   --  algorithm; it assumes there can only be one tree of each degree >
+   --  0.
+
+   --  procedure Increase_Key (Heap : in out Heap_Type; index : in index_type; 
Item : in Element_Type);
+   --  IMPROVEME: implement. need Index (heap, Key), or Add return index.
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Peek (Heap : in Heap_Type) return Constant_Reference_Type;
+   --  Return a constant reference to the min element.
+
+private
+
+   type Node;
+   type Node_Access is access Node;
+
+   type Node is record
+      Element : aliased Element_Type;
+      Parent  : Node_Access;
+      Child   : Node_Access;
+      Left    : Node_Access;
+      Right   : Node_Access;
+      Degree  : Natural;
+      Mark    : Boolean;
+   end record;
+
+   type Heap_Type is new Ada.Finalization.Controlled with record
+      Min   : Node_Access;
+      Count : Base_Peek_Type;
+   end record;
+
+   Empty_Heap : constant Heap_Type := (Ada.Finalization.Controlled with Min => 
null, Count => 0);
+
+end SAL.Gen_Unbounded_Definite_Min_Heaps_Fibonacci;
diff --git a/sal-gen_unbounded_definite_queues-gen_image_aux.adb 
b/sal-gen_unbounded_definite_queues-gen_image_aux.adb
new file mode 100644
index 0000000..233aa1b
--- /dev/null
+++ b/sal-gen_unbounded_definite_queues-gen_image_aux.adb
@@ -0,0 +1,35 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings.Unbounded;
+function SAL.Gen_Unbounded_Definite_Queues.Gen_Image_Aux (Item : in Queue; Aux 
: in Aux_Data) return String
+is
+   use Ada.Strings.Unbounded;
+   Result : Unbounded_String        := To_Unbounded_String ("(");
+   Last   : constant Base_Peek_Type := Item.Count;
+begin
+   for I in 1 .. Last loop
+      Result := Result & Element_Image (Item.Peek (I), Aux);
+      if I /= Last then
+         Result := Result & ", ";
+      end if;
+   end loop;
+   Result := Result & ")";
+   return To_String (Result);
+end SAL.Gen_Unbounded_Definite_Queues.Gen_Image_Aux;
diff --git a/sal-gen_unbounded_definite_queues-gen_image_aux.ads 
b/sal-gen_unbounded_definite_queues-gen_image_aux.ads
new file mode 100644
index 0000000..6ea7c8f
--- /dev/null
+++ b/sal-gen_unbounded_definite_queues-gen_image_aux.ads
@@ -0,0 +1,23 @@
+--  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_Queues.Gen_Image_Aux (Item : in Queue; Aux 
: in Aux_Data) return String;
diff --git a/sal-gen_unbounded_definite_queues.adb 
b/sal-gen_unbounded_definite_queues.adb
new file mode 100644
index 0000000..f71fbe5
--- /dev/null
+++ b/sal-gen_unbounded_definite_queues.adb
@@ -0,0 +1,97 @@
+--  Abstract:
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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);
+
+package body SAL.Gen_Unbounded_Definite_Queues is
+
+   procedure Clear (Queue : in out Pkg.Queue)
+   is begin
+      Queue.Data.Clear;
+   end Clear;
+
+   function Count (Queue : in Pkg.Queue) return Base_Peek_Type
+   is begin
+      return Base_Peek_Type (Queue.Data.Length);
+   end Count;
+
+   function Is_Empty (Queue : in Pkg.Queue) return Boolean
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      return Queue.Data.Length = 0;
+   end Is_Empty;
+
+   function Remove (Queue : in out Pkg.Queue) return Element_Type
+   is
+      use Element_Lists;
+   begin
+      return A : constant Element_Type := Element (Queue.Data.First) do
+         Queue.Data.Delete_First;
+      end return;
+   end Remove;
+
+   procedure Drop (Queue : in out Pkg.Queue)
+   is begin
+      Queue.Data.Delete_First;
+   end Drop;
+
+   function Peek (Queue : in Pkg.Queue; N : Peek_Type := 1) return 
Constant_Reference_Type
+   is
+      use Ada.Containers;
+      use Element_Lists;
+      I : Cursor := Queue.Data.First;
+   begin
+      if Count_Type (N) > Queue.Data.Length then
+         raise Parameter_Error;
+      end if;
+
+      for K in 2 .. N loop
+         Next (I);
+      end loop;
+
+      return (Element => Element_Lists.Constant_Reference (Queue.Data, 
I).Element, Dummy => 1);
+   end Peek;
+
+   function Variable_Peek (Queue : in out Pkg.Queue; N : Peek_Type := 1) 
return Variable_Reference_Type
+   is
+      use Ada.Containers;
+      use Element_Lists;
+      I : Cursor := Queue.Data.First;
+   begin
+      if Count_Type (N) > Queue.Data.Length then
+         raise Parameter_Error;
+      end if;
+
+      for K in 2 .. N loop
+         Next (I);
+      end loop;
+
+      return (Element => Element_Lists.Reference (Queue.Data, I).Element, 
Dummy => 1);
+   end Variable_Peek;
+
+   procedure Add (Queue : in out Pkg.Queue; Item : in Element_Type)
+   is begin
+      Queue.Data.Append (Item);
+   end Add;
+
+   procedure Add_To_Head (Queue : in out Pkg.Queue; Item : in Element_Type)
+   is begin
+      Queue.Data.Prepend (Item);
+   end Add_To_Head;
+
+end SAL.Gen_Unbounded_Definite_Queues;
diff --git a/sal-gen_unbounded_definite_queues.ads 
b/sal-gen_unbounded_definite_queues.ads
new file mode 100644
index 0000000..d891f9d
--- /dev/null
+++ b/sal-gen_unbounded_definite_queues.ads
@@ -0,0 +1,108 @@
+--  Abstract:
+--
+--  An unbounded queue of definite non-limited elements.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Containers.Doubly_Linked_Lists;
+generic
+   type Element_Type is private;
+package SAL.Gen_Unbounded_Definite_Queues is
+
+   package Pkg renames SAL.Gen_Unbounded_Definite_Queues;
+
+   type Queue is tagged private;
+
+   Empty_Queue : constant Queue;
+
+   procedure Clear (Queue : in out Pkg.Queue);
+   --  Empty Queue.
+
+   function Count (Queue : in Pkg.Queue) return Base_Peek_Type;
+   --  Return count of items in the Queue
+
+   function Length (Queue : in Pkg.Queue) return Base_Peek_Type renames Count;
+
+   function Is_Empty (Queue : in Pkg.Queue) return Boolean;
+   --  Return true if no items are in Pkg.Queue.
+
+   function Is_Full (Queue : in Pkg.Queue) return Boolean is (False);
+   --  Return true if Queue is full.
+
+   function Remove (Queue : in out Pkg.Queue) return Element_Type;
+   --  Remove head/front item from Queue, return it.
+   --
+   --  Raise Container_Empty if Is_Empty.
+
+   function Get (Queue : in out Pkg.Queue) return Element_Type renames Remove;
+
+   procedure Drop (Queue : in out Pkg.Queue);
+   --  Remove head/front item from Queue, discard it.
+   --
+   --  Raise Container_Empty if Is_Empty.
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is private
+   with
+      Implicit_Dereference => Element;
+
+   function Peek (Queue : in Pkg.Queue; N : Peek_Type := 1) return 
Constant_Reference_Type;
+   --  Return a constant reference to a queue item. N = 1 is the queue
+   --  head.
+   --
+   --  Raise Parameter_Error if N > Count
+
+   type Variable_Reference_Type (Element : not null access Element_Type) is 
private
+   with Implicit_Dereference => Element;
+
+   function Variable_Peek (Queue : in out Pkg.Queue; N : Peek_Type := 1) 
return Variable_Reference_Type;
+   --  Return a variable reference to a queue item. N = 1 is the queue
+   --  head.
+   --
+   --  Raises Parameter_Error if N > Count
+
+   procedure Add (Queue : in out Pkg.Queue; Item : in Element_Type);
+   --  Add Element to the tail/back of Queue.
+
+   procedure Put (Queue : in out Pkg.Queue; Item : in Element_Type) renames 
Add;
+
+   procedure Add_To_Head (Queue : in out Pkg.Queue; Item : in Element_Type);
+   --  Add Element to the head/front of Queue.
+
+private
+
+   package Element_Lists is new Ada.Containers.Doubly_Linked_Lists 
(Element_Type);
+
+   --  We don't provide cursors or write access to queue elements, so we
+   --  don't need any tampering checks.
+
+   type Queue is tagged record
+      Data : Element_Lists.List;
+      --  Add at Tail/Back = Last, remove at Head/Front = First.
+   end record;
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is
+   record
+      Dummy : Integer := raise Program_Error with "uninitialized reference";
+   end record;
+
+   type Variable_Reference_Type (Element : not null access Element_Type) is
+   record
+      Dummy : Integer := raise Program_Error with "uninitialized reference";
+   end record;
+
+   Empty_Queue : constant Queue := (Data => Element_Lists.Empty_List);
+
+end SAL.Gen_Unbounded_Definite_Queues;
diff --git a/sal-gen_unbounded_definite_red_black_trees.adb 
b/sal-gen_unbounded_definite_red_black_trees.adb
new file mode 100644
index 0000000..2d84c57
--- /dev/null
+++ b/sal-gen_unbounded_definite_red_black_trees.adb
@@ -0,0 +1,863 @@
+--  Abstract :
+--
+--  Generic unbounded red-black tree with definite elements.
+--
+--  Copyright (C) 2017, 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);
+
+package body SAL.Gen_Unbounded_Definite_Red_Black_Trees is
+
+   --  Local declarations (alphabetical order)
+
+   function Count_Tree (Item : in Node_Access; Nil : in Node_Access) return 
Ada.Containers.Count_Type
+   with Pre => Nil /= null;
+
+   procedure Delete_Fixup (T : in out Tree; X : in out Node_Access);
+
+   function Find (Root : in Node_Access; Key : in Key_Type; Nil : in 
Node_Access) return Node_Access
+   with Pre => Nil /= null;
+
+   procedure Left_Rotate (Tree : in out Pkg.Tree; X : in Node_Access)
+   with Pre => X /= null;
+
+   procedure Right_Rotate (Tree : in out Pkg.Tree; X : in Node_Access)
+   with Pre => X /= null;
+
+   procedure Transplant (T : in out Pkg.Tree; U, V : in Node_Access)
+   with Pre => U /= null and T.Root /= null;
+
+   ----------
+   --  local bodies (alphabetical order)
+
+   function Count_Tree (Item : in Node_Access; Nil : in Node_Access) return 
Ada.Containers.Count_Type
+   is
+      use all type Ada.Containers.Count_Type;
+      Result : Ada.Containers.Count_Type := 0;
+   begin
+      if Item.Left /= Nil then
+         Result := Result + Count_Tree (Item.Left, Nil);
+      end if;
+
+      if Item.Right /= Nil then
+         Result := Result + Count_Tree (Item.Right, Nil);
+      end if;
+
+      return Result + 1;
+   end Count_Tree;
+
+   procedure Delete_Fixup (T : in out Tree; X : in out Node_Access)
+   is
+      W : Node_Access;
+   begin
+      --  [1] 13.3 RB-Delete-Fixup
+      --  X is either "doubly black" or "red and black"
+      --  X.Parent is set, even if X = Nil.
+      --  In all cases, Nil.Left = null.Right = null.
+
+      while X /= T.Root and X.Color = Black loop
+         if X = X.Parent.Left then
+            W := X.Parent.Right;
+            if W.Color = Red then
+               W.Color        := Black;
+               X.Parent.Color := Red;
+               Left_Rotate (T, X.Parent);
+               W              := X.Parent.Right;
+            end if;
+
+            if W.Left.Color = Black and W.Right.Color = Black then
+               W.Color := Red;
+               X := X.Parent;
+            else
+               if W.Right.Color = Black then
+                  W.Left.Color := Black;
+                  W.Color      := Red;
+                  Right_Rotate (T, W);
+                  W            := X.Parent.Right;
+               end if;
+               W.Color        := X.Parent.Color;
+               X.Parent.Color := Black;
+               W.Right.Color  := Black;
+               Left_Rotate (T, X.Parent);
+               X              := T.Root;
+            end if;
+         else
+            W := X.Parent.Left;
+            if W.Color = Red then
+               W.Color        := Black;
+               X.Parent.Color := Red;
+               Right_Rotate (T, X.Parent);
+               W              := X.Parent.Left;
+            end if;
+
+            if W.Right.Color = Black and W.Left.Color = Black then
+               W.Color := Red;
+               X       := X.Parent;
+            else
+               if W.Left.Color = Black then
+                  W.Right.Color := Black;
+                  W.Color       := Red;
+                  Left_Rotate (T, W);
+                  W             := X.Parent.Left;
+               end if;
+               W.Color        := X.Parent.Color;
+               X.Parent.Color := Black;
+               W.Left.Color   := Black;
+               Right_Rotate (T, X.Parent);
+               X              := T.Root;
+            end if;
+         end if;
+      end loop;
+      X.Color := Black;
+   end Delete_Fixup;
+
+   function Find (Root : in Node_Access; Key : in Key_Type; Nil : in 
Node_Access) return Node_Access
+   is
+      Node : Node_Access := Root;
+   begin
+      while Node /= Nil loop
+         case Key_Compare (Key, Pkg.Key (Node.Element)) is
+         when Equal =>
+            return Node;
+         when Less =>
+            Node := Node.Left;
+         when Greater =>
+            Node := Node.Right;
+         end case;
+      end loop;
+      return null;
+   end Find;
+
+   procedure Free_Tree (Item : in out Node_Access; Nil : in Node_Access)
+   is begin
+      if Item = Nil or Item = null then
+         raise Programmer_Error;
+      end if;
+
+      if Item.Left /= Nil then
+         Free_Tree (Item.Left, Nil);
+      end if;
+
+      if Item.Right /= Nil then
+         Free_Tree (Item.Right, Nil);
+      end if;
+
+      Free (Item);
+   end Free_Tree;
+
+   procedure Insert_Fixup (Tree : in out Pkg.Tree; Z : in out Node_Access)
+   is
+      --  [1] 13.3 RB-Insert-Fixup (T, z)
+      Nil : Node_Access renames Tree.Nil;
+      Y   : Node_Access;
+   begin
+      while Z.Parent /= Nil and then Z.Parent.Color = Red loop
+         if Z.Parent = Z.Parent.Parent.Left then
+            Y := Z.Parent.Parent.Right;
+            if Y /= Nil and then Y.Color = Red then
+               Z.Parent.Color        := Black;
+               Y.Color               := Black;
+               Z.Parent.Parent.Color := Red;
+               Z                     := Z.Parent.Parent;
+            else
+               if Z = Z.Parent.Right then
+                  Z := Z.Parent;
+                  Left_Rotate (Tree, Z);
+               end if;
+               Z.Parent.Color        := Black;
+               Z.Parent.Parent.Color := Red;
+               Right_Rotate (Tree, Z.Parent.Parent);
+            end if;
+         else
+            Y := Z.Parent.Parent.Left;
+            if Y /= Nil and then Y.Color = Red then
+               Z.Parent.Color        := Black;
+               Y.Color               := Black;
+               Z.Parent.Parent.Color := Red;
+               Z                     := Z.Parent.Parent;
+            else
+               if Z = Z.Parent.Left then
+                  Z := Z.Parent;
+                  Right_Rotate (Tree, Z);
+               end if;
+               Z.Parent.Color        := Black;
+               Z.Parent.Parent.Color := Red;
+               Left_Rotate (Tree, Z.Parent.Parent);
+            end if;
+         end if;
+      end loop;
+      Tree.Root.Color := Black;
+   end Insert_Fixup;
+
+   procedure Left_Rotate (Tree : in out Pkg.Tree; X : in Node_Access)
+   is
+      --  [1] 13.2 Left-Rotate (T, x)
+      Nil : Node_Access renames Tree.Nil;
+      Y   : constant Node_Access := X.Right;
+   begin
+      X.Right := Y.Left;
+      if Y.Left /= Nil then
+         Y.Left.Parent := X;
+      end if;
+      Y.Parent := X.Parent;
+      if X.Parent = Nil then
+         Tree.Root := Y;
+      elsif X = X.Parent.Left then
+         X.Parent.Left := Y;
+      else
+         X.Parent.Right := Y;
+      end if;
+      Y.Left   := X;
+      X.Parent := Y;
+   end Left_Rotate;
+
+   function Minimum (Node : in Node_Access; Nil : in Node_Access) return 
Node_Access
+   is begin
+      return Result : Node_Access := Node
+      do
+         while Result.Left /= Nil loop
+            Result := Result.Left;
+         end loop;
+      end return;
+   end Minimum;
+
+   procedure Right_Rotate (Tree : in out Pkg.Tree; X : in Node_Access)
+   is
+      --  [1] 13.2 Right-Rotate (T, x)
+      Nil : Node_Access renames Tree.Nil;
+      Y   : constant Node_Access := X.Left;
+   begin
+      X.Left := Y.Right;
+      if Y.Right /= Nil then
+         Y.Right.Parent := X;
+      end if;
+      Y.Parent := X.Parent;
+      if X.Parent = Nil then
+         Tree.Root := Y;
+      elsif X = X.Parent.Right then
+         X.Parent.Right := Y;
+      else
+         X.Parent.Left := Y;
+      end if;
+      Y.Right  := X;
+      X.Parent := Y;
+   end Right_Rotate;
+
+   procedure Transplant (T : in out Pkg.Tree; U, V : in Node_Access)
+   is
+      Nil : Node_Access renames T.Nil;
+   begin
+      --  [1] 13.4 RB-Transplant, 12.3 Transplant
+
+      if U.Parent = Nil then
+         T.Root := V;
+      elsif U = U.Parent.Left then
+         U.Parent.Left := V;
+      else
+         U.Parent.Right := V;
+      end if;
+      V.Parent := U.Parent;
+   end Transplant;
+
+   ----------
+   --  Public subprograms, spec order
+
+   overriding procedure Finalize (Object : in out Tree)
+   is begin
+      if Object.Root /= null then
+         if Object.Root = Object.Nil then
+            Free (Object.Nil);
+            Object.Root := null;
+         else
+            Free_Tree (Object.Root, Object.Nil);
+            Free (Object.Nil);
+         end if;
+      end if;
+   end Finalize;
+
+   overriding procedure Initialize (Object : in out Tree)
+   is begin
+      Object.Nil       := new Node;
+      Object.Nil.Color := Black;
+      Object.Root      := Object.Nil;
+   end Initialize;
+
+   function Has_Element (Cursor : in Pkg.Cursor) return Boolean
+   is begin
+      return Cursor.Node /= null;
+   end Has_Element;
+
+   function Constant_Ref
+     (Container : aliased in Tree;
+      Position  :         in Cursor)
+     return Constant_Ref_Type
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Position.Node.all.Element'Access);
+   end Constant_Ref;
+
+   function Constant_Ref
+     (Container : aliased in Tree;
+      Key       :         in Key_Type)
+     return Constant_Ref_Type
+   is
+      Node : constant Node_Access := Find (Container.Root, Key, Container.Nil);
+   begin
+      if Node = null then
+         raise Not_Found;
+      else
+         return (Element => Node.all.Element'Access);
+      end if;
+   end Constant_Ref;
+
+   function Variable_Ref
+     (Container : aliased in Tree;
+      Position  :         in Cursor)
+     return Variable_Ref_Type
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Position.Node.all.Element'Access);
+   end Variable_Ref;
+
+   function Variable_Ref
+     (Container : aliased in Tree;
+      Key       :         in Key_Type)
+     return Variable_Ref_Type
+   is
+      Node : constant Node_Access := Find (Container.Root, Key, Container.Nil);
+   begin
+      if Node = null then
+         raise Not_Found;
+      else
+         return (Element => Node.all.Element'Access);
+      end if;
+   end Variable_Ref;
+
+   function Iterate (Tree : in Pkg.Tree'Class) return Iterator
+   is begin
+      return (Tree.Root, Tree.Nil);
+   end Iterate;
+
+   overriding function First (Iterator : in Pkg.Iterator) return Cursor
+   is
+      Nil  : Node_Access renames Iterator.Nil;
+      Node : Node_Access := Iterator.Root;
+   begin
+      if Node = Nil then
+         return
+           (Node       => null,
+            Direction  => Unknown,
+            Left_Done  => True,
+            Right_Done => True);
+      else
+         loop
+            exit when Node.Left = Nil;
+            Node := Node.Left;
+         end loop;
+
+         return
+           (Node       => Node,
+            Direction  => Ascending,
+            Left_Done  => True,
+            Right_Done => False);
+      end if;
+
+   end First;
+
+   overriding function Next (Iterator : in Pkg.Iterator; Position : in Cursor) 
return Cursor
+   is
+      Nil : Node_Access renames Iterator.Nil;
+   begin
+      if Position.Direction /= Ascending then
+         raise Programmer_Error;
+      end if;
+
+      if Position.Node = null then
+         return
+           (Node       => null,
+            Direction  => Unknown,
+            Left_Done  => True,
+            Right_Done => True);
+
+      elsif Position.Left_Done or Position.Node.Left = Nil then
+         if Position.Right_Done or Position.Node.Right = Nil then
+            if Position.Node.Parent = Nil then
+               return
+                 (Node       => null,
+                  Direction  => Unknown,
+                  Left_Done  => True,
+                  Right_Done => True);
+            else
+               declare
+                  Node : constant Node_Access := Position.Node.Parent;
+                  Temp : constant Cursor      :=
+                    (Node       => Node,
+                     Direction  => Ascending,
+                     Left_Done  => Node.Right = Position.Node or Node.Left = 
Position.Node,
+                     Right_Done => Node.Right = Position.Node);
+               begin
+                  if Temp.Right_Done then
+                     return Next (Iterator, Temp);
+                  else
+                     return Temp;
+                  end if;
+               end;
+            end if;
+         else
+            declare
+               Node : constant Node_Access := Position.Node.Right;
+               Temp : constant Cursor      :=
+                 (Node       => Node,
+                  Direction  => Ascending,
+                  Left_Done  => Node.Left = Nil,
+                  Right_Done => False);
+            begin
+               if Temp.Left_Done then
+                  return Temp;
+               else
+                  return Next (Iterator, Temp);
+               end if;
+            end;
+         end if;
+      else
+         declare
+            Node : constant Node_Access := Position.Node.Left;
+            Temp : constant Cursor      :=
+              (Node       => Node,
+               Direction  => Ascending,
+               Left_Done  => Node.Left = Nil,
+               Right_Done => False);
+         begin
+            if Temp.Left_Done then
+               return Temp;
+            else
+               return Next (Iterator, Temp);
+            end if;
+         end;
+      end if;
+   end Next;
+
+   overriding function Last (Iterator : in Pkg.Iterator) return Cursor
+   is
+      Nil  : Node_Access renames Iterator.Nil;
+      Node : Node_Access := Iterator.Root;
+   begin
+      if Node = Nil then
+         return
+           (Node       => null,
+            Direction  => Unknown,
+            Left_Done  => True,
+            Right_Done => True);
+      else
+         loop
+            exit when Node.Right = Nil;
+            Node := Node.Right;
+         end loop;
+         return
+           (Node       => Node,
+            Direction  => Descending,
+            Right_Done => True,
+            Left_Done  => False);
+      end if;
+   end Last;
+
+   overriding function Previous (Iterator : in Pkg.Iterator; Position : in 
Cursor) return Cursor
+   is
+      Nil : Node_Access renames Iterator.Nil;
+   begin
+      if Position.Direction /= Descending then
+         raise Programmer_Error;
+      end if;
+
+      if Position.Node = null then
+         return
+           (Node       => null,
+            Direction  => Unknown,
+            Left_Done  => True,
+            Right_Done => True);
+
+      elsif Position.Right_Done or Position.Node.Right = Nil then
+         if Position.Left_Done or Position.Node.Left = Nil then
+            if Position.Node.Parent = Nil then
+               return
+                 (Node       => null,
+                  Direction  => Unknown,
+                  Left_Done  => True,
+                  Right_Done => True);
+            else
+               declare
+                  Node : constant Node_Access := Position.Node.Parent;
+                  Temp : constant Cursor      :=
+                    (Node       => Node,
+                     Direction  => Descending,
+                     Right_Done => Node.Left = Position.Node or Node.Right = 
Position.Node,
+                     Left_Done  => Node.Left = Position.Node);
+               begin
+                  if Temp.Left_Done then
+                     return Previous (Iterator, Temp);
+                  else
+                     return Temp;
+                  end if;
+               end;
+            end if;
+         else
+            declare
+               Node : constant Node_Access := Position.Node.Left;
+               Temp : constant Cursor      :=
+                 (Node       => Node,
+                  Direction  => Descending,
+                  Right_Done => Node.Right = Nil,
+                  Left_Done  => False);
+            begin
+               if Temp.Right_Done then
+                  return Temp;
+               else
+                  return Previous (Iterator, Temp);
+               end if;
+            end;
+         end if;
+      else
+         declare
+            Node : constant Node_Access := Position.Node.Right;
+            Temp : constant Cursor      :=
+              (Node       => Node,
+               Direction  => Descending,
+               Right_Done => Node.Right = Nil,
+               Left_Done  => False);
+         begin
+            if Temp.Right_Done then
+               return Temp;
+            else
+               return Previous (Iterator, Temp);
+            end if;
+         end;
+      end if;
+   end Previous;
+
+   function Previous (Iterator : in Pkg.Iterator; Key : in Key_Type) return 
Cursor
+   is
+      Nil  : Node_Access renames Iterator.Nil;
+      Node : Node_Access := Iterator.Root;
+   begin
+      while Node /= Nil loop
+         declare
+            Current_Key : Key_Type renames Pkg.Key (Node.Element);
+         begin
+            case Key_Compare (Key, Current_Key) is
+            when Equal =>
+                  return Previous (Iterator, (Node, Descending, Right_Done => 
True, Left_Done => False));
+
+            when Less =>
+               if Node.Left = Nil then
+                  return Previous (Iterator, (Node, Descending, Right_Done => 
True, Left_Done => True));
+               else
+                  Node := Node.Left;
+               end if;
+
+            when Greater =>
+               if Node.Right = Nil then
+                  return (Node, Descending, Right_Done => True, Left_Done => 
False);
+               else
+                  Node := Node.Right;
+               end if;
+            end case;
+         end;
+      end loop;
+
+      return
+        (Node       => null,
+         Direction  => Unknown,
+         Left_Done  => True,
+         Right_Done => True);
+   end Previous;
+
+   function Find
+     (Iterator  : in Pkg.Iterator;
+      Key       : in Key_Type;
+      Direction : in Direction_Type := Ascending)
+     return Cursor
+   is
+      Nil  : Node_Access renames Iterator.Nil;
+      Node : constant Node_Access := Find (Iterator.Root, Key, Nil);
+   begin
+      if Node = null then
+         return
+           (Node       => null,
+            Direction  => Unknown,
+            Left_Done  => True,
+            Right_Done => True);
+      else
+         return
+           (Node       => Node,
+            Direction  => Direction,
+            Left_Done  =>
+              (case Direction is
+               when Ascending | Unknown => True,
+               when Descending => Node.Left = Nil),
+            Right_Done =>
+              (case Direction is
+               when Ascending => Node.Right = Nil,
+               when Descending | Unknown => True));
+      end if;
+   end Find;
+
+   function Find_In_Range
+     (Iterator    : in Pkg.Iterator;
+      Direction   : in Known_Direction_Type;
+      First, Last : in Key_Type)
+     return Cursor
+   is
+      Nil       : Node_Access renames Iterator.Nil;
+      Node      : Node_Access := Iterator.Root;
+      Candidate : Node_Access := null; -- best result found so far
+   begin
+      while Node /= Nil loop
+         declare
+            Current_Key : Key_Type renames Key (Node.Element);
+         begin
+            case Direction is
+            when Ascending =>
+               case Key_Compare (First, Current_Key) is
+               when Equal =>
+                     return (Node, Ascending, Right_Done => False, Left_Done 
=> True);
+
+               when Less =>
+                  if Node.Left = Nil then
+                     case Key_Compare (Current_Key, Last) is
+                     when Less | Equal =>
+                        return (Node, Ascending, Right_Done => False, 
Left_Done => True);
+                     when Greater =>
+                        if Candidate = null then
+                           return No_Element;
+                        else
+                           return (Candidate, Ascending, Right_Done => False, 
Left_Done => True);
+                        end if;
+                     end case;
+                  else
+                     case Key_Compare (Last, Current_Key) is
+                     when Greater | Equal =>
+                        Candidate := Node;
+                     when Less =>
+                        null;
+                     end case;
+                     Node := Node.Left;
+                  end if;
+
+               when Greater =>
+                  if Node.Right = Nil then
+                     if Candidate = null then
+                        return No_Element;
+                     else
+                        return (Candidate, Ascending, Right_Done => False, 
Left_Done => True);
+                     end if;
+                  else
+                     Node := Node.Right;
+                  end if;
+               end case;
+
+            when Descending =>
+               if Last = Current_Key then
+                  return (Node, Descending, Right_Done => True, Left_Done => 
False);
+
+               else
+                  case Key_Compare (Last, Current_Key) is
+                  when Greater =>
+                     if Node.Right = Nil then
+                        case Key_Compare (Current_Key, First) is
+                        when Greater | Equal =>
+                           return (Node, Descending, Right_Done => True, 
Left_Done => False);
+                        when Less =>
+                           if Candidate = null then
+                              return No_Element;
+                           else
+                              return (Candidate, Ascending, Right_Done => 
False, Left_Done => True);
+                           end if;
+                        end case;
+                     else
+                        case Key_Compare (First, Current_Key) is
+                        when Less | Equal =>
+                           Candidate := Node;
+                        when Greater =>
+                           null;
+                        end case;
+                        Node := Node.Right;
+                     end if;
+                  when Equal | Less =>
+                     if Node.Left = Nil then
+                        if Candidate = null then
+                           return No_Element;
+                        else
+                           return (Candidate, Ascending, Right_Done => False, 
Left_Done => True);
+                        end if;
+                     else
+                        Node := Node.Left;
+                     end if;
+                  end case;
+               end if;
+            end case;
+         end;
+      end loop;
+
+      return No_Element;
+   end Find_In_Range;
+
+   function Count (Tree : in Pkg.Tree) return Ada.Containers.Count_Type
+   is begin
+      if Tree.Root = Tree.Nil then
+         return 0;
+      else
+         return Count_Tree (Tree.Root, Tree.Nil);
+      end if;
+   end Count;
+
+   function Present (Container : in Tree; Key : in Key_Type) return Boolean
+   is
+      Nil  : Node_Access renames Container.Nil;
+      Node : Node_Access := Container.Root;
+   begin
+      while Node /= Nil loop
+         case Key_Compare (Key, Pkg.Key (Node.Element)) is
+         when Equal =>
+            return True;
+         when Less =>
+            Node := Node.Left;
+         when Greater =>
+            Node := Node.Right;
+         end case;
+      end loop;
+      return False;
+   end Present;
+
+   function Insert (Tree : in out Pkg.Tree; Element : in Element_Type) return 
Cursor
+   is
+      --  [1] 13.3 RB-Insert (T, z)
+      Nil   : Node_Access renames Tree.Nil;
+      Z     : Node_Access       := new Node'(Element, Nil, Nil, Nil, Red);
+      Key_Z : constant Key_Type := Key (Z.Element);
+      Y     : Node_Access       := Nil;
+      X     : Node_Access       := Tree.Root;
+
+      Result      : Node_Access;
+      Compare_Z_Y : Compare_Result;
+   begin
+      Nil.Parent := null;
+      Nil.Left   := null;
+      Nil.Right  := null;
+
+      while X /= Nil loop
+         Y := X;
+         Compare_Z_Y := Key_Compare (Key_Z, Key (X.Element));
+         case Compare_Z_Y is
+         when Less =>
+            X := X.Left;
+         when Equal | Greater =>
+            X := X.Right;
+         end case;
+      end loop;
+
+      Z.Parent := Y;
+      if Y = Nil then
+         Tree.Root := Z;
+      else
+         case Compare_Z_Y is
+         when Less =>
+            Y.Left := Z;
+         when Equal | Greater =>
+            Y.Right := Z;
+         end case;
+      end if;
+
+      Result := Z;
+      if Z = Tree.Root then
+         Z.Color := Black;
+      else
+         Insert_Fixup (Tree, Z);
+      end if;
+
+      return
+        (Node       => Result,
+         Direction  => Unknown,
+         Left_Done  => True,
+         Right_Done => True);
+   end Insert;
+
+   procedure Insert (Tree : in out Pkg.Tree; Element : in Element_Type)
+   is
+      Temp : Cursor := Insert (Tree, Element);
+      pragma Unreferenced (Temp);
+   begin
+      null;
+   end Insert;
+
+   procedure Delete (Tree : in out Pkg.Tree; Position : in out Cursor)
+   is
+      Nil          : Node_Access renames Tree.Nil;
+      T            : Pkg.Tree renames Tree;
+      Z            : constant Node_Access :=
+        (if Position.Node = null then raise Parameter_Error else 
Position.Node);
+      Y            : Node_Access          := Z;
+      Y_Orig_Color : Color                := Y.Color;
+      X            : Node_Access;
+   begin
+      --  Catch logic errors in use of Nil
+      Nil.Parent := null;
+      Nil.Left   := null;
+      Nil.Right  := null;
+
+      --  [1] 13.4 RB-Delete.
+      if Z.Left = Nil then
+         X := Z.Right;
+         Transplant (T, Z, Z.Right);
+
+      elsif Z.Right = Nil then
+         X := Z.Left;
+         Transplant (T, Z, Z.Left);
+
+      else
+         Y            := Minimum (Z.Right, Nil);
+         Y_Orig_Color := Y.Color;
+         X            := Y.Right;
+         if Y.Parent = Z then
+            X.Parent := Y;
+            --  This is already true unless X = Nil, in which case delete_fixup
+            --  needs the info.
+         else
+            Transplant (T, Y, Y.Right);
+            Y.Right := Z.Right;
+
+            Y.Right.Parent := Y;
+            --  This is already true unless Y.Right = Nil, in which case
+            --  delete_fixup needs the info.
+         end if;
+
+         Transplant (T, Z, Y);
+         Y.Left := Z.Left;
+
+         Y.Left.Parent := Y;
+         --  This is already true unless Y.Left = Nil, in which case
+         --  delete_fixup needs the info.
+
+         Y.Color := Z.Color;
+      end if;
+
+      if Y_Orig_Color = Black then
+         Delete_Fixup (T, X);
+      end if;
+
+      Free (Position.Node);
+   end Delete;
+
+end SAL.Gen_Unbounded_Definite_Red_Black_Trees;
diff --git a/sal-gen_unbounded_definite_red_black_trees.ads 
b/sal-gen_unbounded_definite_red_black_trees.ads
new file mode 100644
index 0000000..7096970
--- /dev/null
+++ b/sal-gen_unbounded_definite_red_black_trees.ads
@@ -0,0 +1,181 @@
+--  Abstract :
+--
+--  Generic unbounded red-black tree with definite elements, definite
+--  or indefinite key.
+--
+--  References :
+--
+--  [1] Introduction to Algorithms, Thomas H. Cormen, Charles E.
+--  Leiserson, Ronald L. Rivest, Clifford Stein.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Finalization;
+with Ada.Iterator_Interfaces;
+with Ada.Unchecked_Deallocation;
+generic
+   type Element_Type is private;
+   type Key_Type (<>) is private;
+   with function Key (Element : in Element_Type) return Key_Type is <>;
+   with function Key_Compare (Left, Right : in Key_Type) return Compare_Result 
is <>;
+package SAL.Gen_Unbounded_Definite_Red_Black_Trees is
+
+   package Pkg renames Gen_Unbounded_Definite_Red_Black_Trees;
+
+   type Tree is new Ada.Finalization.Limited_Controlled with private
+   with
+     Constant_Indexing => Constant_Ref,
+     Variable_Indexing => Variable_Ref,
+     Default_Iterator  => Iterate,
+     Iterator_Element  => Element_Type;
+
+   overriding procedure Finalize (Object : in out Tree);
+   overriding procedure Initialize (Object : in out Tree);
+
+   type Direction_Type is (Ascending, Descending, Unknown);
+   subtype Known_Direction_Type is Direction_Type range Ascending .. 
Descending;
+   --  Direction of Iterators.
+   --  If Ascending, Next may be called.
+   --  If Descending, Previous may be called.
+   --  If Unknown, neither.
+
+   type Cursor is private;
+
+   No_Element : constant Cursor;
+
+   function Has_Element (Cursor : in Pkg.Cursor) return Boolean;
+
+   type Constant_Ref_Type (Element : not null access constant Element_Type) is 
null record
+   with Implicit_Dereference => Element;
+
+   function Constant_Ref
+     (Container : aliased in Tree;
+      Position  :         in Cursor)
+     return Constant_Ref_Type;
+
+   function Constant_Ref
+     (Container : aliased in Tree;
+      Key       :         in Key_Type)
+     return Constant_Ref_Type;
+
+   type Variable_Ref_Type (Element : not null access Element_Type) is null 
record
+   with Implicit_Dereference => Element;
+
+   function Variable_Ref
+     (Container : aliased in Tree;
+      Position  :         in Cursor)
+     return Variable_Ref_Type;
+
+   function Variable_Ref
+     (Container : aliased in Tree;
+      Key       :         in Key_Type)
+     return Variable_Ref_Type;
+   --  Raises Not_Found if Key not found in Container.
+
+   package Iterators is new Ada.Iterator_Interfaces (Cursor, Has_Element);
+
+   type Iterator is new Iterators.Reversible_Iterator with private;
+
+   function Iterate (Tree : in Pkg.Tree'Class) return Iterator;
+
+   overriding function First (Iterator : in Pkg.Iterator) return Cursor;
+   overriding function Next (Iterator : in Pkg.Iterator; Position : in Cursor) 
return Cursor;
+   overriding function Last (Iterator : in Pkg.Iterator) return Cursor;
+   overriding function Previous (Iterator : in Pkg.Iterator; Position : in 
Cursor) return Cursor;
+
+   function Previous (Iterator : in Pkg.Iterator; Key : in Key_Type) return 
Cursor;
+   --  Initialise Iterator to descending, starting at element with
+   --  largest key < Key. Has_Element (result) is False if there is no
+   --  such element.
+
+   function Find
+     (Iterator  : in Pkg.Iterator;
+      Key       : in Key_Type;
+      Direction : in Direction_Type := Ascending)
+     return Cursor;
+   --  Has_Element is False if Key is not in Container.
+
+   function Find_In_Range
+     (Iterator    : in Pkg.Iterator;
+      Direction   : in Known_Direction_Type;
+      First, Last : in Key_Type)
+     return Cursor;
+   --  Find first element with Key greater than or equal to First, and
+   --  less than or equal to Last. If Direction is Ascending, start
+   --  search at First; if Descending, at Last.
+   --
+   --  Has_Element is False if there is no such Key.
+
+   function Count (Tree : in Pkg.Tree) return Ada.Containers.Count_Type;
+
+   function Present (Container : in Tree; Key : in Key_Type) return Boolean;
+
+   procedure Insert (Tree : in out Pkg.Tree; Element : in Element_Type);
+   function Insert (Tree : in out Pkg.Tree; Element : in Element_Type) return 
Cursor;
+   --  Result points to newly inserted element.
+
+   procedure Delete (Tree : in out Pkg.Tree; Position : in out Cursor);
+   --  Delete element at Position; set Position to point to no element.
+private
+
+   type Node;
+   type Node_Access is access Node;
+
+   type Color is (Red, Black);
+
+   type Node is record
+      Element : aliased Element_Type;
+      Parent  : Node_Access;
+      Left    : Node_Access;
+      Right   : Node_Access;
+      Color   : Pkg.Color;
+   end record;
+
+   procedure Free is new Ada.Unchecked_Deallocation (Node, Node_Access);
+
+   type Tree is new Ada.Finalization.Limited_Controlled with record
+      Root : Node_Access;
+      Nil  : Node_Access;
+      --  Nil is the node pointed to by all links that would otherwise be
+      --  'null'. This simplifies several algorithm (for example,
+      --  Node.Left.Color is always valid). Its parent, left, right links
+      --  are used as temp storage for some algorithms (especially Delete).
+      --  Nil.Color is Black.
+   end record;
+
+   type Cursor is record
+      Node : Node_Access := null;
+
+      Direction  : Direction_Type := Unknown;
+      --  Set in First or Last, enforced in next/prev (cannot change 
direction).
+
+      Left_Done  : Boolean := True;
+      Right_Done : Boolean := True;
+   end record;
+
+   No_Element : constant Cursor :=
+     (Node       => null,
+      Direction  => Unknown,
+      Left_Done  => True,
+      Right_Done => True);
+
+   type Iterator is new Iterators.Reversible_Iterator with
+   record
+      Root : Node_Access;
+      Nil  : Node_Access;
+   end record;
+
+end SAL.Gen_Unbounded_Definite_Red_Black_Trees;
diff --git a/sal-gen_unbounded_definite_stacks-gen_image_aux.adb 
b/sal-gen_unbounded_definite_stacks-gen_image_aux.adb
new file mode 100644
index 0000000..e900846
--- /dev/null
+++ b/sal-gen_unbounded_definite_stacks-gen_image_aux.adb
@@ -0,0 +1,42 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings.Unbounded;
+function SAL.Gen_Unbounded_Definite_Stacks.Gen_Image_Aux
+  (Item  : in Stack;
+   Aux   : in Aux_Data;
+   Depth : in SAL.Base_Peek_Type := 0)
+  return String
+is
+   use Ada.Strings.Unbounded;
+   Result : Unbounded_String        := To_Unbounded_String ("(");
+   Last   : constant Base_Peek_Type :=
+     (if Depth = 0
+      then Item.Top
+      else Base_Peek_Type'Min (Depth, Item.Top));
+begin
+   for I in 1 .. Last loop
+      Result := Result & Element_Image (Item.Peek (I), Aux);
+      if I /= Last then
+         Result := Result & ", ";
+      end if;
+   end loop;
+   Result := Result & ")";
+   return To_String (Result);
+end SAL.Gen_Unbounded_Definite_Stacks.Gen_Image_Aux;
diff --git a/sal-gen_unbounded_definite_stacks-gen_image_aux.ads 
b/sal-gen_unbounded_definite_stacks-gen_image_aux.ads
new file mode 100644
index 0000000..ff732a8
--- /dev/null
+++ b/sal-gen_unbounded_definite_stacks-gen_image_aux.ads
@@ -0,0 +1,27 @@
+--  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_Stacks.Gen_Image_Aux
+  (Item  : in Stack;
+   Aux   : in Aux_Data;
+   Depth : in SAL.Base_Peek_Type := 0)
+  return String;
diff --git a/sal-gen_unbounded_definite_stacks.adb 
b/sal-gen_unbounded_definite_stacks.adb
new file mode 100644
index 0000000..d916b6e
--- /dev/null
+++ b/sal-gen_unbounded_definite_stacks.adb
@@ -0,0 +1,178 @@
+--  Abstract:
+--
+--  see spec
+--
+--  Copyright (C) 1998, 2003, 2009, 2015, 2017, 2018 Stephen Leake.  All 
Rights Reserved.
+--
+--  SAL 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. SAL 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 SAL; 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
+--  SAL, or you link SAL object files with other files to produce an
+--  executable, that 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);
+
+package body SAL.Gen_Unbounded_Definite_Stacks is
+
+   ----------
+   --  local subprogram bodies
+
+   procedure Grow (Stack : in out Sguds.Stack; Desired_Size : in 
Base_Peek_Type)
+   is
+      New_Data : constant Element_Array_Access := new Element_Array (1 .. 
Desired_Size);
+   begin
+      New_Data (1 .. Stack.Top) := Stack.Data (1 .. Stack.Top);
+      Free (Stack.Data);
+      Stack.Data := New_Data;
+   end Grow;
+
+   ----------
+   --  Spec visible subprograms
+   overriding procedure Finalize (Stack : in out Sguds.Stack)
+   is begin
+      if Stack.Data /= null then
+         Free (Stack.Data);
+         Stack.Top := Invalid_Peek_Index;
+      end if;
+   end Finalize;
+
+   overriding procedure Adjust (Stack : in out Sguds.Stack)
+   is begin
+      if Stack.Data /= null then
+         Stack.Data := new Element_Array'(Stack.Data.all);
+      end if;
+   end Adjust;
+
+   overriding
+   function "=" (Left, Right : in Sguds.Stack) return Boolean
+   is begin
+      if Left.Data = null then
+         return Right.Data = null;
+      elsif Left.Top /= Right.Top then
+         return False;
+      else
+         --  Assume stacks differ near top.
+         for I in reverse 1 .. Left.Top loop
+            if Left.Data (I) /= Right.Data (I) then
+               return False;
+            end if;
+         end loop;
+         return True;
+      end if;
+   end "=";
+
+   procedure Clear (Stack : in out Sguds.Stack)
+   is begin
+      --  We don't change the reserved capacity, on the assumption the
+      --  stack will be used again.
+      Stack.Top := 0;
+   end Clear;
+
+   function Depth (Stack : in Sguds.Stack) return Base_Peek_Type
+   is begin
+      return Stack.Top;
+   end Depth;
+
+   function Is_Empty (Stack : in Sguds.Stack) return Boolean
+   is begin
+      return Stack.Top = 0;
+   end Is_Empty;
+
+   function Peek
+     (Stack : in Sguds.Stack;
+      Index : in Peek_Type := 1)
+     return Element_Type
+   is begin
+      return Stack.Data (Stack.Top - Index + 1);
+   end Peek;
+
+   procedure Pop (Stack : in out Sguds.Stack; Count : in Base_Peek_Type := 1)
+   is begin
+      if Stack.Top < Count then
+         raise Container_Empty;
+      else
+         Stack.Top := Stack.Top - Count;
+      end if;
+   end Pop;
+
+   function Pop (Stack : in out Sguds.Stack) return Element_Type
+   is begin
+      if Stack.Top = 0 then
+         raise Container_Empty;
+      else
+         return Result : constant Element_Type := Stack.Peek (1)
+         do
+            Stack.Top := Stack.Top - 1;
+         end return;
+      end if;
+   end Pop;
+
+   procedure Push (Stack : in out Sguds.Stack; Item : in Element_Type)
+   is begin
+      if Stack.Data = null then
+         --  Adding a generic parameter for a reasonably large default initial
+         --  size here makes Wisitoken McKenzie recover slightly slower,
+         --  presumably due to increased cache thrashing.
+         Stack.Data := new Element_Array (1 .. 2);
+      elsif Stack.Top = Stack.Data'Last then
+         Grow (Stack, Desired_Size => 2 * Stack.Data'Last);
+      end if;
+      Stack.Top := Stack.Top + 1;
+      Stack.Data (Stack.Top) := Item;
+   end Push;
+
+   function Top (Stack : in Sguds.Stack) return Element_Type
+   is begin
+      if Stack.Top < 1 then
+         raise SAL.Container_Empty;
+      else
+         return Peek (Stack, 1);
+      end if;
+   end Top;
+
+   procedure Set_Depth
+     (Stack : in out Sguds.Stack;
+      Depth : in     Peek_Type)
+   is begin
+      if Stack.Data = null then
+         Stack.Data := new Element_Array (1 .. 2 * Depth);
+      elsif Depth > Stack.Data'Last then
+         Grow (Stack, Desired_Size => 2 * Depth);
+      end if;
+   end Set_Depth;
+
+   procedure Set
+     (Stack   : in out Sguds.Stack;
+      Index   : in     Peek_Type;
+      Depth   : in     Peek_Type;
+      Element : in     Element_Type)
+   is begin
+      --  Same Position algorithm as in Peek
+      Stack.Top := Depth;
+      Stack.Data (Depth - Index + 1) := Element;
+   end Set;
+
+   function Constant_Ref
+     (Container : aliased in Stack'Class;
+      Position  :         in Peek_Type)
+     return Constant_Ref_Type
+   is begin
+      return
+        (Element => Container.Data (Container.Top - Position + 1)'Access,
+         Dummy => 1);
+   end Constant_Ref;
+
+end SAL.Gen_Unbounded_Definite_Stacks;
diff --git a/sal-gen_unbounded_definite_stacks.ads 
b/sal-gen_unbounded_definite_stacks.ads
new file mode 100644
index 0000000..413071f
--- /dev/null
+++ b/sal-gen_unbounded_definite_stacks.ads
@@ -0,0 +1,133 @@
+--  Abstract:
+--
+--  Stack implementation.
+--
+--  Copyright (C) 1998-2000, 2002-2003, 2009, 2015, 2017, 2018 Stephen Leake.  
All Rights Reserved.
+--
+--  SAL 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. SAL 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 SAL; 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 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.Finalization;
+with Ada.Unchecked_Deallocation;
+generic
+   type Element_Type is private;
+package SAL.Gen_Unbounded_Definite_Stacks is
+
+   package Sguds renames SAL.Gen_Unbounded_Definite_Stacks;
+
+   type Stack is new Ada.Finalization.Controlled with private
+   with
+     Constant_Indexing => Constant_Ref;
+
+   Empty_Stack : constant Stack;
+
+   overriding procedure Finalize (Stack : in out Sguds.Stack);
+   overriding procedure Adjust (Stack : in out Sguds.Stack);
+
+   overriding function "=" (Left, Right : in Sguds.Stack) return Boolean;
+
+   procedure Clear (Stack : in out Sguds.Stack);
+   --  Empty Stack of all items.
+
+   function Depth (Stack : in Sguds.Stack) return Base_Peek_Type;
+   --  Returns current count of items in the Stack
+
+   function Is_Empty (Stack : in Sguds.Stack) return Boolean;
+   --  Returns true iff no items are in Stack.
+
+   function Peek
+     (Stack : in Sguds.Stack;
+      Index : in Peek_Type := 1)
+     return Element_Type;
+   --  Return the Index'th item from the top of Stack; the Item is _not_ 
removed.
+   --  Top item has index 1.
+   --
+   --  Raises Constraint_Error if Index > Depth.
+   --
+   --  See also Constant_Ref, implicit indexing
+
+   procedure Pop (Stack : in out Sguds.Stack; Count : in Base_Peek_Type := 1);
+   --  Remove Count Items from the top of Stack, discard them.
+   --
+   --  Raises Container_Empty if there are fewer than Count items on
+   --  Stack.
+
+   function Pop (Stack : in out Sguds.Stack) return Element_Type;
+   --  Remove Item from the top of Stack, and return it.
+   --
+   --  Raises Container_Empty if Is_Empty.
+
+   procedure Push (Stack : in out Sguds.Stack; Item : in Element_Type);
+   --  Add Item to the top of Stack.
+   --
+   --  May raise Container_Full.
+
+   function Top (Stack : in Sguds.Stack) return Element_Type;
+   --  Return the item at the top of Stack; the Item is _not_ removed.
+   --  Same as Peek (Stack, 1).
+   --
+   --  Raises Container_Empty if Is_Empty.
+
+   procedure Set_Depth
+     (Stack : in out Sguds.Stack;
+      Depth : in     Peek_Type);
+   --  Empty Stack, set its Depth to Depth. Must be followed by Set
+   --  for each element.
+   --
+   --  Useful when creating a stack from pre-existing data.
+
+   procedure Set
+     (Stack   : in out Sguds.Stack;
+      Index   : in     Peek_Type;
+      Depth   : in     Peek_Type;
+      Element : in     Element_Type);
+   --  Set a Stack element. Index is the same as Peek Index; Depth is
+   --  used to compute the index in the underlying array.
+   --
+   --  Stack must have been initialized by Set_Depth.
+   --
+   --  Useful when creating a stack from pre-existing data.
+
+   type Constant_Ref_Type (Element : not null access constant Element_Type) is
+   record
+      Dummy : Integer := raise Program_Error with "uninitialized reference";
+   end record
+   with Implicit_Dereference => Element;
+
+   function Constant_Ref
+     (Container : aliased in Stack'Class;
+      Position  :         in Peek_Type)
+     return Constant_Ref_Type;
+
+private
+
+   type Element_Array is array (Peek_Type range <>) of aliased Element_Type;
+   type Element_Array_Access is access Element_Array;
+   procedure Free is new Ada.Unchecked_Deallocation (Element_Array, 
Element_Array_Access);
+
+   type Stack is new Ada.Finalization.Controlled with record
+      Top  : Base_Peek_Type := Invalid_Peek_Index; -- empty
+      Data : Element_Array_Access;
+
+      --  Top of stack is at Data (Top).
+      --  Data (1 .. Last_Index) has been set at some point.
+   end record;
+
+   Empty_Stack : constant Stack := (Ada.Finalization.Controlled with 
Invalid_Peek_Index, null);
+
+end SAL.Gen_Unbounded_Definite_Stacks;
diff --git a/sal-gen_unbounded_definite_vectors-gen_comparable.adb 
b/sal-gen_unbounded_definite_vectors-gen_comparable.adb
new file mode 100644
index 0000000..1bd9251
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors-gen_comparable.adb
@@ -0,0 +1,73 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body SAL.Gen_Unbounded_Definite_Vectors.Gen_Comparable is
+
+   function Compare (Left, Right : in Vector) return Compare_Result
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Left.Length = 0 then
+         if Right.Length = 0 then
+            return Equal;
+         else
+            --  null is less than non-null
+            return Less;
+         end if;
+
+      elsif Right.Length = 0 then
+         return Greater;
+
+      else
+         declare
+            I : Base_Peek_Type := To_Peek_Type (Left.First);
+            J : Base_Peek_Type := To_Peek_Type (Right.First);
+
+            Left_Last  : constant Base_Peek_Type := To_Peek_Type (Left.Last);
+            Right_Last : constant Base_Peek_Type := To_Peek_Type (Right.Last);
+         begin
+            loop
+               exit when I > Left_Last or J > Right_Last;
+
+               case Element_Compare (Left.Elements (I), Right.Elements (J)) is
+               when Less =>
+                  return Less;
+               when Equal =>
+                  I := I + 1;
+                  J := J + 1;
+               when Greater =>
+                  return Greater;
+               end case;
+            end loop;
+            if I > Left_Last then
+               if J > Right_Last then
+                  return Equal;
+               else
+                  --  right is longer
+                  return Less;
+               end if;
+            else
+               --  left is longer
+               return Greater;
+            end if;
+         end;
+      end if;
+   end Compare;
+
+end SAL.Gen_Unbounded_Definite_Vectors.Gen_Comparable;
diff --git a/sal-gen_unbounded_definite_vectors-gen_comparable.ads 
b/sal-gen_unbounded_definite_vectors-gen_comparable.ads
new file mode 100644
index 0000000..a12f81a
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors-gen_comparable.ads
@@ -0,0 +1,30 @@
+--  Abstract :
+--
+--  Add "<" to 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
+   with function Element_Compare (Left, Right : in Element_Type) return 
Compare_Result;
+package SAL.Gen_Unbounded_Definite_Vectors.Gen_Comparable is
+
+   type Vector is new SAL.Gen_Unbounded_Definite_Vectors.Vector with null 
record;
+
+   function Compare (Left, Right : in Vector) return Compare_Result;
+   --  Similar to Ada "<" for arrays; Ada Reference Manual
+   --  section 4.5.2 para 26/3.
+
+end SAL.Gen_Unbounded_Definite_Vectors.Gen_Comparable;
diff --git a/sal-gen_unbounded_definite_vectors-gen_image.adb 
b/sal-gen_unbounded_definite_vectors-gen_image.adb
new file mode 100644
index 0000000..03a1c5f
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors-gen_image.adb
@@ -0,0 +1,50 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings.Fixed;
+with Ada.Strings.Unbounded;
+function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image (Item : in Vector; 
Strict : in Boolean := False) return String
+is
+   use all type Ada.Containers.Count_Type;
+   use Ada.Strings;
+   use Ada.Strings.Fixed;
+   use Ada.Strings.Unbounded;
+   Result : Unbounded_String        := To_Unbounded_String ("(");
+   First  : constant Base_Peek_Type := To_Peek_Type (Item.First_Index);
+   Last   : constant Base_Peek_Type := To_Peek_Type (Item.Last_Index);
+begin
+   if Strict and Item.Length = 0 then
+      return "(" & Trim (Index_Type'Image (Index_Type'First), Left) & " .. " &
+        Trim (Index_Type'Image (Extended_Index'First), Left) & " => <>)";
+
+   elsif Strict and Item.Length = 1 then
+      return "(" & Trim (Index_Type'Image (Index_Type'First), Left) & " => " &
+        Element_Image (Item.Elements (First)) & ")";
+
+   else
+      for I in First .. Last loop
+         Result := Result & Element_Image (Item.Elements (I));
+         if I /= Last then
+            Result := Result & ", ";
+         end if;
+      end loop;
+      Result := Result & ")";
+      return To_String (Result);
+   end if;
+end SAL.Gen_Unbounded_Definite_Vectors.Gen_Image;
diff --git a/sal-gen_unbounded_definite_vectors-gen_image.ads 
b/sal-gen_unbounded_definite_vectors-gen_image.ads
new file mode 100644
index 0000000..1ace2fe
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors-gen_image.ads
@@ -0,0 +1,24 @@
+--  Abstract :
+--
+--  Image 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
+   with function Element_Image (Item : in Element_Type) return String;
+function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image (Item : in Vector; 
Strict : in Boolean := False) return String;
+--  Image of Item, in Ada aggregate syntax. If Strict, use correct
+--  syntax for 0 and 1 item; otherwise, use () and (item).
diff --git a/sal-gen_unbounded_definite_vectors-gen_image_aux.adb 
b/sal-gen_unbounded_definite_vectors-gen_image_aux.adb
new file mode 100644
index 0000000..c498e0e
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors-gen_image_aux.adb
@@ -0,0 +1,36 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings.Unbounded;
+function SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux (Item : in Vector; 
Aux : in Aux_Data) return String
+is
+   use Ada.Strings.Unbounded;
+   Result : Unbounded_String        := To_Unbounded_String ("(");
+   First  : constant Base_Peek_Type := To_Peek_Type (Item.First_Index);
+   Last   : constant Base_Peek_Type := To_Peek_Type (Item.Last_Index);
+begin
+   for I in First .. Last loop
+      Result := Result & Element_Image (Item.Elements (I), Aux);
+      if I /= Last then
+         Result := Result & ", ";
+      end if;
+   end loop;
+   Result := Result & ")";
+   return To_String (Result);
+end SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux;
diff --git a/sal-gen_unbounded_definite_vectors-gen_image_aux.ads 
b/sal-gen_unbounded_definite_vectors-gen_image_aux.ads
new file mode 100644
index 0000000..0be7c41
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors-gen_image_aux.ads
@@ -0,0 +1,23 @@
+--  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;
diff --git a/sal-gen_unbounded_definite_vectors.adb 
b/sal-gen_unbounded_definite_vectors.adb
new file mode 100644
index 0000000..8faaf88
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors.adb
@@ -0,0 +1,578 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body SAL.Gen_Unbounded_Definite_Vectors is
+
+   function To_Peek_Type (Item : in Extended_Index) return Base_Peek_Type
+   is begin
+      return Base_Peek_Type'Base (Item - Index_Type'First) + Peek_Type'First;
+   end To_Peek_Type;
+
+   function To_Index_Type (Item : in Base_Peek_Type) return Extended_Index
+   is begin
+      return Extended_Index (Item - Peek_Type'First) + Index_Type'First;
+   end To_Index_Type;
+
+   procedure Grow (Elements : in out Array_Access; Index : in Base_Peek_Type)
+   is
+      --  Reallocate Elements so Elements (Index) is a valid element.
+
+      Old_First  : constant Peek_Type := Elements'First;
+      Old_Last   : constant Peek_Type := Elements'Last;
+      New_First  : Peek_Type          := Old_First;
+      New_Last   : Peek_Type          := Old_Last;
+      New_Length : Peek_Type          := Elements'Length;
+
+      New_Array : Array_Access;
+   begin
+      loop
+         exit when New_First <= Index;
+         New_Length := New_Length * 2;
+         New_First  := Peek_Type'Max (Peek_Type'First, Old_Last - New_Length + 
1);
+      end loop;
+      loop
+         exit when New_Last >= Index;
+         New_Length := New_Length * 2;
+         New_Last   := Peek_Type'Min (Peek_Type'Last, New_First + New_Length - 
1);
+      end loop;
+
+      New_Array := new Array_Type (New_First .. New_Last);
+
+      --  We'd like to use this:
+      --
+      --  New_Array (New_First .. Old_First - 1) := (others => <>);
+      --
+      --  but that can overflow the stack, since the aggregate is allocated
+      --  on the stack.
+
+      for I in New_First .. Old_First - 1 loop
+         New_Array (I .. I) := (others => <>);
+      end loop;
+
+      New_Array (Old_First .. Old_Last) := Elements.all;
+
+      for I in Old_Last + 1 .. New_Last loop
+         New_Array (I .. I)   := (others => <>);
+      end loop;
+
+      Free (Elements);
+      Elements := New_Array;
+   end Grow;
+
+   ----------
+   --  public subprograms
+
+   overriding procedure Finalize (Container : in out Vector)
+   is begin
+      Free (Container.Elements);
+      Container.First := No_Index;
+      Container.Last  := No_Index;
+   end Finalize;
+
+   overriding procedure Adjust (Container : in out Vector)
+   is begin
+      if Container.Elements /= null then
+         Container.Elements := new Array_Type'(Container.Elements.all);
+      end if;
+   end Adjust;
+
+   function Length (Container : in Vector) return Ada.Containers.Count_Type
+   is begin
+      --  We assume the type ranges are sensible, so no exceptions occur
+      --  here.
+      if Container.Elements = null then
+         return 0;
+      else
+         return Ada.Containers.Count_Type (To_Peek_Type (Container.Last) - 
Container.Elements'First + 1);
+      end if;
+   end Length;
+
+   function Capacity (Container : in Vector) return Ada.Containers.Count_Type
+   is begin
+      if Container.Elements = null then
+         return 0;
+      else
+         return Ada.Containers.Count_Type (Container.Elements'Length);
+      end if;
+   end Capacity;
+
+   function Element (Container : Vector; Index : Index_Type) return 
Element_Type
+   is begin
+      return Container.Elements (To_Peek_Type (Index));
+   end Element;
+
+   procedure Replace_Element (Container : Vector; Index : Index_Type; New_Item 
: in Element_Type)
+   is begin
+      Container.Elements (To_Peek_Type (Index)) := New_Item;
+   end Replace_Element;
+
+   function First_Index (Container : Vector) return Extended_Index
+   is begin
+      if Container.Elements = null then
+         return No_Index + 1;
+      else
+         return Container.First;
+      end if;
+   end First_Index;
+
+   function Last_Index (Container : Vector) return Extended_Index
+   is begin
+      if Container.Elements = null then
+         return No_Index;
+      else
+         return Container.Last;
+      end if;
+   end Last_Index;
+
+   procedure Append (Container : in out Vector; New_Item : in Element_Type)
+   is begin
+      if Container.First = No_Index then
+         Container.First := Index_Type'First;
+         Container.Last  := Index_Type'First;
+      else
+         Container.Last := Container.Last + 1;
+      end if;
+
+      declare
+         J : constant Base_Peek_Type := To_Peek_Type (Container.Last);
+      begin
+         if Container.Elements = null then
+            Container.Elements := new Array_Type (J .. J);
+
+         elsif J > Container.Elements'Last then
+            Grow (Container.Elements, J);
+         end if;
+
+         Container.Elements (J) := New_Item;
+      end;
+   end Append;
+
+   procedure Append (Container : in out Vector; New_Items : in Vector)
+   is
+      use all type Ada.Containers.Count_Type;
+      Old_Last : Extended_Index := Container.Last;
+   begin
+      if New_Items.Length = 0 then
+         return;
+      end if;
+
+      if Container.First = No_Index then
+         Container.First := Index_Type'First;
+         Old_Last        := Container.First - 1;
+         Container.Last  := Container.First + Extended_Index 
(New_Items.Length) - 1;
+      else
+         Container.Last := Container.Last + Extended_Index (New_Items.Length);
+      end if;
+
+      declare
+         I : constant Peek_Type := To_Peek_Type (Old_Last + 1);
+         J : constant Peek_Type := To_Peek_Type (Container.Last);
+      begin
+         if Container.Elements = null then
+            Container.Elements := new Array_Type (I .. J);
+         elsif J > Container.Elements'Last then
+            Grow (Container.Elements, J);
+         end if;
+
+         Container.Elements (I .. J) := New_Items.Elements
+           (To_Peek_Type (New_Items.First) .. To_Peek_Type (New_Items.Last));
+      end;
+   end Append;
+
+   procedure Prepend (Container : in out Vector; New_Item : in Element_Type)
+   is begin
+      if Container.First = No_Index then
+         Container.First := Index_Type'First;
+         Container.Last  := Index_Type'First;
+      else
+         Container.First := Container.First - 1;
+      end if;
+
+      declare
+         J : constant Peek_Type := To_Peek_Type (Container.First);
+      begin
+         if Container.Elements = null then
+            Container.Elements := new Array_Type (J .. J);
+
+         elsif J < Container.Elements'First then
+            Grow (Container.Elements, J);
+         end if;
+
+         Container.Elements (J) := New_Item;
+      end;
+   end Prepend;
+
+   procedure Prepend
+     (Target       : in out Vector;
+      Source       : in     Vector;
+      Source_First : in     Index_Type;
+      Source_Last  : in     Index_Type)
+   is
+      Source_I : constant Peek_Type := To_Peek_Type (Source_First);
+      Source_J : constant Peek_Type := To_Peek_Type (Source_Last);
+   begin
+      if Target.Elements = null then
+         Target.Elements := new Array_Type'(Source.Elements (Source_I .. 
Source_J));
+         Target.First    := Source_First;
+         Target.Last     := Source_Last;
+      else
+         declare
+            New_First : constant Index_Type := Target.First - (Source_Last - 
Source_First + 1);
+            I         : constant Peek_Type  := To_Peek_Type (New_First);
+            J         : constant Peek_Type  := To_Peek_Type (Target.First - 1);
+         begin
+            if Target.Elements'First > I then
+               Grow (Target.Elements, I);
+            end if;
+            Target.Elements (I .. J) := Source.Elements (Source_I .. Source_J);
+            Target.First := New_First;
+         end;
+      end if;
+   end Prepend;
+
+   procedure Insert
+     (Container : in out Vector;
+      Element   : in     Element_Type;
+      Before    : in     Index_Type)
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Container.Length = 0 then
+         Container.Append (Element);
+      else
+         Container.Last := Container.Last + 1;
+
+         declare
+            J : constant Peek_Type := To_Peek_Type (Before);
+            K : constant Base_Peek_Type := To_Peek_Type (Container.Last);
+         begin
+            if K > Container.Elements'Last then
+               Grow (Container.Elements, K);
+            end if;
+
+            Container.Elements (J + 1 .. K) := Container.Elements (J .. K - 1);
+            Container.Elements (J) := Element;
+         end;
+      end if;
+   end Insert;
+
+   procedure Merge
+     (Target : in out Vector;
+      Source : in out Vector)
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Source.Length = 0 then
+         Source.Clear;
+
+      elsif Target.Length = 0 then
+         Target := Source;
+         Source.Clear;
+
+      else
+         declare
+            New_First : constant Index_Type := Extended_Index'Min 
(Target.First, Source.First);
+            New_Last  : constant Index_Type := Extended_Index'Max 
(Target.Last, Source.Last);
+            New_I     : constant Peek_Type  := To_Peek_Type (New_First);
+            New_J     : constant Base_Peek_Type  := To_Peek_Type (New_Last);
+         begin
+            if New_I < Target.Elements'First then
+               Grow (Target.Elements, New_I);
+            end if;
+            if New_J > Target.Elements'Last then
+               Grow (Target.Elements, New_J);
+            end if;
+
+            Target.Elements (To_Peek_Type (Source.First) .. To_Peek_Type 
(Source.Last)) := Source.Elements
+              (To_Peek_Type (Source.First) .. To_Peek_Type (Source.Last));
+
+            Target.First := New_First;
+            Target.Last  := New_Last;
+
+            Source.Clear;
+         end;
+      end if;
+   end Merge;
+
+   function To_Vector (Item : in Element_Type; Count : in 
Ada.Containers.Count_Type := 1) return Vector
+   is begin
+      return Result : Vector do
+         for I in 1 .. Count loop
+            Result.Append (Item);
+         end loop;
+      end return;
+   end To_Vector;
+
+   function "+" (Element : in Element_Type) return Vector
+   is begin
+      return Result : Vector do
+         Result.Append (Element);
+      end return;
+   end "+";
+
+   function "&" (Left, Right : in Element_Type) return Vector
+   is begin
+      return Result : Vector do
+         Result.Append (Left);
+         Result.Append (Right);
+      end return;
+   end "&";
+
+   function "&" (Left : in Vector; Right : in Element_Type) return Vector
+   is begin
+      return Result : Vector := Left do
+         Result.Append (Right);
+      end return;
+   end "&";
+
+   procedure Set_First (Container : in out Vector; First : in Index_Type)
+   is
+      J : constant Peek_Type := To_Peek_Type (First);
+   begin
+      Container.First := First;
+      if Container.Last = No_Index then
+         Container.Last := First - 1;
+      end if;
+
+      if Container.Last >= First then
+         if Container.Elements = null then
+            Container.Elements := new Array_Type'(J .. To_Peek_Type 
(Container.Last) => <>);
+
+         elsif Container.Elements'First > J then
+            Grow (Container.Elements, J);
+         end if;
+      end if;
+   end Set_First;
+
+   procedure Set_Last (Container : in out Vector; Last : in Extended_Index)
+   is
+      J : constant Base_Peek_Type := To_Peek_Type (Last);
+   begin
+      Container.Last := Last;
+      if Container.First = No_Index then
+         Container.First := Last + 1;
+      end if;
+
+      if Last >= Container.First then
+         if Container.Elements = null then
+            Container.Elements := new Array_Type'(To_Peek_Type 
(Container.First) .. J => <>);
+
+         elsif Container.Elements'Last < J then
+            Grow (Container.Elements, J);
+         end if;
+      end if;
+   end Set_Last;
+
+   procedure Set_First_Last (Container : in out Vector; First : in Index_Type; 
Last : in Extended_Index)
+   is begin
+      Set_First (Container, First);
+      Set_Last (Container, Last);
+   end Set_First_Last;
+
+   procedure Set_Length (Container : in out Vector; Length : in 
Ada.Containers.Count_Type)
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Container.First = No_Index then
+         Container.First := Index_Type'First;
+         Container.Last  := Container.First - 1;
+      end if;
+      if Length > 0 then
+         Container.Set_Last (Index_Type (Length) + Container.First - 1);
+      end if;
+   end Set_Length;
+
+   procedure Set_Length
+     (Container : in out Vector;
+      Length    : in     Ada.Containers.Count_Type;
+      Default   : in     Element_Type)
+   is
+      Old_First : constant Extended_Index := Container.First;
+      Old_Last  : constant Extended_Index := Container.Last;
+   begin
+      Set_Length (Container, Length);
+      if Old_First = No_Index then
+         Container.Elements.all := (others => Default);
+      else
+         Container.Elements (To_Peek_Type (Old_Last + 1) .. To_Peek_Type 
(Container.Last)) := (others => Default);
+      end if;
+   end Set_Length;
+
+   procedure Delete (Container : in out Vector; Index : in Index_Type)
+   is
+      J : constant Peek_Type := To_Peek_Type (Index);
+   begin
+      Container.Elements (J .. J) := (J => <>);
+      if Index = Container.Last then
+         Container.Last := Container.Last - 1;
+      end if;
+   end Delete;
+
+   function Contains (Container : in Vector; Element : in Element_Type) return 
Boolean
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Container.Length = 0 then
+         return False;
+      else
+         for It of Container.Elements.all loop
+            if It = Element then
+               return True;
+            end if;
+         end loop;
+         return False;
+      end if;
+   end Contains;
+
+   function Has_Element (Position : Cursor) return Boolean is
+   begin
+      return Position.Index /= Invalid_Peek_Index;
+   end Has_Element;
+
+   function Element (Position : Cursor) return Element_Type
+   is begin
+      return Position.Container.Elements (Position.Index);
+   end Element;
+
+   function First (Container : aliased in Vector) return Cursor
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Container.Length = 0 then
+         return No_Element;
+      else
+         return (Container'Access, To_Peek_Type (Container.First));
+      end if;
+   end First;
+
+   function Next (Position : in Cursor) return Cursor
+   is begin
+      if Position = No_Element then
+         return No_Element;
+      elsif Position.Index < To_Peek_Type (Position.Container.Last) then
+         return (Position.Container, Position.Index + 1);
+      else
+         return No_Element;
+      end if;
+   end Next;
+
+   procedure Next (Position : in out Cursor)
+   is begin
+      if Position = No_Element then
+         null;
+      elsif Position.Index < To_Peek_Type (Position.Container.Last) then
+         Position.Index := Position.Index + 1;
+      else
+         Position := No_Element;
+      end if;
+   end Next;
+
+   function To_Cursor
+     (Container : aliased in Vector;
+      Index     :         in Extended_Index)
+     return Cursor
+   is begin
+      if Index not in Container.First .. Container.Last then
+         return No_Element;
+      else
+         return (Container'Access, To_Peek_Type (Index));
+      end if;
+   end To_Cursor;
+
+   function To_Index (Position : in Cursor) return Extended_Index
+   is begin
+      if Position = No_Element then
+         return No_Index;
+      else
+         return To_Index_Type (Position.Index);
+      end if;
+   end To_Index;
+
+   function Constant_Ref (Container : aliased Vector; Index : in Index_Type) 
return Constant_Reference_Type
+   is
+      J : constant Peek_Type := To_Peek_Type (Index);
+   begin
+      return (Element => Container.Elements (J)'Access);
+   end Constant_Ref;
+
+   function Variable_Ref
+     (Container : aliased in Vector;
+      Index     :         in Index_Type)
+     return Variable_Reference_Type
+   is
+      J : constant Peek_Type := To_Peek_Type (Index);
+   begin
+      return (Element => Container.Elements (J)'Access);
+   end Variable_Ref;
+
+   overriding function First (Object : Iterator) return Cursor
+   is begin
+      if Object.Container.Elements = null then
+         return (null, Invalid_Peek_Index);
+      else
+         return (Object.Container, To_Peek_Type 
(Object.Container.First_Index));
+      end if;
+   end First;
+
+   overriding function Last  (Object : Iterator) return Cursor
+   is begin
+      if Object.Container.Elements = null then
+         return (null, Invalid_Peek_Index);
+      else
+         return (Object.Container, To_Peek_Type (Object.Container.Last_Index));
+      end if;
+   end Last;
+
+   overriding function Next (Object : in Iterator; Position : in Cursor) 
return Cursor
+   is begin
+      if Position.Index = To_Peek_Type (Object.Container.Last) then
+         return (null, Invalid_Peek_Index);
+      else
+         return (Object.Container, Position.Index + 1);
+      end if;
+   end Next;
+
+   overriding function Previous (Object : in Iterator; Position : in Cursor) 
return Cursor
+   is begin
+      if Position.Index = To_Peek_Type (Index_Type'First) then
+         return (null, Invalid_Peek_Index);
+      else
+         return (Object.Container, Position.Index - 1);
+      end if;
+   end Previous;
+
+   function Iterate (Container : aliased in Vector) return 
Iterator_Interfaces.Reversible_Iterator'Class
+   is begin
+      return Iterator'(Container => Container'Unrestricted_Access);
+   end Iterate;
+
+   function Constant_Ref (Container : aliased Vector; Position : in Cursor) 
return Constant_Reference_Type
+   is begin
+      return (Element => Container.Elements (Position.Index)'Access);
+   end Constant_Ref;
+
+   function Variable_Ref
+     (Container : aliased in Vector;
+      Position  :         in Cursor)
+     return Variable_Reference_Type
+   is begin
+      return (Element => Container.Elements (Position.Index)'Access);
+   end Variable_Ref;
+
+end SAL.Gen_Unbounded_Definite_Vectors;
diff --git a/sal-gen_unbounded_definite_vectors.ads 
b/sal-gen_unbounded_definite_vectors.ads
new file mode 100644
index 0000000..799e73c
--- /dev/null
+++ b/sal-gen_unbounded_definite_vectors.ads
@@ -0,0 +1,226 @@
+--  Abstract :
+--
+--  A simple unbounded vector of definite items, intended to be faster
+--  than Ada.Containers.Vectors.
+--
+--  Prepend is as fast (in amortized time) as Append.
+--
+--  It provides no checking of cursor tampering; higher level code
+--  must ensure that.
+--
+--  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);
+
+with Ada.Finalization;
+with Ada.Iterator_Interfaces;
+with Ada.Unchecked_Deallocation;
+generic
+   type Index_Type is range <>;
+   type Element_Type is private;
+package SAL.Gen_Unbounded_Definite_Vectors is
+
+   subtype Extended_Index is Index_Type'Base
+     range Index_Type'First - 1 ..
+           Index_Type'Min (Index_Type'Base'Last - 1, Index_Type'Last) + 1;
+
+   No_Index : constant Extended_Index := Extended_Index'First;
+
+   type Vector is new Ada.Finalization.Controlled with private with
+      Constant_Indexing => Constant_Ref,
+      Variable_Indexing => Variable_Ref,
+      Default_Iterator  => Iterate,
+      Iterator_Element  => Element_Type;
+
+   Empty_Vector : constant Vector;
+
+   overriding procedure Finalize (Container : in out Vector);
+   overriding procedure Adjust (Container : in out Vector);
+
+   overriding function "=" (Left, Right : in Vector) return Boolean is
+     (raise Programmer_Error);
+   --  Use Gen_Comparable child.
+
+   function Length (Container : in Vector) return Ada.Containers.Count_Type;
+   function Capacity (Container : in Vector) return Ada.Containers.Count_Type;
+
+   procedure Clear (Container : in out Vector)
+   renames Finalize;
+
+   function First_Index (Container : Vector) return Extended_Index;
+   --  No_Index + 1 when Container is empty, so "for I in C.First_Index
+   --  .. C.Last_Index loop" works.
+
+   function Last_Index (Container : Vector) return Extended_Index;
+   --  No_Index when Container is empty.
+
+   function Element (Container : Vector; Index : Index_Type) return 
Element_Type
+   with Pre => Index >= Container.First_Index and Index <= 
Container.Last_Index;
+
+   procedure Replace_Element (Container : Vector; Index : Index_Type; New_Item 
: in Element_Type);
+
+   procedure Append (Container : in out Vector; New_Item : in Element_Type);
+   --  Insert New_Item at end of Container.
+   --
+   --  Raises Constraint_Error if index of new item would be greater than
+   --  Index_Type'Last.
+
+   procedure Append (Container : in out Vector; New_Items : in Vector);
+   --  Insert all elements of New_Items at end of Container.
+
+   procedure Prepend (Container : in out Vector; New_Item : in Element_Type);
+   --  Insert New_Item at beginning of Container.
+   --
+   --  Raises Constraint_Error if index of new item would be less than
+   --  Index_Type'First.
+
+   procedure Prepend
+     (Target       : in out Vector;
+      Source       : in     Vector;
+      Source_First : in     Index_Type;
+      Source_Last  : in     Index_Type);
+   --  Copy Source (Source_First .. Source_Last) to Target, before
+   --  Target.First_Index.
+
+   procedure Insert
+     (Container : in out Vector;
+      Element   : in     Element_Type;
+      Before    : in     Index_Type);
+   --  Existing elements at Before and after are slid to higher indices.
+
+   procedure Merge
+     (Target : in out Vector;
+      Source : in out Vector);
+   --  Copy all elements from Source to Target, to the same index range,
+   --  deleting them from Source, and overwriting overlapping ranges.
+
+   function To_Vector (Item : in Element_Type; Count : in 
Ada.Containers.Count_Type := 1) return Vector;
+
+   function "+" (Element : in Element_Type) return Vector;
+
+   function "&" (Left, Right : in Element_Type) return Vector;
+   function "&" (Left : in Vector; Right : in Element_Type) return Vector;
+
+   procedure Set_First (Container : in out Vector; First : in Index_Type);
+   procedure Set_Last (Container : in out Vector; Last : in Extended_Index);
+   procedure Set_First_Last (Container : in out Vector; First : in Index_Type; 
Last : in Extended_Index);
+   --  Default First is Index_Type'First.
+   --  Elements with First <= index <= Last that have not been set have
+   --  Element_Type default value.
+
+   procedure Set_Length (Container : in out Vector; Length : in 
Ada.Containers.Count_Type);
+   --  Set Last so Container.Length returns Length. New elements have
+   --  Element_Type default value.
+
+   procedure Set_Length
+     (Container : in out Vector;
+      Length    : in     Ada.Containers.Count_Type;
+      Default   : in     Element_Type);
+   --  Set Last so Container.Length returns Length. New elements have
+   --  Default value.
+
+   procedure Delete (Container : in out Vector; Index : in Index_Type);
+   --  Replace Index element contents with default. If Index =
+   --  Container.Last_Index, Container.Last_Index is decremented.
+
+   function Contains (Container : in Vector; Element : in Element_Type) return 
Boolean;
+   --  Return True if Element is in Container, False if not.
+
+   type Constant_Reference_Type (Element : not null access constant 
Element_Type) is null record
+   with Implicit_Dereference => Element;
+
+   function Constant_Ref (Container : aliased in Vector; Index : in 
Index_Type) return Constant_Reference_Type
+   with Pre => Index >= Container.First_Index and Index <= 
Container.Last_Index;
+
+   type Variable_Reference_Type (Element : not null access Element_Type) is 
null record
+   with Implicit_Dereference => Element;
+
+   function Variable_Ref (Container : aliased in Vector; Index : in 
Index_Type) return Variable_Reference_Type
+   with Pre => Index >= Container.First_Index and Index <= 
Container.Last_Index;
+
+   type Cursor is private;
+
+   No_Element : constant Cursor;
+
+   function Has_Element (Position : Cursor) return Boolean;
+   function Element (Position : Cursor) return Element_Type
+   with Pre => Position /= No_Element;
+   function First (Container : aliased in Vector) return Cursor;
+   function Next (Position : in Cursor) return Cursor;
+   procedure Next (Position : in out Cursor);
+
+   function To_Cursor
+     (Container : aliased in Vector;
+      Index     :         in Extended_Index)
+     return Cursor;
+
+   function To_Index (Position : in Cursor) return Extended_Index;
+
+   package Iterator_Interfaces is new Ada.Iterator_Interfaces (Cursor, 
Has_Element);
+
+   function Iterate (Container : aliased in Vector) return 
Iterator_Interfaces.Reversible_Iterator'Class;
+
+   function Constant_Ref (Container : aliased in Vector; Position : in Cursor) 
return Constant_Reference_Type
+   with Pre => Has_Element (Position);
+
+   function Variable_Ref (Container : aliased in Vector; Position  : in 
Cursor) return Variable_Reference_Type
+   with Pre => Has_Element (Position);
+
+private
+
+   type Array_Type is array (SAL.Peek_Type range <>) of aliased Element_Type;
+   type Array_Access is access Array_Type;
+   procedure Free is new Ada.Unchecked_Deallocation (Array_Type, Array_Access);
+
+   type Vector is new Ada.Finalization.Controlled with
+   record
+      Elements : Array_Access;
+      First    : Extended_Index := No_Index;
+      Last     : Extended_Index := No_Index;
+   end record;
+
+   type Vector_Access is access constant Vector;
+   for Vector_Access'Storage_Size use 0;
+
+   type Cursor is record
+      Container : Vector_Access  := null;
+      Index     : Base_Peek_Type := Invalid_Peek_Index;
+   end record;
+
+   type Iterator is new Iterator_Interfaces.Reversible_Iterator with
+   record
+      Container : Vector_Access;
+   end record;
+
+   overriding function First (Object : Iterator) return Cursor;
+   overriding function Last  (Object : Iterator) return Cursor;
+
+   overriding function Next
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+   overriding function Previous
+     (Object   : Iterator;
+      Position : Cursor) return Cursor;
+
+   Empty_Vector : constant Vector := (Ada.Finalization.Controlled with others 
=> <>);
+
+   No_Element : constant Cursor := (others => <>);
+
+   ----------
+   --  Visible for child package
+
+   function To_Peek_Type (Item : in Extended_Index) return Base_Peek_Type with 
Inline;
+
+end SAL.Gen_Unbounded_Definite_Vectors;
diff --git a/sal.adb b/sal.adb
new file mode 100644
index 0000000..6c5013e
--- /dev/null
+++ b/sal.adb
@@ -0,0 +1,32 @@
+--  Abstract:
+--
+--  See spec.
+--
+--  Copyright (C) 1997 - 2004, 2006, 2009 Stephen Leake.  All Rights Reserved.
+--
+--  SAL 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. SAL 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 SAL; 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
+--  SAL, or you link SAL object files with other files to produce
+--  an executable, that 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.
+--
+package body SAL is
+
+   function Version return String is
+   begin
+      return "SAL 2.01";
+   end Version;
+
+end SAL;
diff --git a/sal.ads b/sal.ads
new file mode 100644
index 0000000..0d5a4e6
--- /dev/null
+++ b/sal.ads
@@ -0,0 +1,75 @@
+--  Abstract:
+--
+--  Root package for Stephe's Ada Library packages.
+--
+--  See sal.html for more information.
+--
+--  See http://stephe-leake.org/ada/sal.html for the
+--  latest version.
+--
+--  Contact Stephe at stephen_leake@stephe-leake.org.
+--
+--  Copyright (C) 1997 - 2004, 2008, 2009, 2015, 2017, 2018 Stephen Leake.  
All Rights Reserved.
+--
+--  SAL 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. SAL 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 SAL; 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
+--  SAL, or you link SAL object files with other files to produce
+--  an executable, that 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);
+
+with Ada.Containers;
+
+package SAL is
+   pragma Pure;
+
+   function Version return String;
+   --  Returns string with format "SAL x.xx".
+
+   Container_Empty                  : exception;
+   Container_Full                   : exception;
+   Config_File_Error                : exception;
+   Domain_Error                     : exception;
+   Duplicate_Key                    : exception;
+   Initialization_Error             : exception;
+   Invalid_Format                   : exception;
+   Invalid_Limit                    : exception;
+   Invalid_Operation                : exception;
+   Invalid_Range                    : exception;
+   Iterator_Error                   : exception;
+   Not_Found                        : exception;
+   Not_Implemented                  : exception;
+   Parameter_Error                  : exception;
+   Programmer_Error                 : exception;
+   Range_Error                      : exception;
+
+   --------------
+   --  General options
+
+   type Direction_Type is (Forward, Backward);
+
+   type Duplicate_Action_Type is (Allow, Ignore, Error);
+
+   type Overflow_Action_Type is (Overwrite, Error);
+
+   --  We use a new type for Peek_Type, not just
+   --  Ada.Containers.Count_Type, to enforce Peek_Type'First = top/first.
+   type Base_Peek_Type is new Ada.Containers.Count_Type range 0 .. 
Ada.Containers.Count_Type'Last;
+   subtype Peek_Type is Base_Peek_Type range 1 .. Base_Peek_Type'Last;
+   Invalid_Peek_Index : constant Base_Peek_Type := 0;
+
+   type Compare_Result is (Less, Equal, Greater);
+end SAL;
diff --git a/standard_common.gpr b/standard_common.gpr
new file mode 100644
index 0000000..82e2fc9
--- /dev/null
+++ b/standard_common.gpr
@@ -0,0 +1,118 @@
+-- Standard settings for all of Stephe's Ada projects.
+project Standard_Common is
+   for Source_Dirs use ();
+
+   type Build_Type is ("Debug", "Normal");
+   Build : Build_Type := External ("Standard_Common_Build", "Normal");
+
+   type Profile_Type is ("On", "Off");
+   Profile : Profile_Type := External ("Standard_Common_Profile", "Off");
+
+   type Mem_Check_Type is ("On", "Off");
+   Mem_Check : Profile_Type := External ("Standard_Common_Mem_Check", "Off");
+
+   package Compiler is
+      -- Switches for gcc
+
+      Base_Style_Checks := "-gnaty3abcefhiklnOprtx";
+      Line_Length := "-gnatyM120";
+
+      Style_Checks := (Base_Style_Checks, Line_Length);
+
+      Common_Switches :=
+        (
+         "-fstack-check",
+         "-g",
+         "-gnat2012",
+         "-gnatfoqQ",
+         "-gnatw.d",
+         "-gnatwaBCeJL",
+         "-gnatyO"
+        );
+
+      --  -gnatVa causes some inline procedures to be non-inlineable;
+      --  suppress that warning with -gnatwP.
+      Debug_Switches := Common_Switches &
+        (
+         "-O0",
+         "-gnata",  -- assertions, pre/post-conditions
+         "-gnatVa", -- validity checks
+         "-gnateE", -- extra info in exceptions
+         "-gnatwaP"
+        );
+
+      --  -O3 is measurably faster than -O2 for wisitoken generate
+      --  LR1. We include -fstack-check because it catches
+      --  hard-to-find bugs, and the processors are so fast.
+      --  -fno-var-tracking-assignments speeds compiling of large
+      --  files; var tracking is only useful for debugging.
+      Base_Release_Switches := Common_Switches &
+        (
+         "-O3",
+         "-fno-var-tracking-assignments",
+         "-gnatyO"
+        );
+
+      Inlining := ("-gnatn");
+
+      Release_Switches := Base_Release_Switches & Inlining;
+
+      -- No -ansi; GNAT 7.1 compiler C header files are mingw 64, which don't 
support -ansi
+      Debug_Switches_C := ("-Wall", "-Wstrict-prototypes", "-pedantic", 
"-Werror", "-g", "-O0", "-funwind-tables");
+      Debug_Switches_C_Non_Pedantic := ("-Wall", "-Wstrict-prototypes", 
"-Werror", "-g", "-O0", "-funwind-tables");
+      Release_Switches_C := ("-Wall", "-Wstrict-prototypes", "-pedantic", 
"-Werror", "-g", "-O2", "-funwind-tables");
+      Release_Switches_C_Non_Pedantic := ("-Wall", "-Wstrict-prototypes", 
"-Werror", "-g", "-O2", "-funwind-tables");
+
+   end Compiler;
+
+   --  In project files, normally use this:
+   --  package Compiler is
+   --  for Default_Switches ("Ada") use
+   --     Standard_Common.Compiler.Release_Switches &
+   --     Standard_Common.Compiler.Style_Checks;
+   --  end Compiler;
+
+   package Builder is
+      --  Switches for gnatmake
+      for Default_Switches ("Ada") use ("-C");
+   end Builder;
+
+   --  In project files, normally use this:
+   --  package Builder is
+   --     for Default_Switches ("Ada") use 
Standard_Common.Builder'Default_Switches ("Ada");
+   --  end Builder;
+
+   package Binder is
+      --  Switches for gnatbind
+      for Default_Switches ("Ada") use ("-E");
+
+      Debug_Configuration_Pragmas := "Normalize_Scalars";
+   end Binder;
+
+   --  In project files, normally use this:
+   --  package Binder is
+   --     for Default_Switches ("Ada") use 
Standard_Common.Binder'Default_Switches ("Ada");
+   --  end Binder;
+
+   package Linker is
+      case Profile is
+      when "On" =>
+         case Mem_Check is
+         when "On" =>
+            for Linker_Options use ("-pg", "-lgmem");
+         when "Off" =>
+            for Linker_Options use ("-pg");
+         end case;
+
+      when "Off" =>
+         case Mem_Check is
+         when "On" =>
+            for Linker_Options use ("-lgmem");
+         when "Off" =>
+            null;
+         end case;
+      end case;
+   end Linker;
+
+   --  In project files, no linker package is needed.
+end Standard_Common;
diff --git a/wisi-compat-24.2.el b/wisi-compat-24.2.el
deleted file mode 100644
index 03bae80..0000000
--- a/wisi-compat-24.2.el
+++ /dev/null
@@ -1,34 +0,0 @@
-;;; wisi-compat-24.2.el --- Implement current Emacs features not present in 
Emacs 24.2  -*- lexical-binding:t -*-
-
-;; Copyright (C) 2014-2015 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/>.
-
-;; using cl-lib 0.4 from Gnu ELPA
-
-(when (not (boundp 'defvar-local))
-  (defmacro defvar-local (var val &optional docstring)
-    "Define VAR as a buffer-local variable with default value VAL.
-Like `defvar' but additionally marks the variable as being automatically
-buffer-local wherever it is set."
-    (declare (debug defvar) (doc-string 3))
-    ;; Can't use backquote here, it's too early in the bootstrap.
-    (list 'progn (list 'defvar var val docstring)
-         (list 'make-variable-buffer-local (list 'quote var))))
-  )
-
-(provide 'wisi-compat-24.2)
-;;; wisi-compat-24.2.el ends here
diff --git a/wisi-compile.el b/wisi-compile.el
index 420621a..9788938 100644
--- a/wisi-compile.el
+++ b/wisi-compile.el
@@ -1,6 +1,6 @@
 ;; wisi-compile.el --- Grammar compiler for the wisi parser, integrating Wisi 
OpenToken output.  -*- lexical-binding:t -*-
 ;;
-;; Copyright (C) 2012-2013, 2015-2017 Free Software Foundation, Inc.
+;; Copyright (C) 2012-2017 Free Software Foundation, Inc.
 ;;
 ;; Author: Stephen Leake <stephen_leake@member.fsf.org>
 ;;
@@ -117,7 +117,7 @@ NONTERM is the nonterminal left hand side.
 IACTN is the index of the production in the NTERM rule.
 
 The semantic action function accepts two arguments;
-- $nterm      : the nonterminal
+- wisi-nterm  : the nonterminal
 - wisi-tokens : the list of tokens to be reduced.
 
 It returns nil; it is called for the semantic side-effects only."
@@ -126,16 +126,14 @@ It returns nil; it is called for the semantic 
side-effects only."
         (action-symbol (intern name symbol-obarray)))
 
     (fset action-symbol
-         `(lambda ($nterm wisi-tokens)
+         `(lambda (wisi-nterm wisi-tokens)
             ,form
             nil))
     (byte-compile action-symbol)))
 
 (defun wisi-compile-grammar (grammar)
   "Compile the LALR(1) GRAMMAR; return the automaton for wisi-parse.
-GRAMMAR is a list TERMINALS NONTERMS ACTIONS GOTOS, where:
-
-TERMINALS is a list of terminal token symbols.
+GRAMMAR is a list NONTERMS ACTIONS GOTOS, where:
 
 NONTERMS is a list of productions; each production is a
 list (nonterm (tokens semantic-action) ...) where `semantic-action' is
@@ -151,10 +149,10 @@ terminal tokens. The value of each item in the alists is 
one of:
 
 integer - shift; gives new state
 
-(nonterm . index) - reduce by nonterm production index.
+ (nonterm . index) - reduce by nonterm production index.
 
-(integer (nonterm . index)) - a shift/reduce conflict
-((nonterm . index) (nonterm . index)) - a reduce/reduce conflict
+ (integer (nonterm . index)) - a shift/reduce conflict
+ ((nonterm . index) (nonterm . index)) - a reduce/reduce conflict
 
 The first item in the alist must have the key `default' (not a
 terminal token); it is used when no other item matches the
@@ -185,14 +183,8 @@ implement the semantic action for each nonterminal; the 
function
 names have the format nonterm:index."
   ;; We store named symbols for semantic actions, not just lambda
   ;; functions, so we have a name for debug trace.
-  ;;
-  ;; FIXME: TERMINALS is not used. Eliminating it requires decoupling
-  ;; from OpenToken; we'll do that in the move to FastToken.
-  ;;
-  ;; FIXME: eliminate use of semantic-lex-* in *-wy.el. Similarly
-  ;; requires decoupling from OpenToken
-
-  (let ((defs (nth 1 grammar))
+
+  (let ((defs (nth 0 grammar))
        (symbol-obarray (make-vector 13 0));; for parse actions
         (byte-compile-warnings '(not free-vars)) ;; for "wisi-test-success" in 
test/wisi/*
        def nonterm rhs-list rule
@@ -216,16 +208,16 @@ names have the format nonterm:index."
        ))
 
     ;; replace semantic actions in ACTIONS with symbols from symbol-obarray
-    (let ((nactions (length (nth 2 grammar)))
-         (actions (nth 2 grammar))
+    (let ((nactions (length (nth 1 grammar)))
+         (actions (nth 1 grammar))
          (i 0))
       (while (< i nactions)
        (aset actions i
-             (wisi-replace-actions (aref actions i) symbol-obarray (nth 1 
grammar)))
+             (wisi-replace-actions (aref actions i) symbol-obarray (nth 0 
grammar)))
        (setq i (1+ i)))
       (vector
        actions
-       (nth 3 grammar)
+       (nth 2 grammar)
        symbol-obarray)
       )))
 
diff --git a/wisi-elisp-lexer.el b/wisi-elisp-lexer.el
new file mode 100644
index 0000000..b0d1e3c
--- /dev/null
+++ b/wisi-elisp-lexer.el
@@ -0,0 +1,393 @@
+;;; wisi-elisp-lexer.el --- A lexer for wisi, implemented in elisp -*- 
lexical-binding:t -*-
+;;
+;; Copyright (C) 2017, 2018  Free Software Foundation, Inc.
+;;
+;; Author: 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/>.
+;;
+
+;;; Commentary:
+
+;;;; History: see NEWS-wisi.text
+
+(require 'cl-lib)
+(require 'semantic/lex)
+(require 'wisi-parse-common)
+
+(cl-defstruct wisi-elisp-lexer
+  id-alist ;; alist mapping strings to token ids; used by repair error
+  keyword-table ;; obarray holding keyword tokens
+  punctuation-table ;; obarray holding punctuation tokens
+  punctuation-table-max-length ;; max string length in punctuation-table
+  string-double-term ;; non-nil if strings delimited by double quotes
+  string-quote-escape-doubled ;; Non-nil if a string delimiter is escaped by 
doubling it
+  string-quote-escape
+  ;; Cons (delim . character) where `character' escapes quotes in strings 
delimited by `delim'.
+  string-single-term ;; non-nil if strings delimited by single quotes
+  symbol-term ;; symbol for a terminal symbol token
+  number-term ;; symbol for a terminal number literal token
+  number-p ;; function that determines if argument is a number literal
+  line-begin ;; vector of beginning-of-line positions in buffer
+  last-line ;; index into line-begin of line containing last lexed token
+  )
+
+(defun wisi-elisp-lexer-reset (line-count lexer)
+  "Reset lexer to start a new parse. LINE-COUNT is the count of lines in the 
current buffer."
+  (setf (wisi-elisp-lexer-line-begin lexer) (wisi--set-line-begin line-count))
+  (setf (wisi-elisp-lexer-last-line lexer) nil))
+
+(defvar-local wisi--lexer nil
+  "A `wisi-elisp-lexer' struct defining the lexer for the current buffer.")
+
+(defun wisi-elisp-lexer--safe-intern (name obtable)
+  (let ((var (intern-soft name obtable)))
+    (and (boundp var) (symbol-value var))))
+
+(cl-defun wisi-make-elisp-lexer (&key token-table-raw keyword-table-raw 
string-quote-escape-doubled string-quote-escape)
+  "Return a ‘wisi-elisp-lexer’ object."
+  (let* ((token-table (semantic-lex-make-type-table token-table-raw))
+        (keyword-table (semantic-lex-make-keyword-table keyword-table-raw))
+        (left-paren (cadr (wisi-elisp-lexer--safe-intern "left-paren" 
token-table)))
+        (right-paren (cadr (wisi-elisp-lexer--safe-intern "right-paren" 
token-table)))
+        (punctuation-table (wisi-elisp-lexer--safe-intern "punctuation" 
token-table))
+        (punct-max-length 0)
+        (number (cadr (wisi-elisp-lexer--safe-intern "number" token-table)))
+        (symbol (cadr (wisi-elisp-lexer--safe-intern "symbol" token-table)))
+        (string-double (cadr (wisi-elisp-lexer--safe-intern "string-double" 
token-table)))
+        (string-single (cadr (wisi-elisp-lexer--safe-intern "string-single" 
token-table)))
+        id-alist
+        fail)
+    (dolist (item punctuation-table)
+      ;; check that all chars used in punctuation tokens have punctuation 
syntax
+      (mapc (lambda (char)
+             (when (not (= ?. (char-syntax char)))
+               (setq fail t)
+               (message "in %s, %c does not have punctuation syntax"
+                        (car item) char)))
+           (cdr item))
+
+      ;; accumulate max length
+      (when (< punct-max-length (length (cdr item)))
+       (setq punct-max-length (length (cdr item))))
+
+      ;; build id-alist
+      (push item id-alist)
+      )
+
+    (when fail
+      (error "aborting due to punctuation errors"))
+
+    (when number
+      (push (cons (nth 0 number) "1234") id-alist)
+      (when (nth 2 number)
+       (require (nth 2 number)))) ;; for number-p
+
+    (when left-paren
+      (push left-paren id-alist)
+      (set (intern (cdr left-paren) keyword-table) (car left-paren)))
+    (when right-paren
+      (push right-paren id-alist)
+      (set (intern (cdr right-paren) keyword-table) (car right-paren)))
+
+    (when symbol
+      (push (cons (car symbol) "a_bogus_identifier") id-alist))
+
+    (when string-double
+      (push (cons (car string-double) "\"\"") id-alist))
+
+    (when string-single
+      (push (cons (car string-single) "''") id-alist))
+
+    (dolist (item keyword-table-raw)
+      (push (cons (cdr item) (car item)) id-alist))
+
+    (make-wisi-elisp-lexer
+     :id-alist id-alist
+     :keyword-table keyword-table
+     :punctuation-table punctuation-table
+     :punctuation-table-max-length punct-max-length
+     :string-double-term (car string-double)
+     :string-quote-escape-doubled string-quote-escape-doubled
+     :string-quote-escape string-quote-escape
+     :string-single-term (car string-single)
+     :symbol-term (car symbol)
+     :number-term (nth 0 number)
+     :number-p (nth 1 number)
+     )
+    ))
+
+(defun wisi-number-p (token-text)
+  ;; Not ’wisi-elisp-lexer-number-p’, because this can appear in grammar files.
+  "Return t if TOKEN-TEXT plus text after point matches the
+syntax for a real literal; otherwise nil.  Point is after
+TOKEN-TEXT; move point to just past token."
+  ;; Typical literals:
+  ;; 1234
+  ;; 1234.5678
+  ;; _not_ including non-decimal base, or underscores (see ada-wisi-number-p)
+  ;;
+  ;; Starts with a simple integer
+  (when (string-match "^[0-9]+$" token-text)
+    (when (looking-at "\\.[0-9]+")
+      ;; real number
+      (goto-char (match-end 0))
+      (when (looking-at  "[Ee][+-][0-9]+")
+        ;; exponent
+        (goto-char (match-end 0))))
+
+    t
+    ))
+
+(defun wisi-forward-token ()
+  ;; Not ’wisi-elisp-lexer-forward-token’, for backward compatibility
+  "Move point forward across one token, then skip whitespace and comments.
+Return the corresponding token as a `wisi-tok'.
+If at whitespace or comment, throw an error.
+If at end of buffer, return `wisi-eoi-term'."
+  (let ((start (point))
+       ;; (info "(elisp)Syntax Table Internals" "*info elisp syntax*")
+       end
+       (syntax (syntax-class (syntax-after (point))))
+       (first nil)
+       (comment-line nil)
+       (comment-end nil)
+       token-id token-text line)
+    (cond
+     ((eobp)
+      (setq token-id wisi-eoi-term))
+
+     ((eq syntax 1)
+      ;; punctuation. Find the longest matching string in 
wisi-punctuation-table
+      (forward-char 1)
+      (let ((next-point (point))
+           temp-text temp-id done)
+       (while (not done)
+         (setq temp-text (buffer-substring-no-properties start (point)))
+         (setq temp-id (car (rassoc temp-text 
(wisi-elisp-lexer-punctuation-table wisi--lexer))))
+         (when temp-id
+           (setq token-id temp-id
+                 next-point (point)))
+         (if (or
+              (eobp)
+              (= (- (point) start) 
(wisi-elisp-lexer-punctuation-table-max-length wisi--lexer)))
+             (setq done t)
+           (forward-char 1))
+         )
+       (goto-char next-point)))
+
+     ((memq syntax '(4 5)) ;; open, close parenthesis
+      (forward-char 1)
+      (setq token-text (buffer-substring-no-properties start (point)))
+      (setq token-id (symbol-value (intern-soft token-text 
(wisi-elisp-lexer-keyword-table wisi--lexer)))))
+
+     ((eq syntax 7)
+      ;; string quote, either single or double. we assume point is
+      ;; before the start quote, not the end quote
+      (let ((delim (char-after (point)))
+           (forward-sexp-function nil))
+       (condition-case err
+           (progn
+             (forward-sexp)
+
+             ;; point is now after the end quote; check for an escaped quote
+             (while (or
+                     (and (wisi-elisp-lexer-string-quote-escape-doubled 
wisi--lexer)
+                          (eq (char-after (point)) delim))
+                     (and (eq delim (car (wisi-elisp-lexer-string-quote-escape 
wisi--lexer)))
+                          (eq (char-before (1- (point))) (cdr 
(wisi-elisp-lexer-string-quote-escape wisi--lexer)))))
+               (forward-sexp))
+             (setq token-id (if (= delim ?\")
+                                (wisi-elisp-lexer-string-double-term 
wisi--lexer)
+                              (wisi-elisp-lexer-string-single-term 
wisi--lexer))))
+         (scan-error
+          ;; Something screwed up; we should not get here if
+          ;; syntax-propertize works properly.
+          (signal 'wisi-parse-error (format "wisi-forward-token: forward-sexp 
failed %s" err))
+          ))))
+
+     ((memq syntax '(2 3 6)) ;; word, symbol, expression prefix (includes 
numbers)
+      (skip-syntax-forward "w_'")
+      (setq token-text (buffer-substring-no-properties start (point)))
+      (setq token-id
+           (or (symbol-value (intern-soft (downcase token-text) 
(wisi-elisp-lexer-keyword-table wisi--lexer)))
+               (and (functionp (wisi-elisp-lexer-number-p wisi--lexer))
+                    (funcall (wisi-elisp-lexer-number-p wisi--lexer) 
token-text)
+                    (setq token-text (buffer-substring-no-properties start 
(point)))
+                    (wisi-elisp-lexer-number-term wisi--lexer))
+               (wisi-elisp-lexer-symbol-term wisi--lexer)))
+      )
+
+     (t
+      (signal 'wisi-parse-error (format "wisi-forward-token: unsupported 
syntax %s" syntax)))
+
+     );; cond
+
+    (unless token-id
+      (signal 'wisi-parse-error
+             (wisi-error-msg "unrecognized token '%s'" 
(buffer-substring-no-properties start (point)))))
+
+    (setq end (point))
+
+    (forward-comment (point-max))
+
+    (when (and (not (eq token-id wisi-eoi-term))
+              (eq wisi--parse-action 'indent))
+      ;; parsing for indent; track line numbers
+
+      (if (wisi-elisp-lexer-last-line wisi--lexer)
+         (progn
+           (setq line (wisi-elisp-lexer-last-line wisi--lexer))
+           (when (>= start (aref (wisi-elisp-lexer-line-begin wisi--lexer) 
line))
+             ;; first token on next non-blank line
+             (setq line (1+ line))
+             (setq first t))
+           ;; else other token on line
+           )
+
+       ;; First token on first non-comment line
+       (setq line (line-number-at-pos start))
+       (setq first t)
+       )
+      (setf (wisi-elisp-lexer-last-line wisi--lexer) line)
+
+      ;; set comment-line, comment-end
+      (when (and (< (1+ (wisi-elisp-lexer-last-line wisi--lexer)) (length 
(wisi-elisp-lexer-line-begin wisi--lexer)))
+                (>= (point) (aref (wisi-elisp-lexer-line-begin wisi--lexer)
+                                (1+ (wisi-elisp-lexer-last-line 
wisi--lexer)))))
+       (setq comment-line (1+ (wisi-elisp-lexer-last-line wisi--lexer)))
+       (setf (wisi-elisp-lexer-last-line wisi--lexer) comment-line)
+       (setq comment-end (line-end-position 0)))
+
+      ;; count blank or comment lines following token
+      (when comment-end
+       (while (and (< (1+ (wisi-elisp-lexer-last-line wisi--lexer)) (length 
(wisi-elisp-lexer-line-begin wisi--lexer)))
+                   (>= comment-end (aref (wisi-elisp-lexer-line-begin 
wisi--lexer) (wisi-elisp-lexer-last-line wisi--lexer))))
+         (setf (wisi-elisp-lexer-last-line wisi--lexer) (1+ 
(wisi-elisp-lexer-last-line wisi--lexer))))
+
+      ))
+
+    (make-wisi-tok
+     :token token-id
+     :region (cons start end)
+     :line line
+     :first first
+     :comment-end comment-end
+     :comment-line comment-line)
+    ))
+
+(defun wisi-backward-token ()
+  ;; Not ’wisi-elisp-lexer-backward-token’, for backward compatibility
+  "Move point backward across one token, skipping whitespace and comments.
+Does _not_ handle numbers with wisi-number-p; just sees
+lower-level syntax.  Return a `wisi-tok' - same structure as
+wisi-forward-token, but only sets token-id and region."
+  (forward-comment (- (point)))
+  ;; skips leading whitespace, comment, trailing whitespace.
+
+  ;; (info "(elisp)Syntax Table Internals" "*info elisp syntax*")
+  (let ((end (point))
+       (syntax (syntax-class (syntax-after (1- (point)))))
+       token-id token-text)
+    (cond
+     ((bobp) nil)
+
+     ((eq syntax 1)
+      ;; punctuation. Find the longest matching string in 
wisi-lex-punctuation-table
+      (backward-char 1)
+      (let ((next-point (point))
+           temp-text temp-id done)
+       (while (not done)
+         (setq temp-text (buffer-substring-no-properties (point) end))
+         (when (setq temp-id (car (rassoc temp-text 
(wisi-elisp-lexer-punctuation-table wisi--lexer))))
+           (setq token-id temp-id)
+           (setq next-point (point)))
+         (if (or
+              (bobp)
+              (= (- end (point)) 
(wisi-elisp-lexer-punctuation-table-max-length wisi--lexer)))
+             (setq done t)
+           (backward-char 1))
+         )
+       (goto-char next-point))
+      )
+
+     ((memq syntax '(4 5)) ;; open, close parenthesis
+      (backward-char 1)
+      (setq token-id
+           (symbol-value
+            (intern-soft (buffer-substring-no-properties (point) end)
+                         (wisi-elisp-lexer-keyword-table wisi--lexer)))))
+
+     ((eq syntax 7)
+      ;; a string quote. we assume we are after the end quote, not the start 
quote
+      (let ((delim (char-after (1- (point))))
+           (forward-sexp-function nil))
+       (forward-sexp -1)
+       (setq token-id (if (= delim ?\")
+                          (wisi-elisp-lexer-string-double-term wisi--lexer)
+                        (wisi-elisp-lexer-string-single-term wisi--lexer)))
+       ))
+
+     (t ;; assuming word or symbol syntax
+      (if (zerop (skip-syntax-backward "."))
+         (skip-syntax-backward "w_'"))
+      (setq token-text (buffer-substring-no-properties (point) end))
+      (setq token-id
+           (or (symbol-value (intern-soft (downcase token-text) 
(wisi-elisp-lexer-keyword-table wisi--lexer)))
+               (and (functionp (wisi-elisp-lexer-number-p wisi--lexer))
+                    (funcall (wisi-elisp-lexer-number-p wisi--lexer) 
token-text)
+                    (setq token-text (buffer-substring-no-properties (point) 
end))
+                    (wisi-elisp-lexer-number-term wisi--lexer))
+               (wisi-elisp-lexer-symbol-term wisi--lexer))))
+     )
+
+    (make-wisi-tok
+     :token token-id
+     :region (cons (point) end))
+    ))
+
+;;;; Debugging
+
+(defun wisi-lex-buffer (&optional parse-action)
+  ;; for timing the lexer
+  (interactive)
+  (when (< emacs-major-version 25) (syntax-propertize (point-max)))
+
+  (let* ((wisi--parse-action (or parse-action 'indent))
+        (line-count (1+ (count-lines (point-min) (point-max))))
+        )
+
+    (cl-case wisi--parse-action
+      (indent
+       (setf (wisi-elisp-lexer-last-line wisi--lexer) nil)
+       (setf (wisi-elisp-lexer-line-begin wisi--lexer) (wisi--set-line-begin 
line-count)))
+      (t nil))
+
+    (goto-char (point-min))
+    (while (forward-comment 1))
+    (while (not (eq wisi-eoi-term (wisi-tok-token (wisi-forward-token)))))
+    ))
+
+(defun wisi-show-token ()
+  "Move forward across one keyword, show token."
+  (interactive)
+  (let* ((wisi--parse-action nil)
+        (token (wisi-forward-token)))
+    (message "%s" token)))
+
+
+(provide 'wisi-elisp-lexer)
+;;; end of file
diff --git a/wisi-elisp-parse.el b/wisi-elisp-parse.el
new file mode 100644
index 0000000..83ddba2
--- /dev/null
+++ b/wisi-elisp-parse.el
@@ -0,0 +1,1682 @@
+;; wisi-elisp-parse.el --- Wisi parser  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2013-2015, 2017 - 2018  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
+  ;; 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))
+  "Parse current buffer from beginning."
+
+  (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))
+    ))
+
+(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
diff --git a/wisi-fringe.el b/wisi-fringe.el
new file mode 100644
index 0000000..7f1c10b
--- /dev/null
+++ b/wisi-fringe.el
@@ -0,0 +1,146 @@
+;;; wisi-fringe.el --- show approximate error locations in the fringe
+;;
+;; Copyright (C) 2018  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/>.
+;;
+;; Design:
+;;
+;; Bitmaps are displayed in the fringe by putting a 'display property
+;; on buffer text. However, just doing that also hides the buffer
+;; text. To avoid that, we put the ’display property on a string, and
+;; then an overlay containing that string as ’before-string or
+;; ’after-string on the newline of a buffer line.
+;;
+;; We show approximate error positions in the entire buffer with
+;; single-pixel lines in the right fringe, and mark error lines with
+;; ’!!’ in the left fringe.
+
+(defun wisi-fringe-create-bitmaps ()
+  "Return an array of bitmap symbols containing the fringe bitmaps."
+  ;; First create the ’!!’ bitmap.
+  (define-fringe-bitmap 'wisi-fringe--double-exclaim-bmp
+    (vector
+     #b00000000
+     #b01100110
+     #b01100110
+     #b01100110
+     #b01100110
+     #b01100110
+     #b00000000
+     #b01100110
+     #b01010110
+     #b00000000))
+
+  ;; In condensing the entire buffer to the current window height, we
+  ;; assume a 10 point font, which allows 6 distinct line positions
+  ;; each one pixel high, with one blank pixel between.
+
+  (let ((result (make-vector 64 nil))
+       (i 1))
+    (while (<= i (length result))
+      (aset result (1- i)
+           (define-fringe-bitmap (intern (format "wisi-fringe--line-%d-bmp" i))
+             (vector
+              (if (>= i 32) #b11111111 #b00000000)
+              #b00000000
+              (if (>= (% i 32) 16) #b11111111 #b00000000)
+              #b00000000
+              (if (>= (% i 16) 8) #b11111111 #b00000000)
+              #b00000000
+              (if (>= (% i 8) 4) #b11111111 #b00000000)
+              #b00000000
+              (if (>= (% i 4) 2) #b11111111 #b00000000)
+              #b00000000
+              (if (>= (% i 2) 1) #b11111111 #b00000000)
+              )))
+      (setq i (1+ i)))
+    result))
+
+(defconst wisi-fringe-bitmaps (wisi-fringe-create-bitmaps)
+  "Array of 64 bitmap symbols.")
+
+(defun wisi-fringe--put-right (line bitmap-index)
+  (save-excursion
+    (goto-char (point-min))
+    (forward-line (1- line))
+    (let* ((endpos (line-end-position))
+          (ov (make-overlay endpos (1+ endpos)))
+          (bmp (aref wisi-fringe-bitmaps bitmap-index)))
+      (overlay-put ov 'after-string (propertize "-" 'display (list 
'right-fringe bmp 'compilation-error)))
+      (overlay-put ov 'wisi-fringe t)
+      )))
+
+(defun wisi-fringe--put-left (line)
+  (save-excursion
+    (goto-char (point-min))
+    (forward-line (1- line))
+    (let* ((endpos (line-end-position))
+          (ov (make-overlay endpos (1+ endpos)))
+          (bmp 'wisi-fringe--double-exclaim-bmp))
+      (overlay-put ov 'before-string (propertize "-" 'display (list 
'left-fringe bmp 'compilation-error)))
+      (overlay-put ov 'wisi-fringe t)
+      )))
+
+(defun wisi-fringe--scale (error-line buffer-lines window-line-first 
window-lines)
+  "Return a cons (LINE . BIN) for ERROR-LINE,
+where LINE is the line to display the error bar on, and BIN is a
+6-bit bit vector giving the relative position in that line.
+BUFFER-LINES is the count of lines in the buffer.
+WINDOW-LINE-FIRST is the first and last lines of the buffer
+visible in the window. WINDOW-LINES is the count of lines visible
+in the window."
+  ;; If the end of buffer is inside the window, and this calculation
+  ;; puts a mark after that, it will actually be put on the last real
+  ;; line. That’s good enough for our purposes.
+
+  ;; partial-lines / window-line = 6
+  ;; buffer-lines / window-line = 1/scale
+  ;; buffer-lines / partial-line  = (window-line / partial-lines) * 
(buffer-lines / window-line) = 1/6 * 1/scale
+  (let* ((scale (/ window-lines (float buffer-lines)))
+        (line (floor (* scale error-line)))
+        (rem (- error-line (floor (/ line scale)))))
+    (cons (+ window-line-first line) (lsh 1 (min 5 (floor (* rem (* 6 
scale))))))))
+
+(defun wisi-fringe-display-errors (positions)
+  "Display a bar in the right fringe for each buffer position in POSITIONS.
+The buffer containing POSITIONS must be current, and the window
+displaying that buffer must be current."
+  ;; FIXME: recompute fringe display on scroll!
+  (remove-overlays (point-min) (point-max) 'wisi-fringe t)
+  (let (scaled-posns
+       (buffer-lines (line-number-at-pos (point-max)))
+       (window-lines (window-height))
+       (window-pos-first (window-start))
+       (window-pos-last  (window-end))
+       (window-line-first (line-number-at-pos (window-start))))
+    (dolist (pos positions)
+      (let* ((line (line-number-at-pos pos))
+            (scaled-pos (wisi-fringe--scale line buffer-lines 
window-line-first window-lines)))
+       (when (and (>= pos window-pos-first)
+                  (<= pos window-pos-last))
+         (wisi-fringe--put-left line))
+       (if (and scaled-posns
+                (= (caar scaled-posns) (car scaled-pos)))
+           (setcdr (car scaled-posns) (logior (cdar scaled-posns) (cdr 
scaled-pos)))
+         (push scaled-pos scaled-posns))
+       ))
+
+    (dolist (pos scaled-posns)
+      (wisi-fringe--put-right (car pos) (1- (cdr pos))))
+    ))
+
+(provide 'wisi-fringe)
diff --git a/wisi-parse-common.el b/wisi-parse-common.el
new file mode 100644
index 0000000..3aa2c92
--- /dev/null
+++ b/wisi-parse-common.el
@@ -0,0 +1,341 @@
+;;; wisi-parse-common.el --- declarations used by wisi-parse.el, 
wisi-ada-parse.el, and wisi.el
+;;
+;; Copyright (C) 2014, 2015, 2017, 2018  Free Software Foundation, Inc.
+;;
+;; Author: Stephen Leake <stephen_leake@member.fsf.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/>.
+
+;;; Code:
+
+(cl-defstruct (wisi--lexer-error)
+  pos ;; position (integer) in buffer where error was detected.
+  message  ;; string error message
+  inserted ;; char inserted after pos.
+  )
+
+(cl-defstruct (wisi--parse-error-repair)
+  pos ;; position (integer) in buffer where insert/delete is done.
+  inserted ;; list of token IDs that were inserted before pos
+  deleted  ;; list of token IDs that were deleted after pos
+  )
+
+(cl-defstruct (wisi--parse-error)
+  ;; Includes information derived from compiler error recovery to edit
+  ;; text to fix one error. Used by ’wisi-repair-error’ to edit buffer.
+  pos      ;; position (integer or marker) in buffer where error was detected.
+  message  ;; string error message
+  repair   ;; list of wisi--parse-error-repair.
+  )
+
+(cl-defstruct wisi-parser
+  ;; Separate lists for lexer and parse errors, because lexer errors
+  ;; must be repaired first, before parse errors can be repaired. And
+  ;; they have different structures.
+  lexer-errors
+  ;; list of wisi--lexer-errors from last parse.  Can be more than one if
+  ;; lexer supports error recovery.
+  parse-errors
+  ;; List of wisi--parse-errors from last parse. Can be more than one if
+  ;; parser supports error recovery.
+)
+
+(cl-defgeneric wisi-parse-format-language-options ((parser wisi-parser))
+  "Return a string to be sent to the parser, containing settings
+for the language-specific parser options."
+  ;; not needed for the elisp parser, which can see the options directly.
+  )
+
+(cl-defgeneric wisi-parse-current ((parser wisi-parser))
+  "Parse current buffer.")
+
+(cl-defgeneric wisi-parse-kill ((parser wisi-parser))
+  "Kill any external process associated with parser.")
+
+(cl-defgeneric wisi-parse-find-token ((parser wisi-parser) token-symbol)
+  "Find token with TOKEN-SYMBOL on current parser stack, return token struct.
+For use in grammar actions.")
+
+(cl-defgeneric wisi-parse-stack-peek ((parser wisi-parser) n)
+  "Return the Nth token on the parse stack.
+For use in grammar actions.")
+
+(cl-defstruct
+  (wisi-cache
+   (:constructor wisi-cache-create)
+   (:copier nil))
+  nonterm;; nonterminal from parse
+
+  token
+  ;; terminal symbol from wisi-keyword-table or
+  ;; wisi-punctuation-table, or lower-level nonterminal from parse
+
+  last ;; pos of last char in token, relative to first (0 indexed)
+
+  class ;; one of wisi-class-list
+
+  containing
+  ;; Marker at the start of the containing statement for this token.
+  ;; nil only for first token in buffer
+
+  prev ;; marker at previous motion token in statement; nil if none
+  next ;; marker at next motion token in statement; nil if none
+  end  ;; marker at token at end of current statement
+  )
+
+(defun wisi-get-cache (pos)
+  "Return `wisi-cache' struct from the `wisi-cache' text property at POS."
+  (get-text-property pos 'wisi-cache))
+
+(defun wisi-backward-cache ()
+  "Move point backward to the beginning of the first token preceding point 
that has a cache.
+Returns cache, or nil if at beginning of buffer."
+  ;; If point is not near cache, p-s-p-c will return pos just after
+  ;; cache, so 1- is the beginning of cache.
+  ;;
+  ;; If point is just after end of cache, p-s-p-c will return pos at
+  ;; start of cache.
+  ;;
+  ;; So we test for the property before subtracting 1.
+  (let ((pos (previous-single-property-change (point) 'wisi-cache))
+       cache)
+    (cond
+     ((null pos)
+      (goto-char (point-min))
+      nil)
+
+     ((setq cache (get-text-property pos 'wisi-cache))
+      (goto-char pos)
+      cache)
+
+     (t
+      (setq pos (1- pos))
+      (setq cache (get-text-property pos 'wisi-cache))
+      (goto-char pos)
+      cache)
+     )))
+
+(defun wisi-forward-cache ()
+  "Move point forward to the beginning of the first token after point that has 
a cache.
+Returns cache, or nil if at end of buffer."
+  (let (cache pos)
+    (when (get-text-property (point) 'wisi-cache)
+      ;; on a cache; get past it
+      (goto-char (1+ (point))))
+
+    (setq cache (get-text-property (point) 'wisi-cache))
+    (if cache
+       nil
+
+      (setq pos (next-single-property-change (point) 'wisi-cache))
+      (if pos
+         (progn
+           (goto-char pos)
+           (setq cache (get-text-property pos 'wisi-cache)))
+       ;; at eob
+       (goto-char (point-max))
+       (setq cache nil))
+      )
+    cache
+    ))
+
+(defun wisi-cache-region (cache &optional start)
+  "Return region designated by START (default point) to cache last."
+  (unless start (setq start (point)))
+  (cons start (+ start (wisi-cache-last cache))))
+
+(defvar wisi-debug 0
+  "wisi debug mode:
+0 : normal - ignore parse errors, for indenting new code
+1 : report parse errors (for running tests)
+2 : show parse states, position point at parse errors
+3 : also show top 10 items of parser stack.")
+
+;; The following parameters are easily changeable for debugging.
+(defvar wisi-action-disable nil
+  "If non-nil, disable all elisp actions during parsing.
+Allows timing parse separate from actions.")
+
+(defvar-local wisi-trace-mckenzie 0
+  "McKenzie trace level; 0 for none")
+
+(defvar-local wisi-trace-action 0
+  "Parse action trace level; 0 for none")
+
+(defvar-local wisi-mckenzie-disable nil
+  "If non-nil, disable McKenzie error recovery. Otherwise, use parser 
default.")
+
+(defcustom wisi-mckenzie-task-count nil
+  "If integer, sets McKenzie error recovery task count.
+Higher value (up to system processor limit) runs error recovery
+faster, but may encounter race conditions.  Using only one task
+makes error recovery repeatable; useful for tests.  If nil, uses
+value from grammar file."
+  :type 'integer
+  :group 'wisi
+  :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
+parsed successfully for a solution to be deemed successful.
+Higher value gives better solutions, but may fail if there are
+two errors close together.  If nil, uses value from grammar
+file."
+  :type 'integer
+  :group 'wisi
+  :safe 'integerp)
+(make-variable-buffer-local 'wisi-mckenzie-check-limit)
+
+(defcustom wisi-mckenzie-enqueue-limit nil
+  "If integer, sets McKenzie error recovery algorithm enqueue limit.
+This sets the maximum number of solutions that will be considered.
+Higher value has more recover power, but will be slower to fail.
+If nil, uses value from grammar file."
+  :type 'integer
+  :group 'wisi
+  :safe 'integerp)
+(make-variable-buffer-local 'wisi-mckenzie-enqueue-limit)
+
+(defvar 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.")
+
+(defvar wisi-parse-max-stack-size 500
+  "Maximum parse stack size.
+Larger stack size allows more deeply nested constructs.")
+;; end of easily changeable parameters
+
+(defvar wisi--parse-action nil
+  ;; not buffer-local; only let-bound in wisi-indent-region, 
wisi-validate-cache
+  "Reason current parse is begin run; one of
+{indent, face, navigate}.")
+
+(defvar-local wisi-indent-comment-col-0 nil
+  "If non-nil, comments currently starting in column 0 are left in column 0.
+Otherwise, they are indented with previous comments or code.
+Normally set from a language-specific option.")
+
+(defvar-local wisi-end-caches nil
+  "List of buffer positions of caches in current statement that need 
wisi-cache-end set.")
+
+(defconst wisi-eoi-term 'Wisi_EOI
+  ;; must match FastToken wisi-output_elisp.adb EOI_Name, which must
+  ;; be part of a valid Ada identifer.
+  "End Of Input token.")
+
+(defconst wisi-class-list
+  [motion ;; motion-action
+   name ;; for which-function
+   statement-end
+   statement-override
+   statement-start
+   misc ;; other stuff
+   ]
+  "array of valid token classes; checked in wisi-statement-action, used in 
wisi-process-parse.")
+
+(defun wisi-error-msg (message &rest args)
+  (let ((line (line-number-at-pos))
+       (col (- (point) (line-beginning-position))))
+    (format
+     "%s:%d:%d: %s"
+       (buffer-name) ;; buffer-file-name is sometimes nil here!?
+       line col
+       (apply 'format message args))))
+
+(defvar wisi-parse-error nil)
+(put 'wisi-parse-error
+     'error-conditions
+     '(error wisi-parse-error))
+(put 'wisi-parse-error
+     'error-message
+     "wisi parse error")
+
+(cl-defstruct wisi-tok
+  token  ;; symbol from a token table ;; IMPROVEME: rename to ’id’?
+  region ;; cons giving buffer region containing token text
+
+  nonterminal ;; t if a nonterminal
+
+  line ;; Line number at start of token. Nil for empty nonterminals
+
+  first
+  ;; For terminals, t if token is the first token on a line.
+  ;;
+  ;; For nonterminals, line number of first contained line (not
+  ;; including trailing comments) that needs indenting; it is a
+  ;; comment, or begins with a contained token.
+  ;;
+  ;; Otherwise nil.
+
+  ;; The following are non-nil if token (terminal or non-terminal) is
+  ;; followed by blank or comment lines
+  comment-line ;; first blank or comment line following token
+  comment-end ;; position at end of blank or comment lines
+  )
+
+(defun wisi-token-text (token)
+  "Return buffer text from token range."
+  (let ((region (wisi-tok-region token)))
+    (and region
+       (buffer-substring-no-properties (car region) (cdr region)))))
+
+(defun wisi-and-regions (left right)
+  "Return region enclosing both LEFT and RIGHT."
+  (if left
+      (if right
+         (cons (min (car left) (car right))
+               (max (cdr left) (cdr right)))
+       left)
+    right))
+
+(defun wisi--set-line-begin (line-count)
+  "Return a vector of line-beginning positions, with length LINE-COUNT."
+  (let ((result (make-vector line-count 0)))
+    (save-excursion
+      (goto-char (point-min))
+
+      (dotimes (i line-count)
+       (aset result i (point))
+       (forward-line 1)))
+    result))
+
+;;;; debugging
+(defun wisi-tok-debug-image (tok)
+  "Return id and region from TOK, as string."
+  (cond
+   ((wisi-tok-region tok)
+    (format "(%s %d . %d)"
+           (wisi-tok-token tok)
+           (car (wisi-tok-region tok))
+           (cdr (wisi-tok-region tok))))
+   (t
+    (format "(%s)" (wisi-tok-token tok)))
+   ))
+
+(provide 'wisi-parse-common)
diff --git a/wisi-parse.el b/wisi-parse.el
deleted file mode 100755
index 0076fe5..0000000
--- a/wisi-parse.el
+++ /dev/null
@@ -1,549 +0,0 @@
-;;; wisi-parse.el --- Wisi parser  -*- lexical-binding:t -*-
-
-;; Copyright (C) 2013-2015  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 'semantic/wisent)
-
-;; WORKAROUND: for some reason, this condition doesn't work in batch mode!
-;; (when (and (= emacs-major-version 24)
-;;        (= emacs-minor-version 2))
-  (require 'wisi-compat-24.2)
-;;)
-
-(defvar wisi-parse-max-parallel 15
-  "Maximum number of parallel parsers for acceptable performance.
-If a file needs more than this, it's probably an indication that
-the grammar is excessively redundant.")
-
-(defvar wisi-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 0
-  "wisi debug mode:
-0 : normal - ignore parse errors, for indenting new code
-1 : report parse errors (for running tests)
-2 : show parse states, position point at parse errors, debug-on-error works in 
parser
-3 : also show top 10 items of parser stack.")
-
-(cl-defstruct (wisi-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; see wisi-parse
-
-  stack
-  ;; Each stack item takes two slots: (token-symbol token-text (token-start . 
token-end)), state
-  ;; token-text is nil for nonterminals.
-  ;; this is _not_ the same as the wisent-parse stack; that leaves out 
token-symbol.
-
-  sp ;; stack pointer
-
-  pending
-  ;; list of (action-symbol stack-fragment)
-  )
-
-(defun wisi-error-msg (message &rest args)
-  (let ((line (line-number-at-pos))
-       (col (- (point) (line-beginning-position))))
-    (format
-     "%s:%d:%d: %s"
-       (file-name-nondirectory (buffer-name)) ;; buffer-file-name is sometimes 
nil here!?
-       line col
-       (apply 'format message args))))
-
-(defvar wisi-parse-error nil)
-(put 'wisi-parse-error
-     'error-conditions
-     '(error wisi-parse-error))
-(put 'wisi-parse-error
-     'error-message
-     "wisi parse error")
-
-(defvar-local wisi-cache-max 0
-  "Maximimum position in buffer where wisi-cache text properties are valid.")
-
-(defun wisi-token-text (token)
-  "Return buffer text from token range."
-  (let ((region (cdr token)))
-    (and region
-       (buffer-substring-no-properties (car region) (cdr region)))))
-
-(defun wisi-parse (automaton lexer)
-  "Parse current buffer from bob using the automaton specified in AUTOMATON.
-
-- AUTOMATON is the parse table generated by `wisi-compile-grammar'.
-
-- LEXER is a function with no argument called by the parser to
-  obtain the next token from the current buffer after point, as a
-  list (symbol text start . end), where `symbol' is the terminal
-  symbol, `text' is the token string, `start . end' is the range
-  in the buffer."
-
-  (let* ((actions (aref automaton 0))
-        (gotos   (aref automaton 1))
-        (parser-states ;; vector of parallel parser states
-         (vector
-          (make-wisi-parser-state
-           :label 0
-           :active  'shift
-           :stack   (make-vector wisent-parse-max-stack-size nil)
-           :sp      0
-           :pending nil)))
-        (active-parser-count 1)
-        active-parser-count-prev
-        (active 'shift)
-        (token nil)
-        some-pending
-        )
-
-    (goto-char (point-min))
-    (aset (wisi-parser-state-stack (aref parser-states 0)) 0 0)
-
-    (setq token (funcall lexer))
-    (setq wisi-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-parser-state-active (aref parser-states 
parser-index)))
-         (let* ((parser-state (aref parser-states parser-index))
-                (result (wisi-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)
-               (signal 'wisi-parse-error
-                       (let ((state (aref (wisi-parser-state-stack 
parser-state)
-                                          (wisi-parser-state-sp 
parser-state))))
-                         (wisi-error-msg (concat "too many parallel parsers 
required in grammar state %d;"
-                                                 " simplify grammar, or 
increase `wisi-parse-max-parallel'")
-                                                 state))))
-
-             (let ((j (wisi-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-parser-state-active result)
-                       (cl-case (wisi-parser-state-active result)
-                         (shift 'pending-shift)
-                         (reduce 'pending-reduce)
-                        )))
-                 )
-               (setq active-parser-count (1+ active-parser-count))
-               (when (> active-parser-count (car 
wisi-parse-max-parallel-current))
-                 (setq wisi-parse-max-parallel-current (cons 
active-parser-count (point))))
-               (setf (wisi-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-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; report the error.
-                  (let ((state (aref (wisi-parser-state-stack parser-state)
-                                      (wisi-parser-state-sp parser-state))))
-                    (signal 'wisi-parse-error
-                            (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))))
-                    ))
-                 (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-parser-state-stack 
parser-state)
-                                          (wisi-parser-state-sp 
parser-state))))
-                        (when (eq 'error (wisi-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)))))
-                          )))
-                    (signal 'wisi-parse-error msg)))
-                 ))
-
-               (1
-                (setf (wisi-parser-state-active parser-state) nil); Don't save 
error for later.
-                (let ((parser-state (aref parser-states (wisi-active-parser 
parser-states))))
-                  (wisi-execute-pending (wisi-parser-state-label parser-state)
-                                        (wisi-parser-state-pending 
parser-state))
-                  (setf (wisi-parser-state-pending parser-state) nil)
-                  ))
-               (t
-                ;; We were in a parallel parse, and this parser
-                ;; failed; mark it inactive, don't save error for
-                ;; later.
-                (setf (wisi-parser-state-active parser-state) nil)
-                )))
-           )));; end dotimes
-
-      (when some-pending
-       ;; Change pending-* parsers to *.
-       (dotimes (parser-index (length parser-states))
-         (cond
-          ((eq (wisi-parser-state-active (aref parser-states parser-index)) 
'pending-shift)
-           (setf (wisi-parser-state-active (aref parser-states parser-index)) 
'shift))
-          ((eq (wisi-parser-state-active (aref parser-states parser-index)) 
'pending-reduce)
-           (setf (wisi-parser-state-active (aref parser-states parser-index)) 
'reduce))
-          )))
-
-      (setq active (wisi-parsers-active parser-states active-parser-count))
-      (when (eq active 'shift)
-       (when (> active-parser-count 1)
-         (setq active-parser-count (wisi-parse-elim-identical parser-states 
active-parser-count)))
-
-       (setq token (funcall lexer)))
-    )
-    (when (> active-parser-count 1)
-      (error "ambiguous parse result"))))
-
-(defun wisi-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-parser-state-active (aref parser-states i))
-       (setq result i))
-      (setq i (1+ i)))
-    result))
-
-(defun wisi-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-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-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-parser-state-active (aref parser-states i)))
-       (setq result i))
-      (setq i (1+ i)))
-    (if result result -1)))
-
-(defun wisi-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-parser-state-active (aref parser-states i))
-       (setq result i))
-      (setq i (1+ i)))
-    (unless result
-      (error "no active parsers"))
-    result))
-
-(defun wisi-parse-elim-identical (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-parser-state-active (aref parser-states parser-i))
-      (dotimes (parser-j (- (length parser-states) parser-i 1))
-       (when (wisi-parser-state-active (aref parser-states (+ parser-i 
parser-j 1)))
-         (when (eq (wisi-parser-state-sp (aref parser-states parser-i))
-                    (wisi-parser-state-sp (aref parser-states (+ parser-i 
parser-j 1))))
-           (let ((compare t))
-             (dotimes (stack-i (wisi-parser-state-sp (aref parser-states 
parser-i)))
-               (setq
-                compare
-                (and compare ;; bypass expensive 'arefs' after first stack 
item compare fail
-                     (equal (aref (wisi-parser-state-stack (aref parser-states 
parser-i)) stack-i)
-                            (aref (wisi-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))
-               (setf (wisi-parser-state-active (aref parser-states (+ parser-i 
parser-j 1))) nil)
-               (when (= active-parser-count 1)
-                 ;; the actions for the two parsers are not
-                 ;; identical, but either is good enough for
-                 ;; indentation and navigation, so we just do the
-                 ;; actions for the one that is not terminating.
-                 (let ((parser-state (aref parser-states parser-i)))
-                   (wisi-execute-pending (wisi-parser-state-label parser-state)
-                                         (wisi-parser-state-pending 
parser-state))
-                   (setf (wisi-parser-state-pending parser-state) nil)
-                   ))
-               ))))
-       )))
-  active-parser-count)
-
-(defun wisi-parse-max-pos (tokens)
-  "Return max position in tokens, or point if tokens nil."
-  (let ((result (if tokens 0 (point))))
-    (mapc
-     (lambda (token)
-       (when (cddr token)
-        (setq result (max (cddr token) result))))
-     tokens)
-    result)
-  )
-
-(defun wisi-parse-exec-action (func nonterm tokens)
-  "Execute action if all tokens past wisi-cache-max."
-  ;; We don't execute actions if all tokens are before wisi-cache-max,
-  ;; because later actions can update existing caches, and if the
-  ;; parse fails that won't happen. It also saves time.
-  ;;
-  ;; Also skip if no tokens; nothing to do. This can happen when all
-  ;; tokens in a grammar statement are optional.
-  (if (< 0 (length tokens))
-      (if (>= (wisi-parse-max-pos tokens) wisi-cache-max)
-
-         (funcall func nonterm tokens)
-
-       (when (> wisi-debug 1)
-         (message "... action skipped; before wisi-cache-max %d" 
wisi-cache-max)))
-
-    (when (> wisi-debug 1)
-      (message "... action skipped; no tokens"))
-    ))
-
-(defun wisi-execute-pending (parser-label pending)
-  (when (> wisi-debug 1) (message "%d: pending actions:" parser-label))
-  (while pending
-    (when (> wisi-debug 1) (message "%s" (car pending)))
-
-    (let ((func-args (pop pending)))
-      (wisi-parse-exec-action (nth 0 func-args) (nth 1 func-args) (cl-caddr 
func-args)))
-    ))
-
-(defun wisi-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-parse' for full details.
-Return nil or new parser (a wisi-parse-state struct)."
-  (let* ((state (aref (wisi-parser-state-stack parser-state)
-               (wisi-parser-state-sp parser-state)))
-        (parse-action (wisent-parse-action (car 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-parser-state-sp parser-state)))
-                  (msg (make-vector (+ 1 count) nil)))
-             (dotimes (i count)
-               (aset msg (- count i)
-                     (aref (wisi-parser-state-stack parser-state) (- 
(wisi-parser-state-sp parser-state) i)))
-               )
-             (message "%d: %s: %d: %s"
-                      (wisi-parser-state-label parser-state)
-                      (wisi-parser-state-active parser-state)
-                      (wisi-parser-state-sp parser-state)
-                      msg))
-           (message "   %d: %s: %s" state token parse-action))
-       (message "%d: %d: %s: %s" (wisi-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-parser-state
-            :active  nil
-            :stack   (vconcat (wisi-parser-state-stack parser-state))
-            :sp      (wisi-parser-state-sp parser-state)
-            :pending (wisi-parser-state-pending parser-state)))
-
-      (wisi-parse-2 (cadr parse-action) token new-parser-state t gotos)
-      (setq pendingp t)
-      (setq parse-action (car parse-action))
-      );; when
-
-    ;; current parser
-    (wisi-parse-2 parse-action token parser-state pendingp gotos)
-
-    new-parser-state))
-
-(defun wisi-parse-2 (action token parser-state pendingp gotos)
-  "Execute parser ACTION (must not be a conflict).
-Return nil."
-  (cond
-   ((eq action 'accept)
-    (setf (wisi-parser-state-active parser-state) 'accept))
-
-   ((eq action 'error)
-    (setf (wisi-parser-state-active parser-state) 'error))
-
-   ((natnump action)
-    ;; Shift token and new state (= action) onto stack
-    (let ((stack (wisi-parser-state-stack parser-state)); reference
-         (sp (wisi-parser-state-sp parser-state))); copy
-      (setq sp (+ sp 2))
-      (aset stack (1- sp) token)
-      (aset stack sp action)
-      (setf (wisi-parser-state-sp parser-state) sp))
-    (setf (wisi-parser-state-active parser-state) 'shift))
-
-   (t
-    (wisi-parse-reduce action parser-state pendingp gotos)
-    (setf (wisi-parser-state-active parser-state) 'reduce))
-   ))
-
-(defun wisi-nonterm-bounds (stack i j)
-  "Return a pair (START . END), the buffer region for a nonterminal.
-STACK is the parser stack.  I and J are the indices in STACK of
-the first and last tokens of the nonterminal."
-  (let ((start (cadr (aref stack i)))
-        (end   (cddr (aref stack j))))
-    (while (and (or (not start) (not end))
-               (/= i j))
-      (cond
-       ((not start)
-       ;; item i is an empty production
-       (setq start (cadr (aref stack (setq i (+ i 2))))))
-
-       ((not end)
-       ;; item j is an empty production
-       (setq end (cddr (aref stack (setq j (- j 2))))))
-
-       (t (setq i j))))
-    (and start end (cons start end))))
-
-(defun wisi-parse-reduce (action parser-state pendingp gotos)
-  "Reduce PARSER-STATE.stack, and execute or pend ACTION."
-  (let* ((stack (wisi-parser-state-stack parser-state)); reference
-        (sp (wisi-parser-state-sp parser-state)); copy
-        (token-count (nth 2 action))
-        (nonterm (nth 0 action))
-        (nonterm-region (when (> token-count 0)
-                          (wisi-nonterm-bounds stack (- sp (* 2 (1- 
token-count)) 1) (1- sp))))
-        (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)))
-
-    (when (not new-state)
-      (error "no goto for %s %d" nonterm post-reduce-state))
-
-    (when (nth 1 action)
-      ;; don't need wisi-tokens for a null user action
-      (dotimes (i token-count)
-       (aset tokens (- token-count i 1) (aref stack (- sp (* 2 i) 1)))))
-
-    (setq sp (+ 2 (- sp (* 2 token-count))))
-    (aset stack (1- sp) (cons nonterm nonterm-region))
-    (aset stack sp new-state)
-    (setf (wisi-parser-state-sp parser-state) sp)
-
-    (when (nth 1 action)
-      ;; nothing to do for a null user action
-      (if pendingp
-         (if (wisi-parser-state-pending parser-state)
-             (setf (wisi-parser-state-pending parser-state)
-                   (append (wisi-parser-state-pending parser-state)
-                           (list (list (nth 1 action) nonterm tokens))))
-           (setf (wisi-parser-state-pending parser-state)
-                 (list (list (nth 1 action) nonterm tokens))))
-
-       ;; Not pending.
-       (wisi-parse-exec-action (nth 1 action) nonterm tokens)
-       ))
-    ))
-
-(provide 'wisi-parse)
-;;; wisi-parse.el ends here
diff --git a/wisi-process-parse.el b/wisi-process-parse.el
new file mode 100644
index 0000000..cfc98eb
--- /dev/null
+++ b/wisi-process-parse.el
@@ -0,0 +1,691 @@
+;;; wisi-process-parse.el --- interface to external parse program
+;;
+;; Copyright (C) 2014, 2017, 2018 Free Software Foundation, Inc.
+;;
+;; Author: Stephen Leake <stephen_leake@member.fsf.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-parse-common)
+
+(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,
+;; then runs a loop, waiting for parse requests.
+;;
+;; We only need one process per language; there is no persistent state
+;; in the process between parses, and processes are too heavy-weight
+;; to have one per buffer. We use a global alist of parser objects to
+;; find the right one for the current buffer.
+
+(cl-defstruct (wisi-process--parser (:include wisi-parser))
+  (label nil)             ;; string uniquely identifying parser
+  (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
+  (face-table nil)       ;; vector of face symbols, indexed by integer
+  (busy nil)              ;; t while parser is active
+  (process nil)          ;; running *_wisi_parse executable
+  (buffer nil)                   ;; receives output of executable
+  line-begin              ;; vector of beginning-of-line positions in buffer
+  (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.
+  )
+
+(defvar wisi-process--alist nil
+  "Alist mapping string label to ‘wisi-process--session’ struct")
+
+(defgroup wisi nil
+  "Options for Wisi package."
+  :group 'programming)
+
+(defcustom wisi-process-time-out 1.0
+  "Time out waiting for parser response. An error occurs if there
+  is no response from the parser after waiting this amount 5
+  times."
+  :type 'float
+  :safe 'floatp)
+(make-variable-buffer-local 'wisi-process-time-out)
+
+;;;###autoload
+(defun wisi-process-parse-get (parser)
+  "Return a ‘wisi-process--parser’ object matching PARSER label.
+If label found in ‘wisi-process--alist’, return that.
+Otherwise add PARSER to ‘wisi-process--alist’, return it."
+  (or (cdr (assoc (wisi-process--parser-label parser) wisi-process--alist))
+      (let ((exec-file (locate-file (wisi-process--parser-exec-file parser) 
exec-path '("" ".exe"))))
+
+       (unless exec-file
+         (error "%s not found on `exec-path'" (wisi-process--parser-exec-file 
parser)))
+
+       (push (cons (wisi-process--parser-label parser) parser) 
wisi-process--alist)
+
+       parser
+     )))
+
+(defun wisi-process-parse-set-exec (label exec-file)
+  "Change the EXEC-FILE for parsers with LABEL."
+  (let ((parser (cdr (assoc label wisi-process--alist))))
+    (when parser
+      (wisi-parse-kill parser)
+      (setf (wisi-process--parser-exec-file parser) exec-file))))
+
+(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))
+    (let ((process-connection-type nil) ;; use a pipe, not a pty; avoid 
line-by-line reads
+         (process-name (format " *%s_wisi_parse*" (wisi-process--parser-label 
parser))))
+
+      (unless (buffer-live-p (wisi-process--parser-buffer parser))
+       ;; User may have killed buffer to kill parser.
+       (setf (wisi-process--parser-buffer parser)
+             (get-buffer-create process-name)))
+
+      (with-current-buffer (wisi-process--parser-buffer parser)
+       (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)
+              )))
+
+      (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)
+      )))
+
+(defun wisi-process-parse--wait (parser)
+  "Wait for the current command to complete."
+  (let ((process (wisi-process--parser-process parser))
+       (search-start (point-min))
+       (wait-count 0)
+       (found nil))
+
+    (with-current-buffer (wisi-process--parser-buffer parser)
+      (while (and (process-live-p process)
+                 (progn
+                   ;; process output is inserted before point, so move back 
over it to search it
+                   (goto-char search-start)
+                   (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)))))
+
+       (wisi-process-parse-show-buffer parser)
+       (error "%s process died" (wisi-process--parser-exec-file parser)))
+      )))
+
+(defun wisi-process-parse-show-buffer (parser)
+  "Show PARSER buffer."
+  (if (buffer-live-p (wisi-process--parser-buffer parser))
+      (pop-to-buffer (wisi-process--parser-buffer parser))
+    (error "wisi-process-parse process not active")))
+
+(defun wisi-process-parse--send-parse (parser line-count)
+  "Send a parse command to PARSER external process, followed by
+the content of the current buffer.  Does not wait for command to
+complete."
+  ;; Must match "parse" command arguments in gen_emacs_wisi_parse.adb
+  (let* ((cmd (format "parse %d \"%s\" %d %d %d %d %d %d %d %d %d %d %d %s"
+                     (cl-ecase wisi--parse-action
+                       (navigate 0)
+                       (face 1)
+                       (indent 2))
+                     (if (buffer-file-name) (file-name-nondirectory 
(buffer-file-name)) "")
+                     line-count
+                     (if (> wisi-debug 0) 1 0) ;; debug-mode
+                     (1- wisi-debug) ;; trace_parse
+                     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)
+                     (1- (position-bytes (point-max)))
+                     (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))
+
+    (process-send-string process msg)
+    (process-send-string process (buffer-substring-no-properties (point-min) 
(point-max)))
+
+    ;; We don’t wait for the send to complete; the external process
+    ;; may start parsing and send an error message.
+    ))
+
+(defun wisi-process-parse--send-noop (parser)
+  "Send a noop command to PARSER external process, followed by
+the content of the current buffer.  Does not wait for command to
+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))
+
+    (process-send-string process msg)
+    (process-send-string process (buffer-substring-no-properties (point-min) 
(point-max)))
+    ))
+
+(defun wisi-process-parse--marker-or-nil (item)
+  (if (= -1 item) nil (copy-marker item t)))
+
+(defun wisi-process-parse--Navigate_Cache (parser sexp)
+  ;; sexp is [Navigate_Cache pos statement_id id length class containing_pos 
prev_pos next_pos end_pos]
+  ;; see ‘wisi-process-parse--execute’
+  (let ((pos (aref sexp 1)))
+    (with-silent-modifications
+      (put-text-property
+       pos
+       (1+ pos)
+       'wisi-cache
+       (wisi-cache-create
+       :nonterm    (aref (wisi-process--parser-token-table parser) (aref sexp 
2))
+       :token      (aref (wisi-process--parser-token-table parser) (aref sexp 
3))
+       :last       (aref sexp 4)
+       :class      (aref wisi-class-list (aref sexp 5))
+       :containing (wisi-process-parse--marker-or-nil (aref sexp 6))
+       :prev       (wisi-process-parse--marker-or-nil (aref sexp 7))
+       :next       (wisi-process-parse--marker-or-nil (aref sexp 8))
+       :end        (wisi-process-parse--marker-or-nil (aref sexp 9))
+       )))
+    ))
+
+(defun wisi-process-parse--Face_Property (parser sexp)
+  ;; sexp is [Face_Property first-pos last-pos face-index]
+  ;; see ‘wisi-process-parse--execute’
+  ;; implements wisi--face-action-1
+  (with-silent-modifications
+    (add-text-properties
+     (aref sexp 1)
+     (1+ (aref sexp 2))
+     (list 'font-lock-face (aref (wisi-process--parser-face-table parser) 
(aref sexp 3))
+          'fontified t)
+     )))
+
+(defun wisi-process-parse--Indent (parser sexp)
+  ;; sexp is [Indent line-number indent]
+  ;; see ‘wisi-process-parse--execute’
+  (let ((pos (aref (wisi-process--parser-line-begin parser) (1- (aref sexp 
1)))))
+    (with-silent-modifications
+      (put-text-property
+       (1- pos)
+       pos
+       'wisi-indent
+       (aref sexp 2)))
+    ))
+
+(defun wisi-process-parse--Lexer_Error (parser sexp)
+  ;; sexp is [Lexer_Error char-position <message> <repair-char>]
+  ;; see ‘wisi-process-parse--execute’
+  (let ((pos (aref sexp 1))
+       err)
+
+    (goto-char pos);; for current-column
+
+    (setq err
+         (make-wisi--lexer-error
+          :pos (copy-marker pos)
+          :message
+          (format "%s:%d:%d: %s"
+                  (if (buffer-file-name) (file-name-nondirectory 
(buffer-file-name)) "")
+                  ;; file-name can be nil during vc-resolve-conflict
+                  (line-number-at-pos pos)
+                  (current-column)
+                  (aref sexp 2))
+          :inserted (when (= 4 (length sexp)) (aref sexp 3))))
+
+    (push err (wisi-parser-lexer-errors parser))
+    ))
+
+(defun wisi-process-parse--Parser_Error (parser sexp)
+  ;; sexp is [Parser_Error char-position <string>]
+  ;; see ‘wisi-process-parse--execute’
+  (let ((pos (aref sexp 1))
+       err)
+
+    (goto-char pos);; for current-column
+
+    (setq err
+         (make-wisi--parse-error
+          :pos (copy-marker pos)
+          :message
+          (format "%s:%d:%d: %s"
+                  (if (buffer-file-name) (file-name-nondirectory 
(buffer-file-name)) "")
+                  ;; file-name can be nil during vc-resolve-conflict
+                  (line-number-at-pos pos)
+                  (1+ (current-column))
+                  (aref sexp 2))))
+
+    (push err (wisi-parser-parse-errors parser))
+    ))
+
+(defun wisi-process-parse--Check_Error (parser sexp)
+  ;; sexp is [Check_Error code name-1-pos name-2-pos <string>]
+  ;; see ‘wisi-process-parse--execute’
+  (let* ((name-1-pos (aref sexp 2))
+       (name-1-col (1+ (progn (goto-char name-1-pos)(current-column)))) ;; 
gnat columns are 1 + emacs columns
+       (name-2-pos (aref sexp 3))
+       (name-2-col (1+ (progn (goto-char name-2-pos)(current-column))))
+       (file-name (if (buffer-file-name) (file-name-nondirectory 
(buffer-file-name)) ""))
+       ;; file-name can be nil during vc-resolve-conflict
+       (err (make-wisi--parse-error
+             :pos (copy-marker name-1-pos)
+             :message
+             (format "%s:%d:%d: %s %s:%d:%d"
+                     file-name (line-number-at-pos name-1-pos) name-1-col
+                     (aref sexp 4)
+                     file-name (line-number-at-pos name-2-pos) name-2-col)))
+       )
+
+    (push err (wisi-parser-parse-errors parser))
+    ))
+
+(defun wisi-process-parse--Recover (parser sexp)
+  ;; sexp is [Recover [pos [inserted] [deleted]]...]
+  ;; see ‘wisi-process-parse--execute’
+  ;; convert to list of wisi--parse-error-repair, add to last error
+  (let* ((token-table (wisi-process--parser-token-table parser))
+        (last-error (car (wisi-parser-parse-errors parser))))
+    (unless (= 1 (length sexp))
+      (cl-do ((i 1 (1+ i))) ((= i (length sexp)))
+       (push
+        (make-wisi--parse-error-repair
+         :pos (aref (aref sexp i) 0)
+         :inserted (mapcar (lambda (id) (aref token-table id)) (aref (aref 
sexp i) 1))
+         :deleted  (mapcar (lambda (id) (aref token-table id)) (aref (aref 
sexp i) 2)))
+        (wisi--parse-error-repair last-error)))
+      )))
+
+(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
+  ;;
+  ;; Actions:
+  ;;
+  ;; [Navigate_Cache pos statement_id id length class containing_pos prev_pos 
next_pos end_pos]
+  ;;    Set a wisi-cache text-property.
+  ;;    *pos          : integer buffer position; -1 if nil (not set)
+  ;;    *id           : integer index into parser-token-table
+  ;;    length        : integer character count
+  ;;    class         : integer index into wisi-class-list
+  ;;
+  ;; [Face_Property first-pos last-pos face-index]
+  ;;    Set a font-lock-face text-property
+  ;;    face-index: integer index into parser-elisp-face-table
+  ;;
+  ;; [Indent line-number indent]
+  ;;    Set an indent text property
+  ;;
+  ;; [Lexer_Error char-position <message> <repair-char>]
+  ;;    The lexer detected an error at char-position.
+  ;;
+  ;;    If <repair-char> is not ASCII NUL, it was inserted immediately
+  ;;    after char-position to fix the error.
+  ;;
+  ;; [Parser_Error char-position <message>]
+  ;;    The parser detected a syntax error; save information for later
+  ;;    reporting.
+  ;;
+  ;;    If error recovery is successful, there can be more than one
+  ;;    error reported during a parse.
+  ;;
+  ;; [Check_Error code name-1-pos name-2-pos <string>]
+  ;;    The parser detected a semantic check error; save information
+  ;;    for later reporting.
+  ;;
+  ;;    If error recovery is successful, there can be more than one
+  ;;    error reported during a parse.
+  ;;
+  ;; [Recover [pos [inserted] [deleted]]...]
+  ;;    The parser finished a successful error recovery.
+  ;;
+  ;;    pos: Buffer position
+  ;;
+  ;;    inserted: Virtual tokens (terminal or non-terminal) inserted before 
pos.
+  ;;
+  ;;    deleted: Tokens deleted after pos.
+  ;;
+  ;;    Args are token ids; index into parser-token-table. Save the information
+  ;;    for later use by ’wisi-repair-error’.
+  ;;
+  ;;
+  ;; Numeric action codes are given in the case expression below
+
+  (cl-ecase (aref sexp 0)
+    (1  (wisi-process-parse--Navigate_Cache parser sexp))
+    (2  (wisi-process-parse--Face_Property parser sexp))
+    (3  (wisi-process-parse--Indent parser sexp))
+    (4  (wisi-process-parse--Lexer_Error parser sexp))
+    (5  (wisi-process-parse--Parser_Error parser sexp))
+    (6  (wisi-process-parse--Check_Error parser sexp))
+    (7  (wisi-process-parse--Recover parser sexp))
+    ))
+
+;;;;; main
+
+(cl-defmethod wisi-parse-kill ((parser wisi-process--parser))
+  (when (process-live-p (wisi-process--parser-process parser))
+    (process-send-string (wisi-process--parser-process parser) 
wisi-process-parse-quit-cmd)
+    (sit-for 1.0)
+    (when (process-live-p (wisi-process--parser-process parser))
+      (kill-process (wisi-process--parser-process parser)))
+    )
+  (setf (wisi-process--parser-busy parser) nil))
+
+(defvar wisi--lexer nil) ;; wisi-elisp-lexer.el
+(declare-function wisi-elisp-lexer-reset "wisi-elisp-lexer")
+
+(cl-defmethod wisi-parse-current ((parser wisi-process--parser))
+  "Run the external parser on the current buffer."
+  (wisi-process-parse--require-process parser)
+
+  ;; font-lock can trigger a face parse while navigate or indent parse
+  ;; is active, due to ‘accept-process-output’ below. font-lock must
+  ;; not hang (it is called from an idle timer), so don’t
+  ;; wait. Signaling an error tells font-lock to try again later.
+  ;;
+  ;; If the parser is left busy due to some error, that is a bug. In
+  ;; order to detect such bugs, and avoid weird errors from
+  ;; wisi-indent-region, we signal an error here.
+  (if (wisi-process--parser-busy parser)
+      (progn
+       (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)
+       )
+
+    ;; It is not possible for a background elisp function (ie
+    ;; font-lock) to interrupt this code between checking and setting
+    ;; parser-busy; background elisp can only run when we call
+    ;; accept-process-output below.
+    (setf (wisi-process--parser-busy parser) t)
+
+    (condition-case-unless-debug err
+       (let* ((source-buffer (current-buffer))
+              (response-buffer (wisi-process--parser-buffer parser))
+              (process (wisi-process--parser-process parser))
+              (w32-pipe-read-delay 0) ;; fastest subprocess read
+              response
+              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)
+
+         (setf (wisi-process--parser-total-wait-time parser) 0.0)
+
+         (setf (wisi-parser-lexer-errors parser) nil)
+         (setf (wisi-parser-parse-errors parser) nil)
+
+         (let ((line-count (1+ (count-lines (point-min) (point-max)))))
+           (setf (wisi-process--parser-line-begin parser) 
(wisi--set-line-begin line-count))
+           (wisi-process-parse--send-parse parser line-count)
+
+           ;; We reset the elisp lexer, because post-parse actions may use it.
+           (when wisi--lexer
+             (wisi-elisp-lexer-reset line-count wisi--lexer))
+           )
+
+         (set-buffer response-buffer)
+
+         ;; process responses until prompt received
+         (while (not done)
+
+           ;; process all complete responses currently in buffer
+           (while (and (not need-more)
+                       (not done))
+
+             (goto-char sexp-start)
+
+             (cond
+              ((eobp)
+               (setq need-more (point-max)))
+
+              ((looking-at wisi-process-parse-prompt)
+               (setq done t))
+
+              ((or (looking-at "\\[") ;; encoded action
+                   (looking-at "(")) ;; error or other elisp expression to eval
+               (condition-case nil
+                   (setq response-end (scan-sexps (point) 1))
+                 (error
+                  ;; incomplete response
+                  (setq need-more (point-max))
+                  nil))
+
+               (unless need-more
+                 (setq response-count (1+ response-count))
+                 (setq response (car (read-from-string 
(buffer-substring-no-properties (point) response-end))))
+                 (goto-char response-end)
+                 (forward-line 1)
+                 (setq sexp-start (point))
+
+                 (set-buffer source-buffer) ;; for put-text-property in actions
+                 (if (listp response)
+                     ;; error of some sort
+                     (cond
+                      ((equal '(parse_error) response)
+                       ;; Parser detected a syntax error, and recovery failed, 
so signal it.
+                       (if (wisi-parser-parse-errors parser)
+                           (signal 'wisi-parse-error
+                                   (wisi--parse-error-message (car 
(wisi-parser-parse-errors parser))))
+
+                         ;; can have no errors when testing a new parser
+                         (push
+                          (make-wisi--parse-error :pos 0 :message "parser 
failed with no message")
+                          (wisi-parser-parse-errors parser))
+                         (signal 'wisi-parse-error "parser failed with no 
message")))
+
+                      ((equal 'parse_error (car response))
+                       ;; Parser detected some other error non-fatal error, so 
signal it.
+                       (signal 'wisi-parse-error (cdr response)))
+
+                      ((and (eq 'error (car response))
+                            (string-prefix-p "bad command:" (cadr response)))
+                       ;; Parser dropped bytes, is treating buffer
+                       ;; content bytes as commands. Kill the process
+                       ;; to kill the pipes; there is no other way to
+                       ;; flush them.
+                       (kill-process (wisi-process--parser-process parser))
+                       (signal 'wisi-parse-error "parser lost sync; killed"))
+
+                      (t
+                       ;; Some other error
+                       (condition-case-unless-debug err
+                           (eval response)
+                         (error
+                          (push (make-wisi--parse-error :pos (point) :message 
(cadr err)) (wisi-parser-parse-errors parser))
+                          (signal (car err) (cdr err)))))
+                      )
+
+                   ;; else encoded action
+                   (condition-case-unless-debug err
+                       (wisi-process-parse--execute parser response)
+                     (wisi-parse-error
+                      (push (make-wisi--parse-error :pos (point) :message 
(cadr err)) (wisi-parser-parse-errors parser))
+                      (signal (car err) (cdr err)))))
+
+                 (set-buffer response-buffer)
+                 ))
+
+              (t
+               ;; debug output
+               (forward-line 1)
+               (setq sexp-start (point)))
+              )
+             )
+
+           (unless done
+             ;; end of response buffer
+             (unless (process-live-p process)
+               (wisi-process-parse-show-buffer parser)
+               (error "wisi-process-parse process died"))
+
+             (setq wait-count (1+ wait-count))
+             (setq start-wait-time (float-time))
+
+             ;; If we specify no time-out here, we get messages about
+             ;; "blocking call with quit inhibited", when this is
+             ;; called by font-lock from the display engine.
+             ;;
+             ;; Specifying just-this-one t prevents C-q from
+             ;; interrupting this?
+             (accept-process-output
+              process
+              wisi-process-time-out
+              nil ;; milliseconds
+              nil)  ;; just-this-one
+
+             (setf (wisi-process--parser-total-wait-time parser)
+                   (+ (wisi-process--parser-total-wait-time parser)
+                      (- (float-time) start-wait-time)))
+
+             (when (and (= (point-max) need-more)
+                      (> wait-count 5))
+               (error "wisi-process-parse not getting more text (or bad syntax 
in process output)"))
+
+             (setq need-more nil))
+           );; while not done
+
+         ;; got command prompt
+         (unless (process-live-p process)
+           (wisi-process-parse-show-buffer parser)
+           (error "wisi-process-parse process died"))
+
+         (setf (wisi-process--parser-response-count parser) response-count)
+
+         (setf (wisi-process--parser-busy parser) nil)
+         (set-buffer source-buffer)
+         ;; If we get here, the parse succeeded (possibly with error
+         ;; recovery); move point to end of buffer as the elisp
+         ;; parser does.
+         (goto-char (point-max))
+         )
+
+      (wisi-parse-error
+       (setf (wisi-process--parser-busy parser) nil)
+       (signal (car err) (cdr err)))
+
+      (error
+       (setf (wisi-process--parser-busy parser) nil)
+       (signal (car err) (cdr err))
+       ))))
+
+(defvar wisi--parser nil) ;; wisi.el
+
+(defun wisi-process-send-tokens-noop ()
+  "Run lexer, send tokens to subprocess; otherwise no operation.
+For use with ’wisi-time’."
+  (wisi-process-parse--require-process wisi--parser)
+  (if (wisi-process--parser-busy wisi--parser)
+      (error "%s parser busy" wisi--parse-action)
+
+    ;; not busy
+    (let* ((source-buffer (current-buffer))
+          (action-buffer (wisi-process--parser-buffer wisi--parser))
+          (process (wisi-process--parser-process wisi--parser))
+          (sexp-start (point-min))
+          (need-more nil)
+          (done nil))
+
+      (setf (wisi-process--parser-busy wisi--parser) t)
+      (wisi-process-parse--send-noop wisi--parser)
+
+      (set-buffer action-buffer)
+      (while (and (process-live-p process)
+                 (not done))
+       (goto-char sexp-start)
+       (cond
+        ((eobp)
+         (setq need-more t))
+
+        ((looking-at wisi-process-parse-prompt)
+         (setq done t))
+
+        (t
+         (forward-line 1)
+         (setq sexp-start (point)))
+        )
+
+       (unless done
+         ;; end of response buffer
+         (unless (process-live-p process)
+           (wisi-process-parse-show-buffer wisi--parser)
+           (error "wisi-process-parse process died"))
+
+         (accept-process-output process 1.0 nil nil)
+         (setq need-more nil))
+       )
+      (set-buffer source-buffer)
+      (setf (wisi-process--parser-busy wisi--parser) nil)
+      )))
+
+;;;;; debugging
+(defun wisi-process-parse-ids-to-enum (token-table &rest int-ids)
+  "Translate INT-IDS from process integer token ids to elisp enumeral ids.
+Returns reversed sequence."
+  (let ((enum-ids nil))
+    (cl-dolist (i int-ids)
+      (push (aref token-table i) enum-ids))
+    enum-ids))
+
+(provide 'wisi-process-parse)
diff --git a/wisi.adb b/wisi.adb
new file mode 100644
index 0000000..9b184b7
--- /dev/null
+++ b/wisi.adb
@@ -0,0 +1,1891 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Strings.Bounded;
+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 ";
+
+   Chars_Per_Int : constant Integer := Integer'Width;
+
+   ----------
+   --  body subprogram specs (as needed), alphabetical
+
+   function Indent_Zero_P (Indent : in Indent_Type) return Boolean;
+
+   function Max_Anchor_ID
+     (Data       : in out Parse_Data_Type;
+      First_Line : in     Line_Number_Type;
+      Last_Line  : in     Line_Number_Type)
+     return Integer;
+
+   function Paren_In_Anchor_Line
+     (Data         : in out Parse_Data_Type'Class;
+      Anchor_Token : in     Augmented_Token;
+      Offset       : in     Integer)
+     return Integer;
+
+   ----------
+   --  body subprograms bodies, alphabetical
+
+   function Image (Anchor_IDs : in Anchor_ID_Vectors.Vector) return String
+   is
+      use Ada.Strings.Unbounded;
+      Result : Unbounded_String := +"(";
+   begin
+      for I in Anchor_IDs.First_Index .. Anchor_IDs.Last_Index loop
+         Result := Result & Integer'Image (Anchor_IDs (I));
+         if I /= Anchor_IDs.Last_Index then
+            Result := Result & ", ";
+         else
+            Result := Result & ")";
+         end if;
+      end loop;
+      return -Result;
+   end Image;
+
+   function Image (Indent : in Indent_Type) return String
+   is begin
+      case Indent.Label is
+      when Not_Set =>
+         return "(" & Indent_Label'Image (Indent.Label) & ")";
+
+      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 Anchored =>
+         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
+           (Indent.Anchor_Anchored_ID) & ", " & Integer'Image 
(Indent.Anchor_Anchored_Delta) & ")";
+      end case;
+   end Image;
+
+   procedure Indent_Apply_Anchored
+     (Delta_Indent : in     Anchored_Delta;
+      Indent       : in out Indent_Type)
+   is begin
+      --  [2] wisi-elisp-parse--apply-anchored
+
+      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
+            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
+            Indent :=
+              (Anchor_Anchored,
+               Indent.Anchor_IDs,
+               Delta_Indent.Anchored_ID,
+               Delta_Indent.Anchored_Delta + Indent.Anchor_Indent);
+         end if;
+
+      when Anchored | Anchor_Anchored =>
+         --  already anchored
+         null;
+      end case;
+   end Indent_Apply_Anchored;
+
+   procedure Indent_Apply_Int (Indent : in out Indent_Type; Offset : in 
Integer)
+   is begin
+      --  [2] wisi-elisp-parse--apply-int
+      case Indent.Label is
+      when Not_Set =>
+         Indent := (Int, Offset);
+
+      when Int =>
+         Indent.Int_Indent := Indent.Int_Indent + Offset;
+
+      when Anchor =>
+         Indent.Anchor_Indent := Indent.Anchor_Indent + Offset;
+
+      when Anchored | Anchor_Anchored =>
+         null;
+      end case;
+   end Indent_Apply_Int;
+
+   procedure Indent_Line
+     (Data         : in out Parse_Data_Type;
+      Line         : in     Line_Number_Type;
+      Delta_Indent : in     Delta_Type)
+   is
+      --  See note in Indent_Anchored_2 for why we can't use renames here.
+      Indent : Indent_Type := Data.Indents (Line);
+   begin
+      case Delta_Indent.Label is
+      when Simple =>
+         case Delta_Indent.Simple_Delta.Label is
+         when Int =>
+            Indent_Apply_Int (Indent, Delta_Indent.Simple_Delta.Int_Delta);
+
+         when Anchored =>
+            Indent_Apply_Anchored (Delta_Indent.Simple_Delta, Indent);
+         end case;
+
+      when Hanging =>
+         if Delta_Indent.Hanging_Accumulate or Indent_Zero_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 Int =>
+                  Indent_Apply_Int (Indent, 
Delta_Indent.Hanging_Delta_1.Int_Delta);
+               when Anchored =>
+                  Indent_Apply_Anchored (Delta_Indent.Hanging_Delta_1, Indent);
+               end case;
+            else
+               if Delta_Indent.Hanging_Paren_State = Data.Line_Paren_State 
(Line) then
+                  case Delta_Indent.Hanging_Delta_2.Label is
+                  when Int =>
+                     Indent_Apply_Int (Indent, 
Delta_Indent.Hanging_Delta_2.Int_Delta);
+                  when Anchored =>
+                     Indent_Apply_Anchored (Delta_Indent.Hanging_Delta_2, 
Indent);
+                  end case;
+               end if;
+            end if;
+         end if;
+      end case;
+      Data.Indents.Replace_Element (Line, Indent);
+   end Indent_Line;
+
+   function Indent_Zero_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;
+
+   function Max_Anchor_ID
+     (Data       : in out Parse_Data_Type;
+      First_Line : in     Line_Number_Type;
+      Last_Line  : in     Line_Number_Type)
+     return Integer
+   is
+      Result : Integer := First_Anchor_ID - 1;
+   begin
+      for Line in First_Line .. Last_Line loop
+         declare
+            Indent : Indent_Type renames Data.Indents (Line);
+         begin
+            case Indent.Label is
+            when Not_Set | Int =>
+               null;
+            when Anchor =>
+               Result := Integer'Max (Result, Indent.Anchor_IDs 
(Indent.Anchor_IDs.First_Index));
+            when Anchored =>
+               Result := Integer'Max (Result, Indent.Anchored_ID);
+            when Anchor_Anchored =>
+               Result := Integer'Max (Result, Indent.Anchor_Anchored_ID);
+            end case;
+         end;
+      end loop;
+      return Result;
+   end Max_Anchor_ID;
+
+   function Paren_In_Anchor_Line
+     (Data         : in out Parse_Data_Type'Class;
+      Anchor_Token : in     Augmented_Token;
+      Offset       : in     Integer)
+     return Integer
+   is
+      Left_Paren_ID  : WisiToken.Token_ID renames 
Data.Descriptor.Left_Paren_ID;
+      Right_Paren_ID : WisiToken.Token_ID renames 
Data.Descriptor.Right_Paren_ID;
+
+      I              : Base_Token_Index := Anchor_Token.First_Terminals_Index;
+      Paren_Count    : Integer          := 0;
+      Paren_Char_Pos : Buffer_Pos       := Invalid_Buffer_Pos;
+      Text_Begin_Pos : Buffer_Pos       := Invalid_Buffer_Pos;
+   begin
+      --  [1] wisi-elisp-parse--paren-in-anchor-line. That uses elisp 
syntax-ppss; here
+      --  we search Terminals.
+      loop
+         declare
+            Tok : Augmented_Token renames Data.Terminals (I);
+         begin
+            if Tok.ID = Left_Paren_ID then
+               Paren_Count := Paren_Count + 1;
+               if Paren_Count = 1 then
+                  Paren_Char_Pos := Tok.Char_Region.First;
+               end if;
+
+            elsif Tok.ID = Right_Paren_ID then
+               Paren_Count := Paren_Count - 1;
+
+            end if;
+
+            if Tok.First then
+               Text_Begin_Pos := Tok.Char_Region.First;
+               exit;
+            end if;
+         end;
+         I := I - 1;
+      end loop;
+
+      if Paren_Char_Pos /= Invalid_Buffer_Pos and Text_Begin_Pos /= 
Invalid_Buffer_Pos then
+         return 1 + Offset + Integer (Paren_Char_Pos - Text_Begin_Pos);
+      else
+         return Offset;
+      end if;
+   end Paren_In_Anchor_Line;
+
+   procedure Put (Cache : in Navigate_Cache_Type)
+   is
+      package Bounded is new Ada.Strings.Bounded.Generic_Bounded_Length (Max 
=> 2 + 11 * Chars_Per_Int);
+      use Bounded;
+
+      Line : Bounded_String := To_Bounded_String ("[");
+
+      procedure Append (Item : in Nil_Buffer_Pos)
+      is begin
+         if Item.Set then
+            Append (Line, Buffer_Pos'Image (Item.Item));
+         else
+            Append (Line, " -1");
+         end if;
+      end Append;
+   begin
+      Append (Line, Navigate_Cache_Code);
+      Append (Line, Buffer_Pos'Image (Cache.Pos));
+      Append (Line, WisiToken.Token_ID'Image (Cache.Statement_ID));
+      Append (Line, WisiToken.Token_ID'Image (Cache.ID));
+      Append (Line, Integer'Image (Cache.Length));
+      Append (Line, Integer'Image (Navigate_Class_Type'Pos (Cache.Class)));
+      Append (Cache.Containing_Pos);
+      Append (Cache.Prev_Pos);
+      Append (Cache.Next_Pos);
+      Append (Cache.End_Pos);
+      Append (Line, ']');
+      Ada.Text_IO.Put_Line (To_String (Line));
+   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);
+      use Bounded;
+
+      Line : Bounded_String := To_Bounded_String ("[");
+   begin
+      if Cache.Face.Set then
+         Append (Line, Face_Property_Code);
+         Append (Line, Buffer_Pos'Image (Cache.Region.First));
+         Append (Line, Buffer_Pos'Image (Cache.Region.Last));
+         Append (Line, Integer'Image (Cache.Face.Item));
+         Append (Line, ']');
+         Ada.Text_IO.Put_Line (To_String (Line));
+      end if;
+   end Put;
+
+   procedure Put (Line_Number : in Line_Number_Type; Item : in Indent_Type)
+   is begin
+      --  All Anchors must be resolved at this point, but not all lines have
+      --  an indent computed. A negative indent is an error in either the
+      --  grammar indent rules or the algorithms in this package.
+      case Item.Label is
+      when Not_Set =>
+         Ada.Text_IO.Put_Line
+           ('[' & Indent_Code & Trimmed_Image (Integer (Line_Number)) & " 0]");
+
+      when Int =>
+         declare
+            --  We can easily get negative indents when there are syntax 
errors.
+            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) & ']');
+         end;
+
+      when Anchor | 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)
+   is
+      use Ada.Containers;
+      use Ada.Strings.Unbounded;
+      use WisiToken.Parse.LR;
+
+      Line    : Unbounded_String := To_Unbounded_String ("[");
+      Last_Op : Config_Op        := (Fast_Forward, Token_Index'Last);
+
+   begin
+      if Trace_Action > Detail then
+         Ada.Text_IO.Put_Line (Parse.LR.Image (Item.Ops, Descriptor));
+      end if;
+
+      Append (Line, Recover_Code);
+      if Item.Ops.Length = 0 then
+         Append (Line, "]");
+
+      else
+         for I in Item.Ops.First_Index .. Item.Ops.Last_Index loop
+            declare
+               Op : Config_Op renames Item.Ops (I);
+            begin
+               case Op.Op is
+               when Fast_Forward =>
+                  if Last_Op.Op in Insert then
+                     Append (Line, "][]]");
+                  elsif Last_Op.Op in Delete then
+                     Append (Line, "]]");
+                  end if;
+
+                  Last_Op := Op;
+
+               when Undo_Reduce | Push_Back =>
+                  null;
+
+               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, "[");
+
+                  elsif Last_Op.Op = Delete then
+                     Append (Line, "]][");
+                     Append (Line, Buffer_Pos'Image (Terminals 
(Op.Token_Index).Char_Region.First));
+                     Append (Line, "[");
+
+                  else
+                     --  Last_Op.Op = Insert
+                     null;
+                  end if;
+                  Append (Line, Token_ID'Image (Op.ID));
+
+                  Last_Op := Op;
+
+               when Delete =>
+                  declare
+                     Skip : Boolean := False;
+                  begin
+                     if Last_Op.Op = Fast_Forward then
+                        Append (Line, "[");
+                        Append (Line, Buffer_Pos'Image (Terminals 
(Op.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))
+                        then
+                           declare
+                              Tok_1 : Augmented_Token renames Terminals 
(Last_Op.Token_Index);
+                              Tok_2 : Augmented_Token renames Terminals 
(Op.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
+                                 --  repaired text looks like a single string 
with an embedded quote.
+                                 --  But here, it is two STRING_LITERAL 
tokens. Don't send the second
+                                 --  delete to elisp. See 
test/ada_mode-recover_string_quote_1.adb
+                                 Skip := True;
+                              end if;
+                           end;
+                        end if;
+
+                     end if;
+
+                     if not Skip then
+                        Append (Line, Token_ID'Image (Op.ID));
+                     end if;
+                  end;
+                  Last_Op := Op;
+               end case;
+            end;
+         end loop;
+
+         case Last_Op.Op is
+         when Fast_Forward =>
+            Append (Line, "]");
+
+         when Undo_Reduce | Push_Back =>
+            null;
+
+         when Insert =>
+            Append (Line, "][]]]");
+         when Delete =>
+            Append (Line, "]]]");
+         end case;
+      end if;
+      Ada.Text_IO.Put_Line (To_String (Line));
+   end Put;
+
+   procedure Resolve_Anchors (Data : in out Parse_Data_Type)
+   is
+      Anchor_Indent : array (First_Anchor_ID .. Data.Max_Anchor_ID) of Integer;
+   begin
+      if Trace_Action > Outline then
+         Ada.Text_IO.New_Line;
+         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)));
+         end loop;
+      end if;
+
+      if Data.Max_Anchor_ID >= First_Anchor_ID then
+         for I in Data.Indents.First_Index .. Data.Indents.Last_Index loop
+            declare
+               Indent : constant Indent_Type := Data.Indents (I);
+            begin
+               case Indent.Label is
+               when Not_Set | Int =>
+                  null;
+
+               when Anchor =>
+                  for I of Indent.Anchor_IDs loop
+                     Anchor_Indent (I) := Indent.Anchor_Indent;
+                  end loop;
+                  Data.Indents.Replace_Element (I, (Int, 
Indent.Anchor_Indent));
+
+               when Anchored =>
+                  Data.Indents.Replace_Element (I, (Int, Anchor_Indent 
(Indent.Anchored_ID) + Indent.Anchored_Delta));
+
+               when Anchor_Anchored =>
+                  declare
+                     Temp : constant Integer :=
+                       Anchor_Indent (Indent.Anchor_Anchored_ID) + 
Indent.Anchor_Anchored_Delta;
+                  begin
+                     for I of Indent.Anchor_Anchored_IDs loop
+                        Anchor_Indent (I) := Temp;
+                     end loop;
+                     Data.Indents.Replace_Element (I, (Int, Temp));
+                  end;
+               end case;
+            end;
+         end loop;
+      end if;
+   end Resolve_Anchors;
+
+   procedure Set_End
+     (Data           : in out Parse_Data_Type;
+      Containing_Pos : in     Buffer_Pos;
+      End_Pos        : in     Buffer_Pos)
+   is
+      use Navigate_Cursor_Lists;
+      I            : Cursor := Data.End_Positions.First;
+      Delete_Cache : Boolean;
+      Temp         : Cursor;
+   begin
+      loop
+         exit when not Has_Element (I);
+         declare
+            Cache : Navigate_Cache_Type renames Data.Navigate_Caches (Element 
(I));
+         begin
+            if Cache.Pos in Containing_Pos .. End_Pos then
+               Cache.End_Pos := (True, End_Pos);
+               Delete_Cache := True;
+            else
+               Delete_Cache := False;
+            end if;
+         end;
+         if Delete_Cache then
+            Temp := Next (I);
+            Delete (Data.End_Positions, I);
+
+            I := Temp;
+         else
+            Next (I);
+         end if;
+
+      end loop;
+   end Set_End;
+
+   ----------
+   --  public subprograms (declaration order)
+
+   procedure Initialize
+     (Data              : in out Parse_Data_Type;
+      Descriptor        : access constant WisiToken.Descriptor;
+      Source_File_Name  : in     String;
+      Post_Parse_Action : in     Post_Parse_Action_Type;
+      Line_Count        : in     Line_Number_Type;
+      Params            : in     String)
+   is
+      pragma Unreferenced (Params);
+   begin
+      --  + 1 for data on line following last line; see Lexer_To_Augmented.
+      Data.Line_Begin_Pos.Set_Length (Ada.Containers.Count_Type (Line_Count + 
1), Default => Invalid_Buffer_Pos);
+      Data.Line_Paren_State.Set_Length (Ada.Containers.Count_Type (Line_Count 
+ 1));
+
+      Data.Descriptor        := Descriptor;
+      Data.Source_File_Name  := +Source_File_Name;
+      Data.Post_Parse_Action := Post_Parse_Action;
+
+      case Post_Parse_Action is
+      when Navigate | Face =>
+         null;
+      when Indent =>
+         Data.Indents.Set_Length (Ada.Containers.Count_Type (Line_Count));
+      end case;
+
+      Data.Reset;
+   end Initialize;
+
+   overriding procedure Reset (Data : in out Parse_Data_Type)
+   is begin
+      Data.Terminals.Clear;
+      Data.Leading_Non_Grammar.Clear;
+      --  Data.Line_Begin_Pos  set in Initialize, overwritten in 
Lexer_To_Augmented
+      --  Data.Line_Begin_Token  ""
+
+      for S of Data.Line_Paren_State loop
+         S := 0;
+      end loop;
+      Data.Current_Paren_State := 0;
+
+      Data.Navigate_Caches.Finalize;
+      Data.Navigate_Caches.Initialize;
+      Data.End_Positions.Clear;
+
+      Data.Face_Caches.Finalize;
+      Data.Face_Caches.Initialize;
+
+      for I in Data.Indents.First_Index .. Data.Indents.Last_Index loop
+         Data.Indents.Replace_Element (I, (Label => Not_Set));
+      end loop;
+      Data.Max_Anchor_ID := First_Anchor_ID - 1;
+   end Reset;
+
+   function Source_File_Name (Data : in Parse_Data_Type) return String
+   is begin
+      return -Data.Source_File_Name;
+   end Source_File_Name;
+
+   function Post_Parse_Action (Data : in Parse_Data_Type) return 
Post_Parse_Action_Type
+   is begin
+      return Data.Post_Parse_Action;
+   end Post_Parse_Action;
+
+   overriding
+   procedure Lexer_To_Augmented
+     (Data  : in out          Parse_Data_Type;
+      Token : in              Base_Token;
+      Lexer : not null access WisiToken.Lexer.Instance'Class)
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Lexer.First then
+         Data.Line_Begin_Pos (Token.Line) := Token.Char_Region.First;
+      end if;
+
+      if Token.ID < Data.Descriptor.First_Terminal then
+         --  Non-grammar token
+
+         if Token.ID = Data.Descriptor.New_Line_ID then
+            Data.Line_Paren_State (Token.Line + 1) := Data.Current_Paren_State;
+         end if;
+
+         if Data.Terminals.Length = 0 then
+            Data.Leading_Non_Grammar.Append (Token);
+         else
+            declare
+               Containing_Token : Augmented_Token renames Data.Terminals 
(Data.Terminals.Last_Index);
+
+               Trailing_Blank : constant Boolean :=
+                 Token.ID = Data.Descriptor.New_Line_ID and
+                 (Containing_Token.Non_Grammar.Length > 0 and then
+                    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
+                  Containing_Token.First := True;
+
+                  if Containing_Token.First_Trailing_Comment_Line = 
Invalid_Line_Number then
+                     Containing_Token.First_Trailing_Comment_Line := 
Token.Line;
+                  end if;
+                  Containing_Token.Last_Trailing_Comment_Line  := Token.Line;
+               end if;
+
+               Containing_Token.Non_Grammar.Append ((Token.ID, Token.Line, 
Token.Column, Lexer.First));
+            end;
+         end if;
+
+      else
+         --  grammar token
+         declare
+            Temp : constant Augmented_Token :=
+              (Token.ID,
+               Byte_Region                 => Token.Byte_Region,
+               Line                        => Token.Line,
+               Column                      => Token.Column,
+               Char_Region                 => Token.Char_Region,
+               Deleted                     => False,
+               First                       => Lexer.First,
+               Paren_State                 => Data.Current_Paren_State,
+               First_Terminals_Index       => Data.Terminals.Last_Index + 1,
+               Last_Terminals_Index        => Data.Terminals.Last_Index + 1,
+               First_Indent_Line           => (if Lexer.First then Token.Line 
else Invalid_Line_Number),
+               Last_Indent_Line            => (if Lexer.First then Token.Line 
else Invalid_Line_Number),
+               First_Trailing_Comment_Line => Invalid_Line_Number, -- Set by 
Reduce
+               Last_Trailing_Comment_Line  => Invalid_Line_Number,
+               Non_Grammar                 => <>);
+         begin
+            if Token.ID = Data.Descriptor.Left_Paren_ID then
+               Data.Current_Paren_State := Data.Current_Paren_State + 1;
+
+            elsif Token.ID = Data.Descriptor.Right_Paren_ID then
+               Data.Current_Paren_State := Data.Current_Paren_State - 1;
+            end if;
+
+            Data.Terminals.Append (Temp);
+         end;
+      end if;
+   end Lexer_To_Augmented;
+
+   overriding
+   procedure Delete_Token
+     (Data        : in out Parse_Data_Type;
+      Token_Index : in     WisiToken.Token_Index)
+   is
+      use all type Ada.Containers.Count_Type;
+      Deleted_Token    : Augmented_Token renames Data.Terminals (Token_Index);
+      Prev_Token_Index : WisiToken.Base_Token_Index := Token_Index - 1;
+   begin
+      pragma Assert (Deleted_Token.Deleted = False);
+      Deleted_Token.Deleted := True;
+      if Deleted_Token.Non_Grammar.Length = 0 then
+         return;
+      end if;
+
+      loop
+         if Prev_Token_Index = Base_Token_Index'First then
+            return;
+         end if;
+         exit when Data.Terminals (Prev_Token_Index).Deleted = False;
+         Prev_Token_Index := Prev_Token_Index - 1;
+      end loop;
+      declare
+         Prev_Token : Augmented_Token renames Data.Terminals 
(Prev_Token_Index);
+      begin
+         Prev_Token.Non_Grammar.Append (Deleted_Token.Non_Grammar);
+         if Deleted_Token.First_Trailing_Comment_Line /= Invalid_Line_Number 
then
+            if Prev_Token.First_Trailing_Comment_Line = Invalid_Line_Number 
then
+               if Deleted_Token.First then
+                  Prev_Token.First_Trailing_Comment_Line := 
Deleted_Token.First_Indent_Line;
+               else
+                  Prev_Token.First_Trailing_Comment_Line := 
Deleted_Token.First_Trailing_Comment_Line;
+               end if;
+            end if;
+            Prev_Token.Last_Trailing_Comment_Line  := 
Deleted_Token.Last_Trailing_Comment_Line;
+         end if;
+      end;
+   end Delete_Token;
+
+   overriding
+   procedure Reduce
+     (Data    : in out Parse_Data_Type;
+      Tree    : in out Syntax_Trees.Tree'Class;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     Syntax_Trees.Valid_Node_Index_Array)
+   is
+      Aug_Nonterm : constant Augmented_Token_Access := new Augmented_Token'
+        (ID          => Tree.ID (Nonterm),
+         Byte_Region => Tree.Byte_Region (Nonterm),
+         others      => <>);
+
+      Trailing_Comment_Done : Boolean := False;
+   begin
+      Tree.Set_Augmented (Nonterm, Base_Token_Class_Access (Aug_Nonterm));
+
+      for I in reverse Tokens'Range loop
+         --  'reverse' to find token containing trailing comments; last
+         --  non-virtual and non-empty token.
+         if Tree.Byte_Region (Tokens (I)) /= Null_Buffer_Region then
+            --  Token not entirely virtual
+            declare
+               Aug_Token : constant Aug_Token_Ref := Get_Aug_Token (Data, 
Tree, Tokens (I));
+            begin
+
+               if Data.Post_Parse_Action = Indent then
+                  if Aug_Token.First_Terminals_Index /= 
Augmented_Token_Arrays.No_Index then
+                     Aug_Nonterm.First_Terminals_Index := 
Aug_Token.First_Terminals_Index;
+                  end if;
+
+                  if Aug_Nonterm.Last_Terminals_Index = 
Augmented_Token_Arrays.No_Index then
+                     Aug_Nonterm.Last_Terminals_Index := 
Aug_Token.Last_Terminals_Index;
+                  end if;
+
+                  Aug_Nonterm.First := Aug_Nonterm.First or Aug_Token.First;
+
+                  if Aug_Token.First then
+                     if Aug_Token.First_Indent_Line /= Invalid_Line_Number then
+                        Aug_Nonterm.First_Indent_Line := 
Aug_Token.First_Indent_Line;
+                     elsif Trailing_Comment_Done and 
Aug_Token.First_Trailing_Comment_Line /= Invalid_Line_Number then
+                        Aug_Nonterm.First_Indent_Line := 
Aug_Token.First_Trailing_Comment_Line;
+                     end if;
+
+                     if Aug_Nonterm.Last_Indent_Line = Invalid_Line_Number then
+                        if Trailing_Comment_Done and 
Aug_Token.Last_Trailing_Comment_Line /= Invalid_Line_Number then
+                           Aug_Nonterm.Last_Indent_Line := 
Aug_Token.Last_Trailing_Comment_Line;
+                        elsif Aug_Token.Last_Indent_Line /= 
Invalid_Line_Number then
+                           Aug_Nonterm.Last_Indent_Line := 
Aug_Token.Last_Indent_Line;
+                        end if;
+                     end if;
+                  end if;
+
+                  if not Trailing_Comment_Done then
+                     Aug_Nonterm.First_Trailing_Comment_Line := 
Aug_Token.First_Trailing_Comment_Line;
+                     Aug_Nonterm.Last_Trailing_Comment_Line  := 
Aug_Token.Last_Trailing_Comment_Line;
+                     Trailing_Comment_Done := True;
+                  end if;
+
+               end if; --  Compute_Indent
+
+               if Aug_Token.Line /= Invalid_Line_Number then
+                  Aug_Nonterm.Line   := Aug_Token.Line;
+                  Aug_Nonterm.Column := Aug_Token.Column;
+               end if;
+
+               if Aug_Nonterm.Char_Region.First > Aug_Token.Char_Region.First 
then
+                  Aug_Nonterm.Char_Region.First := Aug_Token.Char_Region.First;
+               end if;
+
+               if Aug_Nonterm.Char_Region.Last < Aug_Token.Char_Region.Last 
then
+                  Aug_Nonterm.Char_Region.Last := Aug_Token.Char_Region.Last;
+               end if;
+
+               Aug_Nonterm.Paren_State := Aug_Token.Paren_State;
+            end;
+         end if; -- Aug_Token not virtual
+      end loop;
+   end Reduce;
+
+   procedure Statement_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      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;
+      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
+            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,
+                  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);
+                     Cache.Statement_ID   := Tree.ID (Nonterm);
+                     Cache.Containing_Pos := Containing_Pos;
+                  end;
+               else
+                  Cursor := Data.Navigate_Caches.Insert
+                    ((Pos            => Token.Char_Region.First,
+                      Statement_ID   => Tree.ID (Nonterm),
+                      ID             => Token.ID,
+                      Length         => Length (Token.Char_Region),
+                      Class          => (if Override_Start_Set then 
Override_Start else Pair.Class),
+                      Containing_Pos => Containing_Pos,
+                      others         => Nil));
+               end if;
+
+               Data.End_Positions.Append (Cursor);
+
+               if First_Item then
+                  First_Item := False;
+                  if Override_Start_Set or Pair.Class = Statement_Start then
+                     Override_Start_Set := False;
+                     Containing_Pos     := (True, Token.Char_Region.First);
+                  end if;
+               end if;
+
+               if Pair.Class = Statement_End and Containing_Pos.Set then
+                  Set_End (Data, Containing_Pos.Item, Token.Char_Region.First);
+               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 Containing_Action
+     (Data       : in out Parse_Data_Type;
+      Tree       : in     Syntax_Trees.Tree;
+      Nonterm    : in     Syntax_Trees.Valid_Node_Index;
+      Tokens     : in     Syntax_Trees.Valid_Node_Index_Array;
+      Containing : in     Positive_Index_Type;
+      Contained  : in     Positive_Index_Type)
+   is
+      use all type WisiToken.Syntax_Trees.Node_Label;
+      pragma Unreferenced (Nonterm);
+
+      --  [2] wisi-containing-action.
+      --
+      --  Compute as much as possible with virtual tokens; see
+      --  test/format_paramlist.adb
+   begin
+      if Tree.Label (Tokens (Containing)) = Syntax_Trees.Virtual_Terminal or
+        Tree.Label (Tokens (Contained)) = Syntax_Trees.Virtual_Terminal
+      then
+         return;
+      end if;
+
+      declare
+         use Navigate_Cache_Trees;
+         Containing_Tok    : constant Aug_Token_Ref := Get_Aug_Token (Data, 
Tree, Tokens (Containing));
+         Containing_Region : Buffer_Region renames Containing_Tok.Char_Region;
+         Contained_Tok     : constant Aug_Token_Ref := Get_Aug_Token (Data, 
Tree, Tokens (Contained));
+         Contained_Region  : Buffer_Region renames Contained_Tok.Char_Region;
+         Iterator          : constant Navigate_Cache_Trees.Iterator := 
Data.Navigate_Caches.Iterate;
+         Cursor            : Navigate_Cache_Trees.Cursor;
+         Mark              : constant Buffer_Pos                    := 
Containing_Region.First;
+      begin
+         if Containing_Region = Null_Buffer_Region then
+            if Tree.Is_Virtual (Tokens (Containing)) then
+               return;
+            else
+               raise Fatal_Error with Error_Message
+                 (File_Name => -Data.Source_File_Name,
+                  Line      => Containing_Tok.Line,
+                  Column    => Containing_Tok.Column,
+                  Message   => "wisi-containing-action: containing-region " &
+                    Image (Containing_Tok.ID, Data.Descriptor.all) &
+                    " is empty. grammar error; bad action.");
+            end if;
+         end if;
+
+         if not Data.Navigate_Caches.Present (Containing_Region.First) then
+            raise Fatal_Error with Error_Message
+              (File_Name => -Data.Source_File_Name,
+               Line      => Containing_Tok.Line,
+               Column    => Containing_Tok.Column,
+               Message   => "wisi-containing-action: containing token " &
+                 Image (Containing_Tok.ID, Data.Descriptor.all) &
+                 " has no cache. grammar error; missing action.");
+         end if;
+
+         if Contained_Tok.Char_Region /= Null_Buffer_Region then
+            --  Contained region is nil in an empty production.
+            Cursor := Previous (Iterator, Contained_Tok.Char_Region.Last);
+
+            while Has_Element (Cursor) loop
+               declare
+                  Cache : Navigate_Cache_Type renames Variable_Ref 
(Data.Navigate_Caches, Cursor).Element.all;
+               begin
+
+                  exit when Cache.Pos < Contained_Region.First or
+                    (Containing_Region.First = Contained_Region.First and
+                       Cache.Pos <= Contained_Region.First);
+
+                  --  Skip blocks that are already marked.
+
+                  if Cache.Containing_Pos.Set then
+                     Cursor := Find (Iterator, Cache.Containing_Pos.Item, 
Direction => Descending);
+                  else
+                     Cache.Containing_Pos := (True, Mark);
+                     Cursor := Previous (Iterator, Cursor);
+                  end if;
+
+               end;
+            end loop;
+         end if;
+      end;
+   end Containing_Action;
+
+   function "+" (Item : in WisiToken.Token_ID) return Token_ID_Lists.List
+   is begin
+      return Result : Token_ID_Lists.List do
+         Result.Append (Item);
+      end return;
+   end "+";
+
+   function "&" (List : in Token_ID_Lists.List; Item : in WisiToken.Token_ID) 
return Token_ID_Lists.List
+   is begin
+      return Result : Token_ID_Lists.List := List do
+         Result.Append (Item);
+      end return;
+   end "&";
+
+   function "&" (Left, Right : in WisiToken.Token_ID) return 
Token_ID_Lists.List
+   is begin
+      return Result : Token_ID_Lists.List do
+         Result.Append (Left);
+         Result.Append (Right);
+      end return;
+   end "&";
+
+   procedure Motion_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      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;
+
+      Start             : Nil_Buffer_Pos := (Set => False);
+      Prev_Keyword_Mark : Nil_Buffer_Pos := (Set => False);
+      Iter              : constant Iterator := Data.Navigate_Caches.Iterate;
+      Prev_Cache_Cur    : Cursor;
+      Cache_Cur         : Cursor;
+      Point             : Buffer_Pos;
+
+      function Match (IDs : in Token_ID_Lists.List) return Boolean
+      is
+         Cache : Navigate_Cache_Type renames Constant_Ref 
(Data.Navigate_Caches, Cache_Cur).Element.all;
+      begin
+         --  [2] wisi-elisp-parse--match-token
+         if (Start.Set and then Point = Start.Item) or else
+           Cache.Containing_Pos = Start
+         then
+            for ID of IDs loop
+               if ID = Cache.ID then
+                  return True;
+               end if;
+            end loop;
+         end if;
+         return False;
+      end Match;
+
+   begin
+      for Param of Params loop
+         if Tree.Byte_Region (Tokens (Param.Index)) /= Null_Buffer_Region then
+            declare
+               Token  : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tokens (Param.Index));
+               Region : constant Buffer_Region := Token.Char_Region;
+            begin
+               if not Start.Set then
+                  Start := (True, Region.First);
+               end if;
+
+               Cache_Cur := Find (Iter, Region.First, Direction => Ascending);
+               if not Has_Element (Cache_Cur) then
+                  if Tree.Is_Virtual (Tokens (Param.Index)) then
+                     return;
+                  else
+                     raise Fatal_Error with Error_Message
+                       (File_Name => -Data.Source_File_Name,
+                        Line      => Token.Line,
+                        Column    => Token.Column,
+                        Message   => "wisi-motion-action: token " &
+                          Token.Image (Data.Descriptor.all) &
+                          " has no cache; add to statement-action.");
+                  end if;
+               end if;
+
+               if Param.IDs.Length = 0 then
+                  if Prev_Keyword_Mark.Set then
+                     Variable_Ref (Data.Navigate_Caches, 
Cache_Cur).Element.Prev_Pos      := Prev_Keyword_Mark;
+                     Variable_Ref (Data.Navigate_Caches, 
Prev_Cache_Cur).Element.Next_Pos := (True, Region.First);
+                  end if;
+
+                  Prev_Keyword_Mark := (True, Region.First);
+                  Prev_Cache_Cur    := Cache_Cur;
+
+               else
+                  Point := Region.First;
+                  loop
+                     exit when Point >= Region.Last;
+                     if Match (Param.IDs) then
+                        if Prev_Keyword_Mark.Set then
+                           if not Constant_Ref (Data.Navigate_Caches, 
Cache_Cur).Element.Prev_Pos.Set and
+                             not Constant_Ref (Data.Navigate_Caches, 
Prev_Cache_Cur).Element.Next_Pos.Set
+                           then
+                              Variable_Ref (Data.Navigate_Caches, 
Cache_Cur).Element.Prev_Pos      := Prev_Keyword_Mark;
+                              Variable_Ref (Data.Navigate_Caches, 
Prev_Cache_Cur).Element.Next_Pos := (True, Point);
+                              Prev_Keyword_Mark := (True, Point);
+                              Prev_Cache_Cur    := Cache_Cur;
+                           end if;
+                        else
+                           Prev_Keyword_Mark := (True, Point);
+                           Prev_Cache_Cur    := Cache_Cur;
+                        end if;
+                     end if;
+
+                     Cache_Cur := Next (Iter, Cache_Cur);
+                     exit when Cache_Cur = No_Element;
+
+                     Point := Constant_Ref (Data.Navigate_Caches, 
Cache_Cur).Element.Pos;
+                  end loop;
+               end if;
+            end;
+         end if;
+      end loop;
+   end Motion_Action;
+
+   procedure Face_Apply_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
+      Params  : in     Face_Apply_Param_Array)
+   is
+      pragma Unreferenced (Nonterm);
+
+      --  [2] wisi-face-apply-action
+      use Face_Cache_Trees;
+
+      Iter       : constant Iterator := Data.Face_Caches.Iterate;
+      Cache_Cur  : Cursor;
+      Suffix_Cur : Cursor;
+   begin
+      for Param of Params loop
+         if Tree.Byte_Region (Tokens (Param.Index)) /= Null_Buffer_Region then
+            declare
+               Token : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tokens (Param.Index));
+            begin
+               Cache_Cur := Find (Iter, Token.Char_Region.First, Direction => 
Ascending);
+               if Has_Element (Cache_Cur) then
+                  declare
+                     Cache : Face_Cache_Type renames Variable_Ref 
(Data.Face_Caches, Cache_Cur).Element.all;
+                  begin
+                     case Cache.Class is
+                     when Prefix =>
+                        Cache.Face := (True, Param.Prefix_Face);
+
+                        --  Check for suffix
+                        Suffix_Cur := Next (Iter, Cache_Cur);
+                        if Has_Element (Suffix_Cur) then
+                           declare
+                              Suf_Cache : Face_Cache_Type renames Variable_Ref
+                                (Data.Face_Caches, Suffix_Cur).Element.all;
+                           begin
+                              if Suffix = Suf_Cache.Class and
+                                Inside (Suf_Cache.Region.First, 
Token.Char_Region)
+                              then
+                                 Suf_Cache.Face := (True, Param.Suffix_Face);
+                              end if;
+                           end;
+                        end if;
+
+                     when Suffix =>
+                        Cache.Face := (True, Param.Suffix_Face);
+                     end case;
+                  end;
+               else
+                  Data.Face_Caches.Insert ((Token.Char_Region, Suffix, (True, 
Param.Suffix_Face)));
+               end if;
+            end;
+         end if;
+      end loop;
+   end Face_Apply_Action;
+
+   procedure Face_Apply_List_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
+      Params  : in     Face_Apply_Param_Array)
+   is
+      pragma Unreferenced (Nonterm);
+
+      --  [2] wisi-face-apply-list-action
+      use Face_Cache_Trees;
+
+      Iter      : constant Iterator := Data.Face_Caches.Iterate;
+      Cache_Cur : Cursor;
+   begin
+      for Param of Params loop
+         if Tree.Byte_Region (Tokens (Param.Index)) /= Null_Buffer_Region then
+            declare
+               Token : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tokens (Param.Index));
+            begin
+               Cache_Cur := Find_In_Range (Iter, Ascending, 
Token.Char_Region.First, Token.Char_Region.Last);
+               loop
+                  exit when not Has_Element (Cache_Cur) or else
+                    Constant_Ref (Data.Face_Caches, 
Cache_Cur).Element.Region.First > Token.Char_Region.Last;
+                  declare
+                     Cache : Face_Cache_Type renames Variable_Ref 
(Data.Face_Caches, Cache_Cur).Element.all;
+                  begin
+                     case Cache.Class is
+                     when Prefix =>
+                        Cache.Face := (True, Param.Prefix_Face);
+
+                     when Suffix =>
+                        Cache.Face := (True, Param.Suffix_Face);
+                     end case;
+                  end;
+                  Cache_Cur := Next (Iter, Cache_Cur);
+               end loop;
+            end;
+         end if;
+      end loop;
+   end Face_Apply_List_Action;
+
+   procedure Face_Mark_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
+      Params  : in     Face_Mark_Param_Array)
+   is
+      pragma Unreferenced (Nonterm);
+
+      --  [2] wisi-face-apply-action
+      use Face_Cache_Trees;
+
+      Iter      : constant Iterator := Data.Face_Caches.Iterate;
+      Cache_Cur : Cursor;
+   begin
+      for Param of Params loop
+         if Tree.Byte_Region (Tokens (Param.Index)) /= Null_Buffer_Region then
+            declare
+               Token : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tokens (Param.Index));
+            begin
+               Cache_Cur := Find (Iter, Token.Char_Region.First, Direction => 
Ascending);
+               if Has_Element (Cache_Cur) then
+                  declare
+                     Cache : Face_Cache_Type renames Variable_Ref 
(Data.Face_Caches, Cache_Cur).Element.all;
+                     Other_Cur : Cursor := Find_In_Range
+                       (Iter, Ascending, Cache.Region.Last + 1, 
Token.Char_Region.Last);
+                     Temp : Cursor;
+                  begin
+                     loop
+                        exit when not Has_Element (Other_Cur) or else
+                          Constant_Ref (Data.Face_Caches, 
Other_Cur).Element.Region.First > Token.Char_Region.Last;
+                        Temp := Other_Cur;
+                        Other_Cur := Next (Iter, Other_Cur);
+                        Delete (Data.Face_Caches, Temp);
+                     end loop;
+
+                     Cache.Class       := Param.Class;
+                     Cache.Region.Last := Token.Char_Region.Last;
+                  end;
+               else
+                  Data.Face_Caches.Insert ((Token.Char_Region, Param.Class, 
(Set => False)));
+               end if;
+            end;
+         end if;
+      end loop;
+   end Face_Mark_Action;
+
+   procedure Face_Remove_Action
+     (Data    : in out Parse_Data_Type;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
+      Params  : in     Face_Remove_Param_Array)
+   is
+      pragma Unreferenced (Nonterm);
+
+      --  [2] wisi-face-remove-action
+      use Face_Cache_Trees;
+
+      Iter      : constant Iterator := Data.Face_Caches.Iterate;
+      Cache_Cur : Cursor;
+      Temp      : Cursor;
+   begin
+      for I of Params loop
+         if Tree.Byte_Region (Tokens (I)) /= Null_Buffer_Region then
+            declare
+               Token : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tokens (I));
+            begin
+               Cache_Cur := Find_In_Range (Iter, Ascending, 
Token.Char_Region.First, Token.Char_Region.Last);
+               loop
+                  exit when not Has_Element (Cache_Cur) or else
+                    Constant_Ref (Data.Face_Caches, 
Cache_Cur).Element.Region.First > Token.Char_Region.Last;
+                  Temp := Cache_Cur;
+                  Cache_Cur := Next (Iter, Cache_Cur);
+                  Delete (Data.Face_Caches, Temp);
+               end loop;
+            end;
+         end if;
+      end loop;
+   end Face_Remove_Action;
+
+   function "+" (Item : in Integer) return Indent_Arg_Arrays.Vector
+   is begin
+      return Result : Indent_Arg_Arrays.Vector do
+         Result.Append (Item);
+      end return;
+   end "+";
+
+   function "&" (List : in Indent_Arg_Arrays.Vector; Item : in Integer) return 
Indent_Arg_Arrays.Vector
+   is begin
+      return Result : Indent_Arg_Arrays.Vector := List do
+         Result.Append (Item);
+      end return;
+   end "&";
+
+   function "&" (Left, Right : in Integer) return Indent_Arg_Arrays.Vector
+   is begin
+      return Result : Indent_Arg_Arrays.Vector do
+         Result.Append (Left);
+         Result.Append (Right);
+      end return;
+   end "&";
+
+   function Image (Item : in Simple_Indent_Param) return String
+   is begin
+      return "(" & Simple_Indent_Param_Label'Image (Item.Label) &
+        (case Item.Label is
+         when Int => Integer'Image (Item.Int_Delta),
+         when Anchored_Label => WisiToken.Positive_Index_Type'Image 
(Item.Anchored_Index) & "," &
+              Integer'Image (Item.Anchored_Delta),
+         when Language => "<language_function>") & ")";
+   end Image;
+
+   function Image (Item : in Indent_Param) return String
+   is begin
+      return "(" & Indent_Param_Label'Image (Item.Label) & ", " &
+        (case Item.Label is
+         when Simple => Image (Item.Param),
+         when Hanging_Label =>
+            Image (Item.Hanging_Delta_1) & ", "  & Image 
(Item.Hanging_Delta_2) & ")");
+   end Image;
+
+   function Image (Item : in Indent_Pair) return String
+   is begin
+      return "(" & Image (Item.Code_Delta) &
+        (if Item.Comment_Present
+         then ", " & Image (Item.Comment_Delta)
+         else "") & ")";
+   end Image;
+
+   procedure Indent_Action_0
+     (Data    : in out Parse_Data_Type'Class;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
+      Params  : in     Indent_Param_Array)
+   is begin
+      if Trace_Action > Outline then
+         Ada.Text_IO.Put_Line ("indent_action_0: " & Tree.Image (Nonterm, 
Data.Descriptor.all));
+      end if;
+
+      --  [2] wisi-indent-action
+      for I in Tokens'Range loop
+         if Tree.Byte_Region (Tokens (I)) /= Null_Buffer_Region then
+            declare
+               use all type SAL.Base_Peek_Type;
+               Tree_Token : constant Syntax_Trees.Valid_Node_Index := Tokens 
(I);
+
+               Token             : constant Aug_Token_Ref := Get_Aug_Token 
(Data, Tree, Tree_Token);
+               Pair              : Indent_Pair renames Params (I);
+               Code_Delta        : Delta_Type;
+               Comment_Param     : Indent_Param;
+               Comment_Param_Set : Boolean                := False;
+               Comment_Delta     : Delta_Type;
+            begin
+               if Trace_Action > Detail then
+                  Ada.Text_IO.Put_Line
+                    ("indent_action_0 a: " & Tree.Image (Tree_Token, 
Data.Descriptor.all) & ": " & Image (Pair));
+               end if;
+
+               if Token.First_Indent_Line /= Invalid_Line_Number then
+                  Code_Delta := Indent_Compute_Delta
+                    (Data, Tree, Tokens, Pair.Code_Delta, Tree_Token, 
Indenting_Comment => False);
+
+                  if Code_Delta /= Null_Delta then
+                     Indent_Token_1 (Data, Token, Code_Delta, 
Indenting_Comment => False);
+                  end if;
+               end if;
+
+               if Token.First_Trailing_Comment_Line /= Invalid_Line_Number then
+                  if Pair.Comment_Present then
+                     Comment_Param     := Pair.Comment_Delta;
+                     Comment_Param_Set := True;
+
+                  elsif I < Tokens'Last then
+                     Comment_Param     := Params (I + 1).Code_Delta;
+                     Comment_Param_Set := True;
+                  end if;
+
+                  if Comment_Param_Set then
+                     Comment_Delta := Indent_Compute_Delta
+                       (Data, Tree, Tokens, Comment_Param, Tree_Token, 
Indenting_Comment => True);
+
+                     if Comment_Delta /= Null_Delta then
+                        Indent_Token_1 (Data, Token, Comment_Delta, 
Indenting_Comment => True);
+                     end if;
+                  end if;
+               end if;
+            end;
+         end if;
+      end loop;
+   end Indent_Action_0;
+
+   procedure Indent_Action_1
+     (Data    : in out Parse_Data_Type'Class;
+      Tree    : in     Syntax_Trees.Tree;
+      Nonterm : in     Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     Syntax_Trees.Valid_Node_Index_Array;
+      N       : in     Positive_Index_Type;
+      Params  : in     Indent_Param_Array)
+   is
+      use all type WisiToken.Syntax_Trees.Node_Label;
+   begin
+      --  [2] wisi-indent-action*
+      for I in Tokens'First .. N loop
+         if Tree.Label (Tokens (I)) /= Virtual_Terminal and then
+           Get_Aug_Token (Data, Tree, Tokens (I)).First
+         then
+            Indent_Action_0 (Data, Tree, Nonterm, Tokens, Params);
+            return;
+         end if;
+      end loop;
+   end Indent_Action_1;
+
+   function Indent_Hanging_1
+     (Data              : in out Parse_Data_Type;
+      Tree              : in     Syntax_Trees.Tree;
+      Tokens            : in     Syntax_Trees.Valid_Node_Index_Array;
+      Tree_Indenting    : in     Syntax_Trees.Valid_Node_Index;
+      Indenting_Comment : in     Boolean;
+      Delta_1           : in     Simple_Indent_Param;
+      Delta_2           : in     Simple_Indent_Param;
+      Option            : in     Boolean;
+      Accumulate        : in     Boolean)
+     return Delta_Type
+   is
+      Indenting_Token : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tree_Indenting);
+   begin
+      --  [2] wisi-elisp-parse--hanging-1
+      if Indenting_Comment then
+         return Indent_Compute_Delta
+           (Data, Tree, Tokens, (Simple, Delta_1), Tree_Indenting, 
Indenting_Comment);
+      else
+         return
+           (Hanging,
+            Hanging_First_Line  => Indenting_Token.Line,
+            Hanging_Paren_State => Indenting_Token.Paren_State,
+            Hanging_Delta_1     => Indent_Compute_Delta
+              (Data, Tree, Tokens, (Simple, Delta_1), Tree_Indenting, 
Indenting_Comment).Simple_Delta,
+            Hanging_Delta_2     =>
+              (if (not Option) or
+                 Indenting_Token.Line = Indenting_Token.First_Indent_Line -- 
first token in tok is first on line
+               then Indent_Compute_Delta
+                 (Data, Tree, Tokens, (Simple, Delta_2), Tree_Indenting, 
Indenting_Comment).Simple_Delta
+               else Indent_Compute_Delta
+                 (Data, Tree, Tokens, (Simple, Delta_1), Tree_Indenting, 
Indenting_Comment).Simple_Delta),
+            Hanging_Accumulate => Accumulate);
+      end if;
+   end Indent_Hanging_1;
+
+   procedure Put (Data : in out Parse_Data_Type)
+   is begin
+      case Data.Post_Parse_Action is
+      when Navigate =>
+         for Cache of Data.Navigate_Caches loop
+            Put (Cache);
+         end loop;
+
+      when Face =>
+         for Cache of Data.Face_Caches loop
+            Put (Cache);
+         end loop;
+
+      when Indent =>
+         --  We don't need "Indent_Leading_Comments"; they are indented to 0,
+         --  which is the default.
+
+         Resolve_Anchors (Data);
+
+         --  Can't set indent for first line
+         for I in Data.Indents.First_Index + 1 .. Data.Indents.Last_Index loop
+            Put (I, Data.Indents (I));
+         end loop;
+      end case;
+   end Put;
+
+   procedure Put
+     (Data         : in Parse_Data_Type;
+      Lexer_Errors : in Lexer.Error_Lists.List;
+      Parse_Errors : in Parse.LR.Parse_Error_Lists.List;
+      Tree         : in Syntax_Trees.Tree)
+   is
+      use all type SAL.Base_Peek_Type;
+      use Ada.Text_IO;
+      use Semantic_Checks;
+
+      function Safe_Pos (Node : in Syntax_Trees.Valid_Node_Index) return 
Buffer_Pos
+      is
+         --  Return a reasonable position for the error at Node.
+         --
+         --  In a successful parse with error recovery, Node is a terminal with
+         --  an augmented token in Data.Terminals, so that is the first
+         --  choice.
+         --
+         --  If this is an error due to a bad recovery, Node may be a virtual
+         --  token, with no position information, so we try to get information
+         --  from its parent.
+         use Syntax_Trees;
+
+         N : Node_Index := Node;
+      begin
+         loop
+            if Tree.Label (N) /= Virtual_Terminal then
+               declare
+                  Ref : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
N);
+               begin
+                  if Ref.Char_Region /= Null_Buffer_Region then
+                     return Ref.Element.Char_Region.First;
+                  end if;
+
+               end;
+            end if;
+            N := Tree.Parent (N);
+            exit when N = Invalid_Node_Index;
+         end loop;
+         return Buffer_Pos'First;
+      end Safe_Pos;
+
+      function Safe_Pos (Token : in Recover_Token) return Buffer_Pos
+      is begin
+         if Token.Name /= Null_Buffer_Region then
+            return Token.Name.First;
+
+         elsif Token.Byte_Region = Null_Buffer_Region then
+            return Buffer_Pos'First;
+
+         else
+            return Token.Byte_Region.First;
+         end if;
+      end Safe_Pos;
+
+   begin
+      for Item of Lexer_Errors loop
+         Put_Line
+           ('[' & Lexer_Error_Code & Buffer_Pos'Image (Item.Char_Pos) &
+              " ""lexer error" &
+              (if Item.Recover_Char (1) = ASCII.NUL
+               then """"
+               elsif Item.Recover_Char (1) = '"'
+               then """ ?\"""
+               else """ ?" & Item.Recover_Char (1)) &
+              "]");
+         if Item.Recover_Char (2) /= ASCII.NUL then
+            raise SAL.Programmer_Error with "lexer error with non-ascii or 
multiple repair char";
+         end if;
+      end loop;
+
+      for Item of Parse_Errors loop
+         --  We don't include parser id here; not very useful.
+         case Item.Label is
+         when Parse.LR.Action =>
+            Put_Line
+              ('[' & Parser_Error_Code & Buffer_Pos'Image (Safe_Pos 
(Item.Error_Token)) &
+                 " ""syntax error: expecting " & Image (Item.Expecting, 
Data.Descriptor.all) &
+                 ", found '" & Image (Tree.ID (Item.Error_Token), 
Data.Descriptor.all) & "'""]");
+
+         when Parse.LR.Check =>
+            Put_Line
+              ('[' & Check_Error_Code & Integer'Image
+                 (Semantic_Checks.Check_Status_Label'Pos 
(Item.Check_Status.Label)) &
+                 (case Item.Check_Status.Label is
+                  when Ok => "",
+                  when Error =>
+                     Buffer_Pos'Image (Safe_Pos 
(Item.Check_Status.Begin_Name)) &
+                     Buffer_Pos'Image (Safe_Pos (Item.Check_Status.End_Name)) &
+                       " ""block name error""]"));
+
+         when Parse.LR.Message =>
+            Put_Line
+              ('[' & Parser_Error_Code & Buffer_Pos'Image (Buffer_Pos'First) &
+                 " """ & (-Item.Msg) & """]");
+         end case;
+
+         if Item.Recover.Stack.Depth > 0 then
+            Put (Item.Recover, Data.Terminals, Data.Descriptor.all);
+         end if;
+      end loop;
+   end Put;
+
+   procedure Put_Error (Data : in Parse_Data_Type; Line_Number : in 
Line_Number_Type; Message : in String)
+   is
+      use Ada.Text_IO;
+   begin
+      Put_Line ("(error """ & Error_Message (-Data.Source_File_Name, 
Line_Number, 0, Message) & """)");
+   end Put_Error;
+
+   ----------
+   --  Spec visible private subprograms, alphabetical
+
+   function Image (Item : in Simple_Delta_Type) return String
+   is begin
+      return "(" & Simple_Delta_Labels'Image (Item.Label) &
+        (case Item.Label is
+         when Int => Integer'Image (Item.Int_Delta),
+         when Anchored => Integer'Image (Item.Anchored_ID) & Integer'Image 
(Item.Anchored_Delta) & " " &
+              Boolean'Image (Item.Anchored_Accumulate) & ")");
+   end Image;
+
+   function Image (Item : in Delta_Type) return String
+   is begin
+      return "(" & Delta_Labels'Image (Item.Label) &
+        (case Item.Label is
+         when Simple => " " & Image (Item.Simple_Delta),
+         when Hanging => Line_Number_Type'Image (Item.Hanging_First_Line) & 
Integer'Image (Item.Hanging_Paren_State) &
+              " " & Image (Item.Hanging_Delta_1) & " " & Image 
(Item.Hanging_Delta_2) & " " &
+              Boolean'Image (Item.Hanging_Accumulate)) & ")";
+   end Image;
+
+   function Current_Indent_Offset
+     (Data         : in Parse_Data_Type;
+      Anchor_Token : in Augmented_Token'Class;
+      Offset       : in Integer)
+     return Integer
+   is begin
+      return Offset + Integer (Anchor_Token.Char_Region.First - 
Data.Line_Begin_Pos (Anchor_Token.Line));
+   end Current_Indent_Offset;
+
+   function Find
+     (Data  : in Parse_Data_Type;
+      ID    : in Token_ID;
+      Token : in Augmented_Token'Class)
+     return Base_Token_Index
+   is begin
+      --  linear search for ID.
+      for I in Token.First_Terminals_Index .. Token.Last_Terminals_Index loop
+         if Data.Terminals (I).ID = ID then
+            return I;
+         end if;
+      end loop;
+      return Augmented_Token_Arrays.No_Index;
+   end Find;
+
+   function First_Line
+     (Token             : in Augmented_Token;
+      Indenting_Comment : in Boolean)
+     return Line_Number_Type
+   is begin
+      return
+        (if Indenting_Comment then
+           (if Token.First_Trailing_Comment_Line = Invalid_Line_Number
+            then Token.Line
+            else Token.First_Trailing_Comment_Line)
+         else
+           (if Token.First_Indent_Line = Invalid_Line_Number
+            then Token.Line
+            else Token.First_Indent_Line));
+   end First_Line;
+
+   function Get_Aug_Token
+     (Data       : in Parse_Data_Type'Class;
+      Tree       : in Syntax_Trees.Tree'Class;
+      Tree_Index : in Syntax_Trees.Valid_Node_Index)
+     return Aug_Token_Ref
+   is
+      use all type Syntax_Trees.Node_Label;
+   begin
+      return
+        (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 Nonterm => (Element => Augmented_Token_Access (Tree.Augmented 
(Tree_Index))));
+   end Get_Aug_Token;
+
+   overriding
+   function Image
+     (Item       : in Augmented_Token;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is
+      ID_Image : constant String := WisiToken.Image (Item.ID, Descriptor);
+   begin
+      if Item.Line /= Invalid_Line_Number and Trace_Action <= Detail then
+         return "(" & ID_Image &
+           Line_Number_Type'Image (Item.Line) & ":" & Trimmed_Image (Integer 
(Item.Column)) & ")";
+
+      elsif Item.Char_Region = Null_Buffer_Region then
+         return "(" & ID_Image & ")";
+
+      else
+         return "(" & ID_Image & ", " & Image (Item.Char_Region) & ")";
+      end if;
+   end Image;
+
+   function Image
+     (Item       : in Augmented_Token_Access_Array;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is
+      use all type SAL.Base_Peek_Type;
+      use Ada.Strings.Unbounded;
+      Result : Unbounded_String := +"(";
+   begin
+      for I in Item'Range loop
+         Result := Result & Image (Item (I).all, Descriptor);
+         if I /= Item'Last then
+            Result := Result & ", ";
+         end if;
+      end loop;
+      Result := Result & ")";
+      return -Result;
+   end Image;
+
+   function Indent_Anchored_2
+     (Data        : in out Parse_Data_Type;
+      Anchor_Line : in     Line_Number_Type;
+      Last_Line   : in     Line_Number_Type;
+      Offset      : in     Integer;
+      Accumulate  : in     Boolean)
+     return Delta_Type
+   is
+      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
+      --  types; AARM 4.8 (6/3)), and we may need to change the Label.
+      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);
+
+      when Int =>
+         Indent := (Anchor, To_Vector (Anchor_ID, 1), Indent.Int_Indent);
+
+      when Anchor =>
+         Indent.Anchor_IDs := Anchor_ID & Indent.Anchor_IDs;
+
+      when Anchored =>
+         Indent := (Anchor_Anchored, To_Vector (Anchor_ID, 1), 
Indent.Anchored_ID, Indent.Anchored_Delta);
+
+      when Anchor_Anchored =>
+         Indent.Anchor_Anchored_IDs := Anchor_ID & Indent.Anchor_Anchored_IDs;
+      end case;
+
+      Data.Indents.Replace_Element (Anchor_Line, Indent);
+
+      return (Simple, (Anchored, Anchor_ID, Offset, Accumulate));
+   end Indent_Anchored_2;
+
+   function Indent_Compute_Delta
+     (Data              : in out Parse_Data_Type'Class;
+      Tree              : in     Syntax_Trees.Tree;
+      Tokens            : in     Syntax_Trees.Valid_Node_Index_Array;
+      Param             : in     Indent_Param;
+      Tree_Indenting    : in     Syntax_Trees.Valid_Node_Index;
+      Indenting_Comment : in     Boolean)
+     return Delta_Type
+   is
+      Indenting_Token : constant Aug_Token_Ref := Get_Aug_Token (Data, Tree, 
Tree_Indenting);
+   begin
+      --  [2] wisi-elisp-parse--indent-compute-delta, which evals 
wisi-anchored*, wisi-hanging*.
+      case Param.Label is
+      when Simple =>
+         case Param.Param.Label is
+         when Int =>
+            return (Simple, (Int, Param.Param.Int_Delta));
+
+         when Anchored_Label =>
+            if Indenting_Token.Byte_Region = Null_Buffer_Region or
+              Tree.Byte_Region (Tokens (Param.Param.Anchored_Index)) = 
Null_Buffer_Region
+            then
+               --  One of these is an entirely virtual token
+               return Null_Delta;
+            else
+               declare
+                  Anchor_Token : constant Aug_Token_Ref := Get_Aug_Token
+                    (Data, Tree, Tokens (Param.Param.Anchored_Index));
+               begin
+                  case Anchored_Label'(Param.Param.Label) is
+                  when Anchored_0 =>
+                     --  [2] wisi-anchored, wisi-anchored-1
+                     return Indent_Anchored_2
+                       (Data,
+                        Anchor_Line => Anchor_Token.Line,
+                        Last_Line   => Indenting_Token.Last_Line 
(Indenting_Comment),
+                        Offset      => Current_Indent_Offset (Data, 
Anchor_Token, Param.Param.Anchored_Delta),
+                        Accumulate  => True);
+
+                  when Anchored_1 =>
+                     --  [2] wisi-anchored%
+                     return Indent_Anchored_2
+                       (Data,
+                        Anchor_Line => Anchor_Token.Line,
+                        Last_Line   => Indenting_Token.Last_Line 
(Indenting_Comment),
+                        Offset      => Paren_In_Anchor_Line (Data, 
Anchor_Token, Param.Param.Anchored_Delta),
+                        Accumulate  => True);
+
+                  when Anchored_2 =>
+                     --  [2] wisi-anchored%-
+                     return Indent_Anchored_2
+                       (Data,
+                        Anchor_Line => Anchor_Token.Line,
+                        Last_Line   => Indenting_Token.Last_Line 
(Indenting_Comment),
+                        Offset      => Paren_In_Anchor_Line (Data, 
Anchor_Token, Param.Param.Anchored_Delta),
+                        Accumulate  => False);
+
+                  when Anchored_3 =>
+                     --  [2] wisi-anchored*
+                     if Indenting_Token.First then
+                        return Indent_Anchored_2
+                          (Data,
+                           Anchor_Line => Anchor_Token.Line,
+                           Last_Line   => Indenting_Token.Last_Line 
(Indenting_Comment),
+                           Offset      => Current_Indent_Offset (Data, 
Anchor_Token, Param.Param.Anchored_Delta),
+                           Accumulate  => True);
+
+                     else
+                        return Null_Delta;
+                     end if;
+
+                  when Anchored_4 =>
+                     --  [2] wisi-anchored*-
+                     return Indent_Anchored_2
+                       (Data,
+                        Anchor_Line => Anchor_Token.Line,
+                        Last_Line   => Indenting_Token.Last_Line 
(Indenting_Comment),
+                        Offset      => Current_Indent_Offset (Data, 
Anchor_Token, Param.Param.Anchored_Delta),
+                        Accumulate  => False);
+
+                  end case;
+               end;
+            end if;
+
+         when Language =>
+            return Param.Param.Function_Ptr
+              (Data, Tree, Tokens, Tree_Indenting, Indenting_Comment, 
Param.Param.Args);
+         end case;
+
+      when Hanging_Label =>
+         case Hanging_Label'(Param.Label) is
+         when Hanging_0 => -- wisi-hanging
+            return Indent_Hanging_1
+              (Data, Tree, Tokens, Tree_Indenting, Indenting_Comment, 
Param.Hanging_Delta_1,
+               Param.Hanging_Delta_2,
+               Option => False, Accumulate => True);
+         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 => True, Accumulate => True);
+         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 => False);
+         end case;
+      end case;
+   end Indent_Compute_Delta;
+
+   procedure Indent_Token_1
+     (Data              : in out Parse_Data_Type;
+      Indenting_Token   : in     Augmented_Token'Class;
+      Delta_Indent      : in     Delta_Type;
+      Indenting_Comment : in     Boolean)
+   is
+      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
+      if Trace_Action > Detail then
+         Ada.Text_IO.Put_Line
+           ("indent_token_1: " & Indenting_Token.Image (Data.Descriptor.all) & 
" " & Image (Delta_Indent) &
+              Line_Number_Type'Image (First_Line) & " .." & 
Line_Number_Type'Image (Last_Line) &
+              (if Indenting_Comment then " comment" else ""));
+      end if;
+
+      for Line in First_Line .. Last_Line loop
+         if Data.Indent_Comment_Col_0 then
+            declare
+               use all type Ada.Text_IO.Count;
+               Indent : Boolean := True;
+            begin
+               if Data.Line_Begin_Token.all (Line) /= 
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.Col = 0
+                     then
+                        Indent := False;
+                        exit;
+                     end if;
+                  end loop;
+               end if;
+
+               if Indent then
+                  Indent_Line (Data, Line, Delta_Indent);
+               end if;
+            end;
+         else
+            Indent_Line (Data, Line, Delta_Indent);
+         end if;
+      end loop;
+   end Indent_Token_1;
+
+   function Last_Line
+     (Token             : in Augmented_Token;
+      Indenting_Comment : in Boolean)
+     return Line_Number_Type
+   is begin
+      return
+        (if Indenting_Comment then
+           (if Token.Last_Trailing_Comment_Line = Invalid_Line_Number
+            then Token.Line
+            else Token.Last_Trailing_Comment_Line)
+         else
+           (if Token.Last_Indent_Line = Invalid_Line_Number
+            then Token.Line
+            else Token.Last_Indent_Line));
+   end Last_Line;
+
+end Wisi;
diff --git a/wisi.ads b/wisi.ads
new file mode 100644
index 0000000..cfc61e1
--- /dev/null
+++ b/wisi.ads
@@ -0,0 +1,666 @@
+--  Abstract :
+--
+--  Ada implementation of wisi parser actions.
+--
+--  References
+--
+--  [1] wisi.el - defines parse action functions.
+--
+--  [2] wisi-elisp-parse.el - defines parse action functions.
+--
+--  [3] wisi-process-parse.el - defines elisp/process API
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Containers.Doubly_Linked_Lists;
+with Ada.Containers.Vectors;
+with Ada.Strings.Unbounded;
+with Ada.Text_IO;
+with Ada.Unchecked_Deallocation;
+with SAL.Gen_Unbounded_Definite_Red_Black_Trees;
+with SAL.Gen_Unbounded_Definite_Vectors;
+with WisiToken.Parse.LR;
+with WisiToken.Lexer;
+with WisiToken.Syntax_Trees;
+package Wisi is
+   use all type WisiToken.Base_Buffer_Pos;
+
+   type Post_Parse_Action_Type is (Navigate, Face, Indent);
+
+   type Parse_Data_Type
+     (Line_Begin_Token : not null access constant 
WisiToken.Line_Begin_Token_Vectors.Vector)
+     is new WisiToken.Syntax_Trees.User_Data_Type with private;
+
+   procedure Initialize
+     (Data              : in out Parse_Data_Type;
+      Descriptor        : access constant WisiToken.Descriptor;
+      Source_File_Name  : in     String;
+      Post_Parse_Action : in     Post_Parse_Action_Type;
+      Line_Count        : in     WisiToken.Line_Number_Type;
+      Params            : in     String);
+   --  Line_Count only used for Indent. Params contains language-specific
+   --  indent parameter values.
+   --
+   --  It is possible to do without the Line_Count parameter, and grow
+   --  the various vectors dynamically. However, doing that caused
+   --  intermittent problems with too many lines; the Ada code saw more
+   --  lines than the elisp code did. Using the elisp line count is more
+   --  reliable.
+
+   overriding procedure Reset (Data : in out Parse_Data_Type);
+   --  Reset for a new parse, with data from previous Initialize.
+
+   function Source_File_Name (Data : in Parse_Data_Type) return String;
+   function Post_Parse_Action (Data : in Parse_Data_Type) return 
Post_Parse_Action_Type;
+
+   overriding
+   procedure Lexer_To_Augmented
+     (Data  : in out          Parse_Data_Type;
+      Token : in              WisiToken.Base_Token;
+      Lexer : not null access WisiToken.Lexer.Instance'Class);
+
+   overriding
+   procedure Delete_Token
+     (Data        : in out Parse_Data_Type;
+      Token_Index : in     WisiToken.Token_Index);
+
+   overriding
+   procedure Reduce
+     (Data    : in out Parse_Data_Type;
+      Tree    : in out WisiToken.Syntax_Trees.Tree'Class;
+      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);
+   --  Matches [1] wisi-class-list.
+
+   type Index_Navigate_Class is record
+      Index : WisiToken.Positive_Index_Type; -- into Tokens
+      Class : Navigate_Class_Type;
+   end record;
+
+   type Statement_Param_Array is array (Natural range <>) of 
Index_Navigate_Class;
+
+   procedure Statement_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;
+      Params  : in     Statement_Param_Array);
+
+   procedure Containing_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;
+      Containing : in     WisiToken.Positive_Index_Type;
+      Contained  : in     WisiToken.Positive_Index_Type);
+
+   package Token_ID_Lists is new Ada.Containers.Doubly_Linked_Lists 
(WisiToken.Token_ID, WisiToken."=");
+
+   Empty_IDs : constant Token_ID_Lists.List := Token_ID_Lists.Empty_List;
+
+   function "+" (Item : in WisiToken.Token_ID) return Token_ID_Lists.List;
+   function "&" (List : in Token_ID_Lists.List; Item : in WisiToken.Token_ID) 
return Token_ID_Lists.List;
+   function "&" (Left, Right : in WisiToken.Token_ID) return 
Token_ID_Lists.List;
+
+   type Index_IDs is record
+      Index : WisiToken.Positive_Index_Type; -- into Tokens
+      IDs   : Token_ID_Lists.List;
+   end record;
+
+   package Index_IDs_Vectors is new Ada.Containers.Vectors 
(Ada.Containers.Count_Type, Index_IDs);
+
+   subtype Motion_Param_Array is Index_IDs_Vectors.Vector;
+
+   procedure Motion_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;
+      Params  : in     Motion_Param_Array);
+   --  Implements [2] wisi-motion-action.
+
+   type Index_Faces is record
+      Index       : WisiToken.Positive_Index_Type; -- into Tokens
+      Prefix_Face : Integer; -- into grammar.Face_List
+      Suffix_Face : Integer; -- into grammar.Face_List
+   end record;
+
+   type Face_Apply_Param_Array is array (Natural range <>) of Index_Faces;
+
+   procedure Face_Apply_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;
+      Params  : in     Face_Apply_Param_Array);
+   --  Implements [2] wisi-face-apply-action.
+
+   procedure Face_Apply_List_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;
+      Params  : in     Face_Apply_Param_Array);
+   --  Implements [2] wisi-face-apply-list-action.
+
+   type Face_Class_Type is (Prefix, Suffix);
+   --  Matches wisi-cache-class values set in [1] wisi-face-apply-action.
+
+   type Index_Face_Class is record
+      Index : WisiToken.Positive_Index_Type; -- into Tokens
+      Class : Face_Class_Type;
+   end record;
+
+   type Face_Mark_Param_Array is array (Natural range <>) of Index_Face_Class;
+
+   procedure Face_Mark_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;
+      Params  : in     Face_Mark_Param_Array);
+   --  Implements [2] wisi-face-mark-action.
+
+   type Face_Remove_Param_Array is array (Natural range <>) of 
WisiToken.Positive_Index_Type;
+
+   procedure Face_Remove_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;
+      Params  : in     Face_Remove_Param_Array);
+   --  Implements [2] wisi-face-remove-action.
+
+   ----------
+   --  Indent
+   --
+   --  elisp indent functions are represented by the Indent_Param type,
+   --  not Ada functions. This is to get the execution time right; in
+   --  elisp, the array of parameters to wisi-indent-action is not
+   --  evaluated when wisi-indent-action is called; each parameter is
+   --  evaluated by wisi-elisp-parse--indent-compute-delta.
+
+   type Simple_Indent_Param_Label is -- not hanging
+     (Int,
+      Anchored_0, -- wisi-anchored
+      Anchored_1, -- wisi-anchored%
+      Anchored_2, -- wisi-anchored%-
+      Anchored_3, -- wisi-anchored*
+      Anchored_4, -- wisi-anchored*-
+      Language    -- language-specific function
+     );
+   subtype Anchored_Label is Simple_Indent_Param_Label range Anchored_0 .. 
Anchored_4;
+
+   --  Arguments to language-specific functions are integers; one of
+   --  delta, Token_Number, or Token_ID - the syntax does not distinguish
+   --  among these three types.
+
+   package Indent_Arg_Arrays is new Ada.Containers.Vectors 
(WisiToken.Positive_Index_Type, Integer);
+
+   function "+" (Item : in Integer) return Indent_Arg_Arrays.Vector;
+   function "&" (List : in Indent_Arg_Arrays.Vector; Item : in Integer) return 
Indent_Arg_Arrays.Vector;
+   function "&" (Left, Right : in Integer) return Indent_Arg_Arrays.Vector;
+
+   type Delta_Type (<>) is private;
+
+   type Language_Indent_Function is access function
+     (Data              : in out Parse_Data_Type'Class;
+      Tree              : in     WisiToken.Syntax_Trees.Tree;
+      Tree_Tokens       : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
+      Tree_Indenting    : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+      Indenting_Comment : in     Boolean;
+      Args              : in     Indent_Arg_Arrays.Vector)
+     return Delta_Type;
+
+   Null_Args : Indent_Arg_Arrays.Vector renames Indent_Arg_Arrays.Empty_Vector;
+
+   type Simple_Indent_Param (Label : Simple_Indent_Param_Label := Int) is
+   record
+      case Label is
+      when Int =>
+         Int_Delta : Integer;
+
+      when Anchored_Label =>
+         Anchored_Index : WisiToken.Positive_Index_Type;
+         Anchored_Delta : Integer;
+
+      when Language =>
+         Function_Ptr : Language_Indent_Function;
+         Args         : Indent_Arg_Arrays.Vector;
+      end case;
+   end record;
+
+   function Image (Item : in Simple_Indent_Param) return String;
+
+   type Indent_Param_Label is
+     (Simple,
+      Hanging_0, -- wisi-hanging
+      Hanging_1, -- wisi-hanging%
+      Hanging_2  -- wisi-hanging%-
+     );
+   subtype Hanging_Label is Indent_Param_Label range Hanging_0 .. Hanging_2;
+
+   type Indent_Param (Label : Indent_Param_Label := Simple) is
+   record
+      case Label is
+      when Simple =>
+         Param : Simple_Indent_Param;
+
+      when Hanging_Label =>
+         Hanging_Delta_1 : Simple_Indent_Param;
+         Hanging_Delta_2 : Simple_Indent_Param;
+
+      end case;
+   end record;
+
+   function Image (Item : in Indent_Param) return String;
+
+   type Indent_Pair (Comment_Present : Boolean := False) is
+   record
+      Code_Delta : Indent_Param;
+      case Comment_Present is
+      when True =>
+         Comment_Delta : Indent_Param;
+      when False =>
+         null;
+      end case;
+   end record;
+
+   function Image (Item : in Indent_Pair) return String;
+
+   type Indent_Param_Array is array (WisiToken.Positive_Index_Type range <>) 
of Indent_Pair;
+
+   procedure Indent_Action_0
+     (Data    : in out Parse_Data_Type'Class;
+      Tree    : in     WisiToken.Syntax_Trees.Tree;
+      Nonterm : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
+      Params  : in     Indent_Param_Array);
+   --  Implements [2] wisi-indent-action.
+
+   procedure Indent_Action_1
+     (Data    : in out Parse_Data_Type'Class;
+      Tree    : in     WisiToken.Syntax_Trees.Tree;
+      Nonterm : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+      Tokens  : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
+      N       : in     WisiToken.Positive_Index_Type;
+      Params  : in     Indent_Param_Array);
+   --  Implements [2] wisi-indent-action*.
+
+   function Indent_Hanging_1
+     (Data              : in out Parse_Data_Type;
+      Tree              : in     WisiToken.Syntax_Trees.Tree;
+      Tokens            : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
+      Tree_Indenting    : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+      Indenting_Comment : in     Boolean;
+      Delta_1           : in     Simple_Indent_Param;
+      Delta_2           : in     Simple_Indent_Param;
+      Option            : in     Boolean;
+      Accumulate        : in     Boolean)
+     return Delta_Type;
+   --  [2] wisi-elisp-parse--hanging-1
+   --
+   --  Language specific child packages override this to implement
+   --  wisi-elisp-parse-indent-hanging-function.
+
+   procedure Put (Data : in out Parse_Data_Type);
+   --  Perform post-parse actions, then put result to
+   --  Ada.Text_IO.Current_Output, as encoded responses as defined in [3]
+   --  wisi-process-parse--execute.
+
+   procedure Put
+     (Data         : in Parse_Data_Type;
+      Lexer_Errors : in WisiToken.Lexer.Error_Lists.List;
+      Parse_Errors : in WisiToken.Parse.LR.Parse_Error_Lists.List;
+      Tree         : in WisiToken.Syntax_Trees.Tree);
+   --  Put Lexer_Errors and Parse_Errors to Ada.Text_IO.Current_Output,
+   --  as encoded error responses as defined in [3]
+   --  wisi-process-parse--execute.
+
+   procedure Put_Error (Data : in Parse_Data_Type; Line_Number : in 
WisiToken.Line_Number_Type; Message : in String);
+   --  Put an error elisp form to Ada.Text_IO.Standard_Output.
+
+private
+
+   type Non_Grammar_Token is record
+      ID    : WisiToken.Token_ID         := WisiToken.Invalid_Token_ID;
+      Line  : WisiToken.Line_Number_Type := WisiToken.Invalid_Line_Number;
+      Col   : Ada.Text_IO.Count          := Ada.Text_IO.Count'Last;
+      First : Boolean                    := False;
+      --  Column is needed to detect comments in column 0.
+   end record;
+
+   package Non_Grammar_Token_Arrays is new SAL.Gen_Unbounded_Definite_Vectors
+     (WisiToken.Token_Index, Non_Grammar_Token);
+
+   type Augmented_Token is new WisiToken.Base_Token with record
+      --  Most fields are set by Lexer_To_Augmented at parse time; others
+      --  are set by Reduce for nonterminals.
+
+      Deleted : Boolean := False;
+      --  Set True by Parse_Data_Type.Delete_Token; Non_Grammar tokens are
+      --  moved to the previous non-deleted token.
+
+      --  The following fields are only needed for indent.
+
+      First : Boolean := False;
+      --  For a terminal, True if the token is not empty and it is first on
+      --  a line, or if it contains trailing blank or comment lines.
+      --
+      --  For a nonterminal, True if some contained token's First is True,
+      --  including trailing comments and blank lines.
+
+      Paren_State : Integer := 0;
+      --  Parenthesis nesting count, before token.
+
+      First_Terminals_Index : WisiToken.Base_Token_Index := 
WisiToken.Base_Token_Arrays.No_Index;
+      --  For virtual tokens, No_Index.
+      --
+      --  For terminal tokens, index of this token in Parser.Terminals.
+      --
+      --  For nonterminal tokens, index of first contained token in
+      --  Parser.Terminals.
+
+      Last_Terminals_Index : WisiToken.Base_Token_Index := 
WisiToken.Base_Token_Arrays.No_Index;
+      --  For non-virtual nonterminal tokens, index of last contained
+      --  token in Parser.Terminals.
+      --
+      --  For all others, same as First_Terminals_Index.
+
+      First_Indent_Line : WisiToken.Line_Number_Type := 
WisiToken.Invalid_Line_Number;
+      Last_Indent_Line  : WisiToken.Line_Number_Type := 
WisiToken.Invalid_Line_Number;
+      --  Lines that need indenting; first token on these lines is contained
+      --  in this token. If First is False, these are Invalid_Line_Number.
+      --
+      --  First_, Last_Indent_Line include blank and comment lines between
+      --  grammar tokens, but exclude trailing blanks and comments after the
+      --  last token, so they can be indented differently.
+
+      First_Trailing_Comment_Line : WisiToken.Line_Number_Type := 
WisiToken.Invalid_Line_Number;
+      Last_Trailing_Comment_Line  : WisiToken.Line_Number_Type := 
WisiToken.Invalid_Line_Number;
+      --  Trailing comment or blank lines (after the last contained grammar
+      --  token) that need indenting. Excludes comments following code on a
+      --  line. If there are no such lines, these are Invalid_Line_Number.
+
+      Non_Grammar : Non_Grammar_Token_Arrays.Vector;
+      --  For terminals, non-grammar tokens immediately following. For
+      --  nonterminals, empty.
+
+   end record;
+
+   overriding
+   function Image
+     (Item       : in Augmented_Token;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+   --  Return a string for debug/test messages
+
+   function First_Line
+     (Token             : in Augmented_Token;
+      Indenting_Comment : in Boolean)
+     return WisiToken.Line_Number_Type;
+   function Last_Line
+     (Token             : in Augmented_Token;
+      Indenting_Comment : in Boolean)
+     return WisiToken.Line_Number_Type;
+   --  Return first and last line in Token's region.
+
+   type Augmented_Token_Access is access all Augmented_Token;
+   procedure Free is new Ada.Unchecked_Deallocation (Augmented_Token, 
Augmented_Token_Access);
+
+   type Augmented_Token_Access_Array is array (WisiToken.Positive_Index_Type 
range <>) of Augmented_Token_Access;
+   --  1 indexed to match token numbers in grammar actions.
+
+   function Image
+     (Item       : in Augmented_Token_Access_Array;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+
+   package Augmented_Token_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(WisiToken.Token_Index, Augmented_Token);
+   --  Index matches Base_Token_Arrays.
+
+   package Line_Paren_Vectors is new SAL.Gen_Unbounded_Definite_Vectors 
(WisiToken.Line_Number_Type, Integer);
+   package Line_Begin_Pos_Vectors is new SAL.Gen_Unbounded_Definite_Vectors
+     (WisiToken.Line_Number_Type, WisiToken.Buffer_Pos);
+
+   type Nil_Buffer_Pos (Set : Boolean := False) is record
+      case Set is
+      when True =>
+         Item : WisiToken.Buffer_Pos;
+      when False =>
+         null;
+      end case;
+   end record;
+
+   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
+   end record;
+
+   function Key (Cache : in Navigate_Cache_Type) return WisiToken.Buffer_Pos 
is (Cache.Pos);
+
+   function Key_Compare (Left, Right : in WisiToken.Buffer_Pos) return 
SAL.Compare_Result is
+     (if Left > Right then SAL.Greater
+      elsif Left = Right then SAL.Equal
+      else SAL.Less);
+
+   package Navigate_Cache_Trees is new 
SAL.Gen_Unbounded_Definite_Red_Black_Trees
+     (Navigate_Cache_Type, WisiToken.Buffer_Pos);
+
+   type Nil_Integer (Set : Boolean := False) is record
+      case Set is
+      when True =>
+         Item : Integer;
+      when False =>
+         null;
+      end case;
+   end record;
+
+   type Face_Cache_Type is record
+      Region : WisiToken.Buffer_Region;
+      Class  : Face_Class_Type; -- wisi-cache-class; one of {'prefix | 'suffix}
+      Face   : Nil_Integer;     -- not set, or index into *-process-faces-names
+   end record;
+
+   function Key (Cache : in Face_Cache_Type) return WisiToken.Buffer_Pos is 
(Cache.Region.First);
+
+   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);
+
+   package Anchor_ID_Vectors is new Ada.Containers.Vectors (Natural, Positive);
+
+   type Indent_Type (Label : Indent_Label := Not_Set) is record
+      --  [2] wisi-elisp-parse--indent elements. Indent values may be
+      --  negative while indents are being computed.
+      case Label is
+      when Not_Set =>
+         null;
+
+      when Int =>
+         Int_Indent : Integer;
+
+      when Anchor =>
+         Anchor_IDs    : Anchor_ID_Vectors.Vector; --  Largest ID first.
+         Anchor_Indent : Integer;
+
+      when Anchored =>
+         Anchored_ID    : Positive;
+         Anchored_Delta : Integer; -- added to Anchor_Indent of Anchor_ID
+
+      when Anchor_Anchored =>
+         Anchor_Anchored_IDs   : Anchor_ID_Vectors.Vector;
+         Anchor_Anchored_ID    : Natural;
+         Anchor_Anchored_Delta : Integer;
+      end case;
+   end record;
+   First_Anchor_ID : constant Positive := Positive'First;
+
+   package Indent_Vectors is new Ada.Containers.Vectors 
(WisiToken.Line_Number_Type, Indent_Type);
+   package Navigate_Cursor_Lists is new Ada.Containers.Doubly_Linked_Lists
+     (Navigate_Cache_Trees.Cursor, Navigate_Cache_Trees."=");
+
+   type Parse_Data_Type
+     (Line_Begin_Token : not null access constant 
WisiToken.Line_Begin_Token_Vectors.Vector)
+     is new WisiToken.Syntax_Trees.User_Data_Type with
+   record
+      --  Data from parsing
+
+      Terminals : Augmented_Token_Arrays.Vector;
+      --  All terminal grammar tokens, in lexical order. Each contains any
+      --  immediately following non-grammar tokens. Does not contain
+      --  nonterminal or virtual tokens.
+
+      Leading_Non_Grammar : WisiToken.Base_Token_Arrays.Vector;
+      --  non-grammar tokens before first grammar token.
+
+      Line_Begin_Pos : Line_Begin_Pos_Vectors.Vector;
+      --  Character position at the start of the first token on each line.
+
+      Line_Paren_State : Line_Paren_Vectors.Vector;
+      --  Parenthesis nesting state at the start of each line; used by
+      --  Indent. Set by Lexer_To_Augmented on New_Line_ID.
+
+      Current_Paren_State : Integer;
+      --  Current parenthesis nesting state; used by Indent. Set by
+      --  Lexer_To_Augmented on Left_Paren_ID, Right_Paren_ID.
+
+      --  Data for post-parse actions
+
+      Descriptor        : access constant WisiToken.Descriptor;
+      Source_File_Name  : Ada.Strings.Unbounded.Unbounded_String;
+      Post_Parse_Action : Post_Parse_Action_Type;
+      Navigate_Caches   : Navigate_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.
+
+      --  Copied from language-specific parameters
+      Indent_Comment_Col_0 : Boolean := False;
+
+      --  Dynamic data for Indent
+      Max_Anchor_ID : Integer;
+   end record;
+
+   type Simple_Delta_Labels is (Int, Anchored);
+
+   type Simple_Delta_Type (Label : Simple_Delta_Labels := Int) is
+   record
+      case Label is
+      when Int =>
+         Int_Delta : Integer;
+
+      when Anchored =>
+         Anchored_ID         : Natural;
+         Anchored_Delta      : Integer;
+         Anchored_Accumulate : Boolean;
+
+      end case;
+   end record;
+   subtype Anchored_Delta is Simple_Delta_Type (Anchored);
+
+   function Image (Item : in Simple_Delta_Type) return String;
+   --  For debugging
+
+   type Delta_Labels is (Simple, Hanging);
+
+   type Delta_Type (Label : Delta_Labels := Simple) is
+   record
+      --  Matches DELTA input to wisi--indent-token-1
+      case Label is
+      when Simple =>
+         Simple_Delta : Simple_Delta_Type;
+
+      when Hanging =>
+         Hanging_First_Line  : WisiToken.Line_Number_Type;
+         Hanging_Paren_State : Integer;
+         Hanging_Delta_1     : Simple_Delta_Type; -- indentation of first line
+         Hanging_Delta_2     : Simple_Delta_Type; -- indentation of 
continuation lines
+         Hanging_Accumulate  : Boolean;
+      end case;
+   end record;
+
+   Null_Delta : constant Delta_Type := (Simple, (Int, 0));
+
+   function Image (Item : in Delta_Type) return String;
+   --  For debugging
+
+   ----------
+   --  Utilities for language-specific child packages
+
+   subtype Aug_Token_Ref is Augmented_Token_Arrays.Variable_Reference_Type;
+
+   function Current_Indent_Offset
+     (Data         : in Parse_Data_Type;
+      Anchor_Token : in Augmented_Token'Class;
+      Offset       : in Integer)
+     return Integer;
+   --  Return offset from beginning of first token on line containing
+   --  Anchor_Token, to beginning of Anchor_Token, plus Offset.
+
+   function Find
+     (Data  : in Parse_Data_Type;
+      ID    : in WisiToken.Token_ID;
+      Token : in Augmented_Token'Class)
+     return WisiToken.Base_Token_Index;
+   --  Return index in Parser.Terminals of first token in
+   --  Token.Char_Region with ID. If not found, return
+   --  No_Index.
+
+   function Get_Aug_Token
+     (Data       : in Parse_Data_Type'Class;
+      Tree       : in WisiToken.Syntax_Trees.Tree'Class;
+      Tree_Index : in WisiToken.Syntax_Trees.Valid_Node_Index)
+     return Aug_Token_Ref;
+
+   function Indent_Anchored_2
+     (Data        : in out Parse_Data_Type;
+      Anchor_Line : in     WisiToken.Line_Number_Type;
+      Last_Line   : in     WisiToken.Line_Number_Type;
+      Offset      : in     Integer;
+      Accumulate  : in     Boolean)
+     return Delta_Type;
+   --  [2] wisi-elisp-parse--anchored-2
+
+   function Indent_Compute_Delta
+     (Data              : in out Parse_Data_Type'Class;
+      Tree              : in     WisiToken.Syntax_Trees.Tree;
+      Tokens            : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array;
+      Param             : in     Indent_Param;
+      Tree_Indenting    : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+      Indenting_Comment : in     Boolean)
+     return Delta_Type;
+
+   procedure Indent_Token_1
+     (Data              : in out Parse_Data_Type;
+      Indenting_Token   : in     Augmented_Token'Class;
+      Delta_Indent      : in     Delta_Type;
+      Indenting_Comment : in     Boolean);
+   --  [2] wisi-elisp-parse--indent-token-1. Sets Data.Indents, so caller
+   --  may not be in a renames for a Data.Indents element.
+
+end Wisi;
diff --git a/wisi.el b/wisi.el
index b063321..00719a2 100755
--- a/wisi.el
+++ b/wisi.el
@@ -1,1642 +1,1298 @@
-;;; wisi.el --- Utilities for implementing an indentation/navigation engine 
using a generalized LALR parser -*- lexical-binding:t -*-
-;;
-;; Copyright (C) 2012 - 2017  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: 1.1.6
-;; package-requires: ((cl-lib "0.4") (emacs "24.3"))
-;; URL: http://www.nongnu.org/ada-mode/wisi/wisi.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
-;;
-;;;; indentation algorithm overview
-;;
-;; This design is inspired in part by experience writing a SMIE
-;; indentation engine for Ada, and the wisent parser.
-;;
-;; The general approach to indenting a given token is to find the
-;; start of the statement it is part of, or some other relevant point
-;; in the statement, and indent relative to that.  So we need a parser
-;; that lets us find statement indent points from arbitrary places in
-;; the code.
-;;
-;; For example, the grammar for Ada as represented by the EBNF in LRM
-;; Annex P is not LALR(1), so we use a generalized LALR(1) parser (see
-;; wisi-parse, wisi-compile).
-;;
-;; The parser actions cache indentation and other information as text
-;; properties of tokens in statements.
-;;
-;; An indentation engine moves text in the buffer, as does user
-;; editing, so we can't rely on character positions remaining
-;; constant.  So the parser actions use markers to store
-;; positions.  Text properties also move with the text.
-;;
-;; The stored information includes a marker at each statement indent
-;; point.  Thus, the indentation algorithm is: find the previous token
-;; with cached information, and either indent from it, or fetch from
-;; it the marker for a previous statement indent point, and indent
-;; relative to that.
-;;
-;; 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 now we invalidate the cache after
-;; the edit point if the change involves anything other than
-;; whitespace.
-;;
-;;; Handling parse errors:
-;;
-;; When a parse fails, the cache information before the failure point
-;; is only partly correct, and there is no cache informaiton after the
-;; failure point.
-;;
-;; However, in the case where a parse previously succeeded, and the
-;; current parse fails due to editing, we keep the preceding cache
-;; information by setting wisi-cache-max to the edit point in
-;; wisi-before change; the parser does not apply actions before that
-;; point.
-;;
-;; This allows navigation and indentation in the text preceding the
-;; edit point, and saves some time.
-;;
-;;;; comparison to the SMIE parser
-;;
-;; The central problem to be solved in building the SMIE parser is
-;; grammar precedence conflicts; the general solution is refining
-;; keywords so that each new keyword can be assigned a unique
-;; precedence.  This means ad hoc code must be written to determine the
-;; correct refinement for each language keyword from the surrounding
-;; tokens.  In effect, for a complex language like Ada, the knowledge
-;; of the language grammar is mostly embedded in the refinement code;
-;; only a small amount is in the refined grammar.  Implementing a SMIE
-;; parser for a new language involves the same amount of work as the
-;; first language.
-;;
-;; Using a generalized LALR parser avoids that particular problem;
-;; assuming the language is already defined by a grammar, it is only a
-;; matter of a format change to teach the wisi parser the
-;; language.  The problem in a wisi indentation engine is caching the
-;; output of the parser in a useful way, since we can't start the
-;; parser from arbitrary places in the code (as we can with the SMIE
-;; parser). A second problem is determining when to invalidate the
-;; cache.  But these problems are independent of the language being
-;; parsed, so once we have one wisi indentation engine working,
-;; adapting it to new languages should be quite simple.
-;;
-;; The SMIE parser does not find the start of each statement, only the
-;; first language keyword in each statement; additional code must be
-;; written to find the statement start and indent points.  The wisi
-;; parser finds the statement start and indent points directly.
-;;
-;; In SMIE, it is best if each grammar rule is a complete statement,
-;; so forward-sexp will traverse the entire statement.  If nested
-;; non-terminals are used, forward-sexp may stop inside one of the
-;; nested non-terminals.  This problem does not occur with the wisi
-;; parser.
-;;
-;; A downside of the wisi parser is conflicts in the grammar; they can
-;; be much more difficult to resolve than in the SMIE parser.  The
-;; generalized parser helps by handling conflicts, but it does so by
-;; running multiple parsers in parallel, persuing each choice in the
-;; conflict.  If the conflict is due to a genuine ambiguity, both paths
-;; will succeed, which causes the parse to fail, since it is not clear
-;; which set of text properties to store.  Even if one branch
-;; ultimately fails, running parallel parsers over large sections of
-;; code is slow.  Finally, this approach can lead to exponential growth
-;; in the number of parsers.  So grammar conflicts must still be
-;; analyzed and minimized.
-;;
-;; In addition, the complete grammar must be specified; in smie, it is
-;; often possible to specify a subset of the grammar.
-;;
-;;;; grammar compiler and parser
-;;
-;; Since we are using a generalized LALR(1) parser, we cannot use any
-;; of the wisent grammar functions.  We use OpenToken wisi-generate
-;; to compile BNF to Elisp source (similar to
-;; semantic-grammar-create-package), and wisi-compile-grammar to
-;; compile that to the parser table.
-;;
-;; Semantic provides a complex lexer, more complicated than we need
-;; for indentation.  So we use the elisp lexer, which consists of
-;; `forward-comment', `skip-syntax-forward', and `scan-sexp'.  We wrap
-;; that in functions that return tokens in the form wisi-parse
-;; expects.
-;;
-;;;; lexer
-;;
-;; The lexer is `wisi-forward-token'. It relies on syntax properties,
-;; so syntax-propertize must be called on the text to be lexed before
-;; wisi-forward-token is called. In general, it is hard to determine
-;; an appropriate end-point for syntax-propertize, other than
-;; point-max. So we call (syntax-propertize point-max) in wisi-setup,
-;; and also call syntax-propertize in wisi-after-change.
-;; FIXME: no longer needed in Emacs 25? (email from Stefan Monnier)
-;;
-;;;; code style
-;;
-;; 'wisi' was originally short for "wisent indentation engine", but
-;; now is just a name.
-;;
-;; not using lexical-binding because we support Emacs 23
-;;
-;;;;;
-
-;;; Code:
-
-(require 'cl-lib)
-(require 'wisi-parse)
-
-;; WORKAROUND: for some reason, this condition doesn't work in batch mode!
-;; (when (and (= emacs-major-version 24)
-;;        (= emacs-minor-version 2))
-  (require 'wisi-compat-24.2)
-;;)
-
-(defcustom wisi-size-threshold 100000
-  "Max size (in characters) for using wisi parser results for syntax 
highlighting and file navigation."
-  :type 'integer
-  :group 'wisi
-  :safe 'integerp)
-(make-variable-buffer-local 'wisi-size-threshold)
-
-;;;; lexer
-
-(defvar-local wisi-class-list nil)
-(defvar-local wisi-keyword-table nil)
-(defvar-local wisi-punctuation-table nil)
-(defvar-local wisi-punctuation-table-max-length 0)
-(defvar-local wisi-string-double-term nil);; string delimited by double quotes
-(defvar-local wisi-string-quote-escape-doubled nil
-  "Non-nil if a string delimiter is escaped by doubling it (as in Ada).")
-(defvar-local wisi-string-quote-escape nil
-  "Cons (delim . character) where `character' escapes quotes in strings 
delimited by `delim'.")
-(defvar-local wisi-string-single-term nil) ;; string delimited by single quotes
-(defvar-local wisi-symbol-term nil)
-(defvar-local wisi-number-term nil)
-(defvar-local wisi-number-p nil)
-
-(defun wisi-number-p (token-text)
-  "Return t if TOKEN-TEXT plus text after point matches the
-syntax for a real literal; otherwise nil.  Point is after
-TOKEN-TEXT; move point to just past token."
-  ;; Typical literals:
-  ;; 1234
-  ;; 1234.5678
-  ;; _not_ including non-decimal base, or underscores (see ada-wisi-number-p)
-  ;;
-  ;; Starts with a simple integer
-  (when (string-match "^[0-9]+$" token-text)
-    (when (looking-at "\\.[0-9]+")
-      ;; real number
-      (goto-char (match-end 0))
-      (when (looking-at  "[Ee][+-][0-9]+")
-        ;; exponent
-        (goto-char (match-end 0))))
-
-    t
-    ))
-
-(defun wisi-forward-token ()
-  "Move point forward across one token, skipping leading whitespace and 
comments.
-Return the corresponding token, in format: (token start . end) where:
-
-`token' is a token symbol (not string) from `wisi-punctuation-table',
-`wisi-keyword-table', `wisi-string-double-term', `wisi-string-double-term' or 
`wisi-symbol-term'.
-
-`start, end' are the character positions in the buffer of the start
-and end of the token text.
-
-If at end of buffer, returns `wisent-eoi-term'."
-  (forward-comment (point-max))
-  ;; skips leading whitespace, comment, trailing whitespace.
-
-  (let ((start (point))
-       ;; (info "(elisp)Syntax Table Internals" "*info elisp syntax*")
-       (syntax (syntax-class (syntax-after (point))))
-       token-id token-text)
-    (cond
-     ((eobp)
-      (setq token-id wisent-eoi-term))
-
-     ((eq syntax 1)
-      ;; punctuation. Find the longest matching string in 
wisi-punctuation-table
-      (forward-char 1)
-      (let ((next-point (point))
-           temp-text temp-id done)
-       (while (not done)
-         (setq temp-text (buffer-substring-no-properties start (point)))
-         (setq temp-id (car (rassoc temp-text wisi-punctuation-table)))
-         (when temp-id
-           (setq token-id temp-id
-                 next-point (point)))
-         (if (or
-              (eobp)
-              (= (- (point) start) wisi-punctuation-table-max-length))
-             (setq done t)
-           (forward-char 1))
-         )
-       (goto-char next-point)))
-
-     ((memq syntax '(4 5)) ;; open, close parenthesis
-      (forward-char 1)
-      (setq token-text (buffer-substring-no-properties start (point)))
-      (setq token-id (symbol-value (intern-soft token-text 
wisi-keyword-table))))
-
-     ((eq syntax 7)
-      ;; string quote, either single or double. we assume point is
-      ;; before the start quote, not the end quote
-      (let ((delim (char-after (point)))
-           (forward-sexp-function nil))
-       (condition-case err
-           (progn
-             (forward-sexp)
-
-             ;; point is now after the end quote; check for an escaped quote
-             (while (or
-                     (and wisi-string-quote-escape-doubled
-                          (eq (char-after (point)) delim))
-                     (and (eq delim (car wisi-string-quote-escape))
-                          (eq (char-before (1- (point))) (cdr 
wisi-string-quote-escape))))
-               (forward-sexp))
-             (setq token-id (if (= delim ?\") wisi-string-double-term 
wisi-string-single-term)))
-         (scan-error
-          ;; Something screwed up; we should not get here if
-          ;; syntax-propertize works properly.
-          (signal 'wisi-parse-error (format "wisi-forward-token: forward-sexp 
failed %s" err))
-          ))))
-
-     (t ;; assuming word or symbol syntax; includes numbers
-      (skip-syntax-forward "w_'")
-      (setq token-text (buffer-substring-no-properties start (point)))
-      (setq token-id
-           (or (symbol-value (intern-soft (downcase token-text) 
wisi-keyword-table))
-               (and (functionp wisi-number-p)
-                    (funcall wisi-number-p token-text)
-                    (setq token-text (buffer-substring-no-properties start 
(point)))
-                    wisi-number-term)
-               wisi-symbol-term))
-      )
-     );; cond
-
-    (unless token-id
-      (signal 'wisi-parse-error
-             (wisi-error-msg "unrecognized token '%s'" 
(buffer-substring-no-properties start (point)))))
-
-    (cons token-id (cons start (point)))
-    ))
-
-(defun wisi-backward-token ()
-  "Move point backward across one token, skipping whitespace and comments.
-Does _not_ handle numbers with wisi-number-p; just sees lower-level syntax.
-Return (nil start . end) - same structure as
-wisi-forward-token, but does not look up symbol."
-  (forward-comment (- (point)))
-  ;; skips leading whitespace, comment, trailing whitespace.
-
-  ;; (info "(elisp)Syntax Table Internals" "*info elisp syntax*")
-  (let ((end (point))
-       (syntax (syntax-class (syntax-after (1- (point))))))
-    (cond
-     ((bobp) nil)
-
-     ((eq syntax 1)
-      ;; punctuation. Find the longest matching string in 
wisi-punctuation-table
-      (backward-char 1)
-      (let ((next-point (point))
-           temp-text done)
-       (while (not done)
-         (setq temp-text (buffer-substring-no-properties (point) end))
-         (when (car (rassoc temp-text wisi-punctuation-table))
-           (setq next-point (point)))
-         (if (or
-              (bobp)
-              (= (- end (point)) wisi-punctuation-table-max-length))
-             (setq done t)
-           (backward-char 1))
-         )
-       (goto-char next-point))
-      )
-
-     ((memq syntax '(4 5)) ;; open, close parenthesis
-      (backward-char 1))
-
-     ((eq syntax 7)
-      ;; a string quote. we assume we are after the end quote, not the start 
quote
-      (let ((forward-sexp-function nil))
-       (forward-sexp -1)))
-
-     (t ;; assuming word or symbol syntax
-      (if (zerop (skip-syntax-backward "."))
-         (skip-syntax-backward "w_'")))
-     )
-    (cons nil (cons (point) end))
-    ))
-
-;;;; token info cache
-;;
-;; the cache stores the results of parsing as text properties on
-;; keywords, for use by the indention and motion engines.
-
-(cl-defstruct
-  (wisi-cache
-   (:constructor wisi-cache-create)
-   (:copier nil))
-  nonterm;; nonterminal from parse (set by wisi-statement-action)
-
-  token
-  ;; terminal symbol from wisi-keyword-table or
-  ;; wisi-punctuation-table, or lower-level nonterminal from parse
-  ;; (set by wisi-statement-action)
-
-  last ;; pos of last char in token, relative to first (0 indexed)
-
-  class
-  ;; arbitrary lisp symbol, used for indentation and navigation.
-  ;; some classes are defined by wisi:
-  ;;
-  ;; 'block-middle - a block keyword (ie: if then else end), not at the start 
of a statement
-  ;;
-  ;; 'block-start - a block keyword at the start of a statement
-  ;;
-  ;; 'statement-start - the start of a statement
-  ;;
-  ;; 'open-paren
-  ;;
-  ;; others are language-specific
-
-  containing
-  ;; Marker at the containing keyword for this token.
-  ;; A containing keyword is an indent point; the start of a
-  ;; statement, or 'begin', 'then' or 'else' for a block of
-  ;; statements, etc.
-  ;; nil only for first token in buffer
-
-  prev ;; marker at previous motion token in statement; nil if none
-  next ;; marker at next motion token in statement; nil if none
-  end  ;; marker at token at end of current statement
-  )
-
-(defvar-local wisi-parse-table nil)
-
-(defvar-local wisi-parse-failed nil
-  "Non-nil when a recent parse has failed - cleared when parse succeeds.")
-
-(defvar-local wisi-parse-try nil
-  "Non-nil when parse is needed - cleared when parse succeeds.")
-
-(defvar-local wisi-change-need-invalidate nil
-  "When non-nil, buffer position to invalidate from.
-Used in before/after change functions.")
-
-(defvar-local wisi-end-caches nil
-  "List of buffer positions of caches in current statement that need 
wisi-cache-end set.")
-
-(defun wisi-delete-cache (after)
-  (with-silent-modifications
-    (remove-text-properties after (point-max) '(wisi-cache nil))
-    ;; We don't remove 'font-lock-face; that's annoying to the user,
-    ;; since they won't be restored until a parse for some other
-    ;; reason, and they are likely to be right anyway.
-    ))
-
-(defun wisi-invalidate-cache(&optional after)
-  "Invalidate parsing caches for the current buffer from AFTER to end of 
buffer."
-  (interactive)
-  (if (not after)
-      (setq after (point-min))
-    (setq after
-       (save-excursion
-         (goto-char after)
-         (line-beginning-position))))
-  (when (> wisi-debug 0) (message "wisi-invalidate %s:%d" (current-buffer) 
after))
-  (setq wisi-cache-max after)
-  (setq wisi-parse-try t)
-  (syntax-ppss-flush-cache after)
-  (wisi-delete-cache after)
-  )
-
-;; To see the effects of wisi-before-change, wisi-after-change, you need:
-;; (global-font-lock-mode 0)
-;; (setq jit-lock-functions nil)
-;;
-;; otherwise jit-lock runs and overrides them
-
-(defun wisi-before-change (begin end)
-  "For `before-change-functions'."
-  ;; begin . end is range of text being deleted
-
-  ;; If jit-lock-after-change is before wisi-after-change in
-  ;; after-change-functions, it might use any invalid caches in the
-  ;; inserted text.
-  ;;
-  ;; So we check for that here, and ensure it is after
-  ;; wisi-after-change, which deletes the invalid caches
-  (when (boundp 'jit-lock-mode)
-    (when (memq 'wisi-after-change (memq 'jit-lock-after-change 
after-change-functions))
-      (setq after-change-functions (delete 'wisi-after-change 
after-change-functions))
-      (add-hook 'after-change-functions 'wisi-after-change nil t))
-    )
-
-  (setq wisi-change-need-invalidate nil)
-
-  (when (> end begin)
-    (save-excursion
-      ;; (info "(elisp)Parser State")
-      (let* ((begin-state (syntax-ppss begin))
-            (end-state (syntax-ppss end))
-            ;; syntax-ppss has moved point to "end".
-            (word-end (progn (skip-syntax-forward "w_")(point))))
-
-       ;; Remove grammar face from word(s) containing change region;
-       ;; might be changing to/from a keyword. See
-       ;; test/ada_mode-interactive_common.adb Obj_1
-       (goto-char begin)
-       (skip-syntax-backward "w_")
-       (with-silent-modifications
-         (remove-text-properties (point) word-end '(font-lock-face nil 
fontified nil)))
-
-       (if (<= wisi-cache-max begin)
-           ;; Change is in unvalidated region; either the parse was
-           ;; failing, or there is more than one top-level grammar
-           ;; symbol in buffer.
-           (when wisi-parse-failed
-             ;; The parse was failing, probably due to bad syntax; this
-             ;; change may have fixed it, so try reparse.
-             (setq wisi-parse-try t))
-
-         ;; else change is in validated region
-         ;;
-         ;; don't invalidate parse for whitespace, string, or comment changes
-         (cond
-          ((and
-            (nth 3 begin-state); in string
-            (nth 3 end-state)))
-          ;; no easy way to tell if there is intervening non-string
-
-          ((and
-            (nth 4 begin-state); in comment
-            (nth 4 end-state))
-           ;; too hard to detect case where there is intervening
-           ;; code; no easy way to go to end of comment if not
-           ;; newline
-           )
-
-          ;; Deleting whitespace generally does not require parse, but
-          ;; deleting all whitespace between two words does; check that
-          ;; there is whitespace on at least one side of the deleted
-          ;; text.
-          ;;
-          ;; We are not in a comment (checked above), so treat
-          ;; comment end as whitespace in case it is newline, except
-          ;; deleting a comment end at begin means commenting the
-          ;; current line; requires parse.
-          ((and
-            (eq (car (syntax-after begin)) 0) ; whitespace
-            (memq (car (syntax-after (1- end))) '(0 12)) ; whitespace, comment 
end
-            (or
-             (memq (car (syntax-after (1- begin))) '(0 12))
-             (memq (car (syntax-after end)) '(0 12)))
-            (progn
-              (goto-char begin)
-              (skip-syntax-forward " >" end)
-              (eq (point) end))))
-
-          (t
-           (setq wisi-change-need-invalidate
-                 (progn
-                   (goto-char begin)
-                   ;; note that because of the checks above, this never
-                   ;; triggers a parse, so it's fast
-                   (wisi-goto-statement-start)
-                   (point))))
-          )))
-      ))
-  )
-
-(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.
-
-  ;; (syntax-ppss-flush-cache begin) is in before-change-functions
-
-  (syntax-propertize end) ;; see comments above on "lexer" re syntax-propertize
-
-  ;; Remove caches on inserted text, which could have caches from
-  ;; before the failed parse (or another buffer), and are in any case
-  ;; invalid. No point in removing 'fontified; that's handled by
-  ;; jit-lock.
-
-  (with-silent-modifications
-    (remove-text-properties begin end '(wisi-cache nil font-lock-face nil)))
-
-  ;; Also remove grammar face from word(s) containing change region;
-  ;; might be changing to/from a keyword. See
-  ;; test/ada_mode-interactive_common.adb Obj_1
-  (save-excursion
-    ;; (info "(elisp)Parser State")
-    (let ((need-invalidate wisi-change-need-invalidate)
-         begin-state end-state word-end)
-      (when (> end begin)
-       (setq begin-state (syntax-ppss begin))
-       (setq end-state (syntax-ppss end))
-       ;; syntax-ppss has moved point to "end".
-
-       ;; extend fontification over new text,
-       (skip-syntax-forward "w_")
-       (setq word-end (point))
-       (goto-char begin)
-       (skip-syntax-backward "w_")
-       (with-silent-modifications
-         (remove-text-properties (point) word-end '(font-lock-face nil 
fontified nil))))
-
-      (if (<= wisi-cache-max begin)
-         ;; Change is in unvalidated region
-         (when wisi-parse-failed
-           ;; The parse was failing, probably due to bad syntax; this
-           ;; change may have fixed it, so try reparse.
-           (setq wisi-parse-try t))
-
-       ;; Change is in validated region
-       (cond
-        (wisi-change-need-invalidate
-         ;; wisi-before change determined the removed text alters the
-         ;; parse
-         )
-
-        ((= end begin)
-         (setq need-invalidate nil))
-
-        ((and
-          (nth 3 begin-state); in string
-          (nth 3 end-state))
-         ;; no easy way to tell if there is intervening non-string
-         (setq need-invalidate nil))
-
-        ((and
-          (nth 4 begin-state)
-          (nth 4 end-state)); in comment
-         ;; no easy way to detect intervening code
-         (setq need-invalidate nil)
-         ;; no caches to remove
-         )
-
-        ;; Adding whitespace generally does not require parse, but in
-        ;; the middle of word it does; check that there was
-        ;; whitespace on at least one side of the inserted text.
-        ;;
-        ;; We are not in a comment (checked above), so treat
-        ;; comment end as whitespace in case it is newline
-        ((and
-          (or
-           (memq (car (syntax-after (1- begin))) '(0 12)); whitespace, comment 
end
-           (memq (car (syntax-after end)) '(0 12)))
-          (progn
-           (goto-char begin)
-           (skip-syntax-forward " >" end)
-           (eq (point) end)))
-         (setq need-invalidate nil))
-
-        (t
-         (setq need-invalidate
-               (progn
-                 (goto-char begin)
-                 ;; note that because of the checks above, this never
-                 ;; triggers a parse, so it's fast
-                 (wisi-goto-statement-start)
-                 (point))))
-        )
-
-       (if need-invalidate
-           (wisi-invalidate-cache need-invalidate)
-
-         ;; else move cache-max by the net change length.
-         (setq wisi-cache-max
-               (+ wisi-cache-max (- end begin length))) )
-       ))
-    ))
-
-(defun wisi-get-cache (pos)
-  "Return `wisi-cache' struct from the `wisi-cache' text property at POS.
-If accessing cache at a marker for a token as set by `wisi-cache-tokens', POS 
must be (1- mark)."
-  (get-text-property pos 'wisi-cache))
-
-(defvar-local wisi-parse-error-msg nil)
-
-(defun wisi-goto-error ()
-  "Move point to position in last error message (if any)."
-  (when (and wisi-parse-error-msg
-            (string-match ":\\([0-9]+\\):\\([0-9]+\\):" wisi-parse-error-msg))
-    (let ((line (string-to-number (match-string 1 wisi-parse-error-msg)))
-         (col (string-to-number (match-string 2 wisi-parse-error-msg))))
-      (push-mark)
-      (goto-char (point-min))
-      (forward-line (1- line))
-      (forward-char col))))
-
-(defun wisi-show-parse-error ()
-  "Show last wisi-parse error."
-  (interactive)
-  (cond
-   (wisi-parse-failed
-    (wisi-goto-error)
-    (message wisi-parse-error-msg))
-
-   (wisi-parse-try
-    (message "need parse"))
-
-   (t
-    (message "parse succeeded"))
-   ))
-
-(defvar wisi-post-parse-succeed-hook nil
-  "Hook run after parse succeeds.")
-
-(defun wisi-validate-cache (pos &optional error-on-fail)
-  "Ensure cached data is valid at least up to POS in current buffer."
-  (let ((msg (when (> wisi-debug 0) (format "wisi: parsing %s:%d ..." 
(buffer-name) (line-number-at-pos pos)))))
-    ;; If wisi-cache-max = pos, then there is no cache at pos; need parse
-    (when (and wisi-parse-try
-              (<= wisi-cache-max pos))
-      (when (> wisi-debug 0)
-       (message msg))
-
-      ;; Don't keep retrying failed parse until text changes again.
-      (setq wisi-parse-try nil)
-
-      (setq wisi-parse-error-msg nil)
-      (setq wisi-end-caches nil)
-
-      (if (> wisi-debug 1)
-         ;; let debugger stop in wisi-parse
-         (progn
-           (save-excursion
-             (wisi-parse wisi-parse-table 'wisi-forward-token)
-             (setq wisi-cache-max (point))
-             (setq wisi-parse-failed nil))
-           (run-hooks 'wisi-post-parse-succeed-hook))
-
-       ;; else capture errors from bad syntax, so higher level
-       ;; functions can try to continue and/or we don't bother the
-       ;; user.
-       (condition-case err
-           (progn
-             (save-excursion
-               (wisi-parse wisi-parse-table 'wisi-forward-token)
-               (setq wisi-cache-max (point))
-               (setq wisi-parse-failed nil))
-             (run-hooks 'wisi-post-parse-succeed-hook))
-         (wisi-parse-error
-          ;; delete caches past wisi-cache-max added by failed parse
-          (wisi-delete-cache wisi-cache-max)
-          (setq wisi-parse-failed t)
-          (setq wisi-parse-error-msg (cdr err)))
-         ))
-      (if wisi-parse-error-msg
-         ;; error
-         (cond
-          ((> wisi-debug 0)
-           (message "%s error" msg)
-           (wisi-goto-error)
-           (error wisi-parse-error-msg)))
-       ;; no msg; success
-       (when (> wisi-debug 0)
-         (message "%s done" msg)))
-      )
-    (when (and error-on-fail (not (>= wisi-cache-max pos)))
-      (error "parse failed"))
-    ))
-
-(defun wisi-fontify-region (_begin end)
-  "For `jit-lock-functions'."
-  (when (< (point-max) wisi-size-threshold)
-    (wisi-validate-cache end)))
-
-(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 (1- containing))))))
-
-(defun wisi-cache-region (cache)
-  "Return region designated by cache.
-Point must be at cache."
-  (cons (point) (+ (point) (wisi-cache-last cache))))
-
-(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))))
-
-;;;; parse actions
-
-(defun wisi-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)
-(defvar $nterm nil)
-;; keep byte-compiler happy; `wisi-tokens' and `$nterm' are bound in
-;; action created by wisi-semantic-action, and in module parser.
-;; FIXME: $nterm should have wisi- prefix
-
-(defun wisi-statement-action (pairs)
-  "Cache 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])"
-  (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 (cdr (aref wisi-tokens number)));; wisi-tokens is 
let-bound in wisi-parse-reduce
-              (token (car (aref wisi-tokens number)))
-              (class (aref pairs (setq i (1+ i))))
-              (mark
-               ;; Marker one char into token, so indent-line-to
-               ;; inserts space before the mark, not after
-               (when region (copy-marker (1+ (car region)))))
-              cache)
-
-         (setq i (1+ i))
-
-         (unless (memq class wisi-class-list)
-           (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 
generic_formal_part in
-                   ;;
-                   ;; generic_package_declaration : generic_formal_part 
package_specification SEMICOLON
-                   ;;    (wisi-statement-action 1 'block-start 2 'block-middle 
3 'statement-end)
-                   ;;
-                   ;; or simple_statement in
-                   ;;
-                   ;; statement : label_opt simple_statement
-                   ;;
-                   ;; override nonterm, class, containing
-                   ;; set end only if not set yet (due to failed parse)
-                   (progn
-                     (cl-case (wisi-cache-class cache)
-                       (block-start
-                        (setf (wisi-cache-class cache)
-                              (cond
-                                ((eq override-start nil)
-                                 (cond
-                                  ((memq class '(block-start statement-start)) 
'block-start)
-                                  (t 'block-middle)))
-
-                                ((memq override-start '(block-start 
statement-start)) 'block-start)
-
-                                (t (error "unexpected override-start"))
-                                )))
-                       (t
-                        (setf (wisi-cache-class cache) (or override-start 
class)))
-                       )
-                     (setf (wisi-cache-nonterm cache) $nterm)
-                     (setf (wisi-cache-containing cache) first-keyword-mark)
-                     (unless (wisi-cache-end cache)
-                       (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    $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
-                           (memq class '(block-start statement-start)))
-                   (setq override-start nil)
-                   (setq first-keyword-mark mark)))
-
-               (when (eq class 'statement-end)
-                 (wisi-set-end (1- first-keyword-mark) (copy-marker (1+ (car 
region)))))
-               )
-
-           ;; 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
-                      (memq class '(block-middle block-start statement-start)))
-             (setq override-start class)))
-       ))
-      )))
-
-(defun wisi-containing-action (containing-token contained-token)
-  "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."
-  ;; wisi-tokens is is bound in action created by wisi-semantic-action
-  (let* ((containing-region (cdr (aref wisi-tokens (1- containing-token))))
-        (contained-region (cdr (aref wisi-tokens (1- contained-token)))))
-
-    (unless containing-region ;;
-      (signal 'wisi-parse-error
-             (wisi-error-msg
-              "wisi-containing-action: containing-region '%s' is empty. 
grammar error; bad action"
-              (wisi-token-text (aref wisi-tokens (1- containing-token))))))
-
-    (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))))))
-
-    (while (not containing-region)
-      ;; containing-token is empty; use next
-      (setq containing-region (cdr (aref wisi-tokens 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 (1+ (car containing-region)))))
-         (while cache
-
-           ;; skip blocks that are already marked
-           (while (and (>= (point) (car contained-region))
-                       (markerp (wisi-cache-containing cache)))
-             (goto-char (1- (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-match-class-token (cache class-tokens)
-  "Return t if CACHE matches CLASS-TOKENS.
-CLASS-TOKENS is a vector [number class token_id class token_id ...].
-number is ignored."
-  (let ((i 1)
-       (done nil)
-       (result nil)
-       class token)
-    (while (and (not done)
-               (< i (length class-tokens)))
-      (setq class (aref class-tokens i))
-      (setq token (aref class-tokens (setq i (1+ i))))
-      (setq i (1+ i))
-      (when (and (eq class (wisi-cache-class cache))
-                (eq token (wisi-cache-token cache)))
-       (setq result t
-             done t))
-      )
-    result))
-
-(defun wisi-motion-action (token-numbers)
-  "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 class token_id]:
-vector [number class token_id class token_id ...]:
-   mark all tokens in number nonterminal matching (class token_id) with nil 
prev/next."
-  (save-excursion
-    (let (prev-keyword-mark
-         prev-cache
-         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 region (cdr (aref wisi-tokens (1- token-number))))
-           (when region
-             (setq cache (wisi-get-cache (car region)))
-             (setq mark (copy-marker (1+ (car region))))
-
-             (when (and prev-keyword-mark
-                        cache
-                        (null (wisi-cache-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)
-             ))
-
-          ((vectorp token-number)
-           ;; token-number may contain 0, 1, or more 'class token_id' pairs
-           ;; the corresponding region may be empty
-           ;; there must have been a prev keyword
-           (setq region (cdr (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.
-             (goto-char (car region))
-             (setq cache (or (wisi-get-cache (point))
-                             (wisi-forward-cache)))
-             (while (< (point) (cdr region))
-               (when (wisi-match-class-token cache token-number)
-                 (when (null (wisi-cache-prev cache))
-                   (setf (wisi-cache-prev cache) prev-keyword-mark))
-                 (when (null (wisi-cache-next cache))
-                   (setq mark (copy-marker (1+ (point))))
-                   (setf (wisi-cache-next prev-cache) mark)
-                   (setq prev-keyword-mark mark)
-                   (setq prev-cache cache)))
-
-               (setq cache (wisi-forward-cache))
-             )))
-
-          (t
-           (error "unexpected token-number %s" token-number))
-          )
-
-         ))
-      )))
-
-(defun wisi-extend-action (first last)
-  "Extend text of cache at token FIRST to cover all tokens thru LAST."
-  (let* ((first-region (cdr (aref wisi-tokens (1- first))));; wisi-tokens is 
let-bound in wisi-parse-reduce
-        (last-region (cdr (aref wisi-tokens (1- last))))
-       cache)
-
-    (when first-region
-      (setq cache (wisi-get-cache (car first-region)))
-      (setf (wisi-cache-last cache) (- (cdr last-region) (car first-region)))
-      )
-    ))
-
-(defun wisi-face-action-1 (face region &optional override-no-error)
-  "Apply FACE to REGION.
-If OVERRIDE-NO-ERROR is non-nil, don't report an error for overriding an 
existing face."
-  (when region
-    ;; We allow overriding a face property, because we don't want to
-    ;; delete them in wisi-invalidate (see comments there). On the
-    ;; other hand, it can be an error, so keep this debug
-    ;; code. However, to validly report errors, note that
-    ;; font-lock-face properties must be removed first, or the buffer
-    ;; must be fresh (never parsed), and wisi-debug must be > 1.
-    ;;
-    ;; Grammar sets override-no-error when a higher-level production might
-    ;; override a face in a lower-level production.
-    (when (> wisi-debug 1)
-      (let ((cur-face (get-text-property (car region) 'font-lock-face)))
-       (when cur-face
-         (unless override-no-error
-           (message "%s:%d overriding face %s with %s on '%s'"
-                    (buffer-file-name)
-                    (line-number-at-pos (car region))
-                    face
-                    cur-face
-                    (buffer-substring-no-properties (car region) (cdr 
region))))
-
-         )))
-    (with-silent-modifications
-      (add-text-properties
-       (car region) (cdr region)
-       (list
-       'font-lock-face face
-       'fontified t)))
-    ))
-
-(defun wisi-face-action (pairs &optional no-override)
-  "Cache face information in text properties of tokens.
-Intended as a grammar non-terminal action.
-
-PAIRS is a vector of the form [token-number face token-number face ...]
-token-number may be an integer, or a vector [integer token_id token_id ...]
-
-For an integer token-number, apply face to the first cached token
-in the range covered by wisi-tokens[token-number]. If there are
-no cached tokens, apply face to entire wisi-tokens[token-number]
-region.
-
-For a vector token-number, apply face to the first cached token
-in the range matching one of token_id covered by
-wisi-tokens[token-number].
-
-If NO-OVERRIDE is non-nil, don't override existing face."
-  (let (number region face (tokens nil) cache (i 0) (j 1))
-    (while (< i (length pairs))
-      (setq number (aref pairs i))
-      (setq face (aref pairs (setq i (1+ i))))
-      (cond
-       ((integerp number)
-       (setq region (cdr (aref wisi-tokens (1- number))));; wisi-tokens is 
let-bound in wisi-parse-reduce
-       (when region
-         (save-excursion
-           (goto-char (car region))
-           (setq cache (or (wisi-get-cache (point))
-                           (wisi-forward-cache)))
-           (if (< (point) (cdr region))
-               (when cache
-                 (wisi-face-action-1 face (wisi-cache-region cache) 
no-override))
-
-             ;; no caches in region; just apply face to region
-             (wisi-face-action-1 face region no-override))
-           )))
-
-       ((vectorp number)
-       (setq region (cdr (aref wisi-tokens (1- (aref number 0)))))
-       (when region
-         (while (< j (length number))
-           (setq tokens (cons (aref number j) tokens))
-           (setq j (1+ j)))
-         (save-excursion
-           (goto-char (car region))
-           (setq cache (wisi-forward-find-token tokens (cdr region) t))
-           ;; might be looking for IDENTIFIER in name, but only have "*".
-           (when cache
-             (wisi-face-action-1 face (wisi-cache-region cache) no-override))
-           )))
-       )
-      (setq i (1+ i))
-
-      )))
-
-(defun wisi-face-list-action (pairs &optional no-override)
-  "Cache face information in text properties of tokens.
-Intended as a grammar non-terminal action.
-
-PAIRS is a vector of the form [token-number face token-number face ...]
-token-number is an integer. Apply face to all cached tokens
-in the range covered by wisi-tokens[token-number].
-
-If NO-OVERRIDE is non-nil, don't override existing face."
-  (let (number region face cache (i 0))
-    (while (< i (length pairs))
-      (setq number (aref pairs i))
-      (setq face (aref pairs (setq i (1+ i))))
-      (setq region (cdr (aref wisi-tokens (1- number))));; wisi-tokens is 
let-bound in wisi-parse-reduce
-      (when region
-       (save-excursion
-         (goto-char (car region))
-         (setq cache (or (wisi-get-cache (point))
-                         (wisi-forward-cache)))
-         (while (<= (point) (cdr region))
-           (when cache
-             (wisi-face-action-1 face (wisi-cache-region cache) no-override))
-           (setq cache (wisi-forward-cache))
-           )))
-
-      (setq i (1+ i))
-
-      )))
-
-;;;; motion
-(defun wisi-backward-cache ()
-  "Move point backward to the beginning of the first token preceding point 
that has a cache.
-Returns cache, or nil if at beginning of buffer."
-  (let (cache pos)
-    (setq pos (previous-single-property-change (point) 'wisi-cache))
-    ;; There are three cases:
-    ;;
-    ;; 1) caches separated by non-cache chars: 'if ... then'
-    ;;    pos is before 'f', cache is on 'i'
-    ;;
-    ;; 2) caches not separated: ');'
-    ;;    pos is before ';', cache is on ';'
-    ;;
-    ;; 3) at bob; pos is nil
-    ;;
-    (if pos
-       (progn
-         (setq cache (get-text-property pos 'wisi-cache))
-         (if cache
-             ;; case 2
-             (goto-char pos)
-           ;; case 1
-           (setq cache (get-text-property (1- pos) 'wisi-cache))
-           (goto-char (1- pos))))
-      ;; at bob
-      (goto-char (point-min))
-      (setq cache nil))
-    cache
-    ))
-
-(defun wisi-forward-cache ()
-  "Move point forward to the beginning of the first token after point that has 
a cache.
-Returns cache, or nil if at end of buffer."
-  (let (cache pos)
-    (when (get-text-property (point) 'wisi-cache)
-      ;; on a cache; get past it
-      (goto-char (1+ (point))))
-
-    (setq cache (get-text-property (point) 'wisi-cache))
-    (if cache
-       nil
-
-      (setq pos (next-single-property-change (point) 'wisi-cache))
-      (if pos
-         (progn
-           (goto-char pos)
-           (setq cache (get-text-property pos 'wisi-cache)))
-       ;; at eob
-       (goto-char (point-max))
-       (setq cache nil))
-      )
-    cache
-    ))
-
-(defun wisi-forward-find-class (class limit)
-  "Search 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 (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 a token that has a cache with TOKEN.
-If point is at a matching token, return that token.
-TOKEN 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, then if NOERROR is nil, throw an
-error, if non-nil, return nil."
-  (let ((token-list (cond
-                    ((listp token) token)
-                    (t (list token))))
-       (cache (wisi-get-cache (point)))
-       (done nil))
-    (while (not (or done
-                   (and cache
-                        (memq (wisi-cache-token cache) token-list))))
-      (setq cache (wisi-forward-cache))
-      (when (>= (point) limit)
-       (if noerror
-           (progn
-             (setq done t)
-             (setq cache nil))
-         (error "cache with token %s not found" token))))
-    cache))
-
-(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 (1- (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-max) t) ;; ensure there is a next cache to 
move to
-    (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 (1- next))
-             (wisi-forward-token)
-             (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) t)
-  (let ((cache (wisi-get-cache (point))))
-    (if cache
-       (let ((prev (wisi-cache-prev cache)))
-         (if prev
-             (goto-char (1- prev))
-           (wisi-backward-cache)))
-      (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
-   ((markerp (wisi-cache-containing cache))
-    (goto-char (1- (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 block-start or 
statement-start.
-Return start cache."
-  (when
-    ;; cache nil at bob, or on cache in partially parsed statement
-    (while (and cache
-               (not (memq (wisi-cache-class cache) '(block-start 
statement-start))))
-      (setq cache (wisi-goto-containing cache)))
-    )
-  cache)
-
-(defun wisi-goto-end-1 (cache)
-  (goto-char (1- (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) t)
-  (let ((cache (wisi-get-cache (point))))
-    (unless cache
-      (setq cache (wisi-backward-cache)))
-    (wisi-goto-start cache)))
-
-(defun wisi-goto-statement-end ()
-  "Move point to token at end of statement point is in or before."
-  (interactive)
-  (wisi-validate-cache (point) t)
-  (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 (1- (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 (1- (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-current (offset)
-  "Return indentation OFFSET relative to indentation of current line."
-  (+ (current-indentation) offset)
-  )
-
-(defun wisi-indent-paren (offset)
-  "Return indentation OFFSET relative to preceding open paren."
-  (save-excursion
-    (goto-char (nth 1 (syntax-ppss)))
-    (+ (current-column) offset)))
-
-(defun wisi-indent-start (offset cache)
-  "Return indentation of OFFSET relative to containing ancestor
-of CACHE with class statement-start or block-start."
-  (wisi-goto-start cache)
-  (+ (current-indentation) offset))
-
-(defun wisi-indent-statement ()
-  "Indent region given by `wisi-goto-start' on cache at or before point, then 
wisi-cache-end."
-  (wisi-validate-cache (point) t)
-
-  (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 (progn
-                    (when (wisi-cache-end cache)
-                      ;; nil when cache is statement-end
-                      (goto-char (1- (wisi-cache-end cache))))
-                    (point))))
-         (indent-region start end)
-         ))
-      )))
-
-(defvar-local wisi-indent-calculate-functions nil
-  "Functions to calculate indentation. Each called with point
-  before a token at the beginning of a line (at current
-  indentation); return indentation column for that token, or
-  nil. May move point. Calling stops when first function returns
-  non-nil.")
-
-(defvar-local wisi-post-parse-fail-hook
-  "Function to reindent portion of buffer.
-Called from `wisi-indent-line' 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-line fails due to parse failing; cleared when 
indent succeeds.")
-
-(defvar-local wisi-indent-fallback 'wisi-indent-fallback-default
-  "Function to compute indent for current line when wisi parse fails.")
-
-(defun wisi-indent-fallback-default ()
-  ;; no indent info at point. Assume user is
-  ;; editing; indent to previous line, fix it
-  ;; after parse succeeds
-  (forward-line -1);; safe at bob
-  (back-to-indentation)
-  (current-column))
-
-(defun wisi-indent-line ()
-  "Indent current line using the wisi indentation engine."
-  (interactive)
-
-  (let ((savep (point))
-       indent)
-    (save-excursion
-      (back-to-indentation)
-      (when (>= (point) savep) (setq savep nil))
-
-      (when (>= (point) wisi-cache-max)
-       (wisi-validate-cache (line-end-position))) ;; include at lease the 
first token on this line
-
-      (if (> (point) wisi-cache-max)
-         (progn
-             (setq wisi-indent-failed t)
-             (setq indent (funcall wisi-indent-fallback)))
-
-       ;; parse succeeded
-       (when wisi-indent-failed
-         ;; previous parse failed
-         (setq wisi-indent-failed nil)
-         (run-hooks 'wisi-post-parse-fail-hook))
-
-       (when (> (point) wisi-cache-max)
-         (error "wisi-post-parse-fail-hook invalidated parse."))
-
-       (setq indent
-             (with-demoted-errors
-                 (or (run-hook-with-args-until-success 
'wisi-indent-calculate-functions) 0))
-             )
-       ))
-
-    (if savep
-       ;; point was inside line text; leave it there
-       (save-excursion (indent-line-to indent))
-      ;; point was before line text; move to start of text
-      (indent-line-to indent))
-    ))
-
-;;;; debug
-(defun wisi-parse-buffer ()
-  (interactive)
-  (syntax-propertize (point-max))
-  (wisi-invalidate-cache)
-  (wisi-validate-cache (point-max)) t)
-
-(defun wisi-lex-buffer ()
-  (interactive)
-  (syntax-propertize (point-max))
-  (goto-char (point-min))
-  (while (not (eq wisent-eoi-term (car (wisi-forward-token)))))
-  )
-
-(defun wisi-show-cache ()
-  "Show cache at point."
-  (interactive)
-  (message "%s" (wisi-get-cache (point))))
-
-(defun wisi-show-token ()
-  "Move forward across one keyword, show token_id."
-  (interactive)
-  (let ((token (wisi-forward-token)))
-    (message "%s" (car token))))
-
-(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)))
-    ))
-
-(defun wisi-show-cache-max ()
-  (interactive)
-  (push-mark)
-  (goto-char wisi-cache-max))
-
-;;;;; setup
-
-(defun wisi-setup (indent-calculate post-parse-fail class-list keyword-table 
token-table parse-table)
-  "Set up a buffer for parsing files with wisi."
-  (setq wisi-class-list class-list)
-  (setq wisi-string-double-term (car (symbol-value (intern-soft 
"string-double" token-table))))
-  (setq wisi-string-single-term (car (symbol-value (intern-soft 
"string-single" token-table))))
-  (setq wisi-symbol-term (car (symbol-value (intern-soft "symbol" 
token-table))))
-
-  (let ((numbers (cadr (symbol-value (intern-soft "number" token-table)))))
-    (setq wisi-number-term (car numbers))
-    (setq wisi-number-p (cdr numbers)))
-
-  (setq wisi-punctuation-table (symbol-value (intern-soft "punctuation" 
token-table)))
-  (setq wisi-punctuation-table-max-length 0)
-  (let (fail)
-    (dolist (item wisi-punctuation-table)
-      (when item ;; default matcher can be nil
-
-       ;; check that all chars used in punctuation tokens have punctuation 
syntax
-       (mapc (lambda (char)
-               (when (not (= ?. (char-syntax char)))
-                 (setq fail t)
-                 (message "in %s, %c does not have punctuation syntax"
-                          (car item) char)))
-             (cdr item))
-
-       (when (< wisi-punctuation-table-max-length (length (cdr item)))
-         (setq wisi-punctuation-table-max-length (length (cdr item)))))
-      )
-    (when fail
-      (error "aborting due to punctuation errors")))
-
-  (setq wisi-keyword-table keyword-table)
-  (setq wisi-parse-table parse-table)
-
-  ;; 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 'forward-sexp-function) #'wisi-forward-sexp)
-
-  (setq wisi-post-parse-fail-hook post-parse-fail)
-  (setq wisi-indent-failed nil)
-
-  (add-hook 'before-change-functions 'wisi-before-change nil t)
-  (add-hook 'after-change-functions 'wisi-after-change nil t)
-
-  (jit-lock-register 'wisi-fontify-region)
-
-  ;; see comments on "lexer" above re syntax-propertize
-  (syntax-propertize (point-max))
-
-  (wisi-invalidate-cache)
-  )
-
-(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 - 2018  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.0.0
+;; package-requires: ((cl-lib "1.0") (emacs "25.0") (seq "2.20"))
+;; URL: http://www.nongnu.org/ada-mode/wisi/wisi.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.
+;;
+;; 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 font-lock and navigate, keeping track of the point after which
+;; caches have been deleted is sufficent (see `wisi-cache-max').
+;;
+;; For indenting, 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 (normally the whole buffer),
+;; 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.
+;;
+;; 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.
+;;
+;; 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 100000
+  "Max size (in characters) for using wisi parser results for anything."
+  :type 'integer
+  :group 'wisi
+  :safe 'integerp)
+(make-variable-buffer-local 'wisi-size-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.")
+
+;;;; 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--cache-max
+  (list
+   (cons 'face nil)
+   (cons 'navigate nil)
+   (cons 'indent nil))
+  "Alist of maximimum position in buffer where parser text properties are 
valid.")
+
+(defun wisi-cache-max (&optional parse-action)
+  ;; Don't need 'wisi-set-cache-max; (move-marker (wisi-cache-max) foo) works
+  (let ((mark (cdr (assoc (or parse-action wisi--parse-action) 
wisi--cache-max))))
+    (unless (marker-position mark)
+      ;; Sometimes marker gets set to <marker in no buffer>; not clear how.
+      (move-marker mark (point-min)))
+    mark))
+
+(defun wisi--delete-face-cache (after)
+  (with-silent-modifications
+    (remove-text-properties after (point-max) '(wisi-face nil 'font-lock-face 
nil))
+    ))
+
+(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))
+    ))
+
+(defun wisi--delete-indent-cache (after)
+  (with-silent-modifications
+    (remove-text-properties after (point-max) '(wisi-indent nil))
+    ))
+
+(defun wisi-invalidate-cache (action after)
+  "Invalidate ACTION caches for the current buffer from AFTER to end of 
buffer."
+  (when (< after (wisi-cache-max action))
+    (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)
+      ;; 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)
+      ;; indent cache is stored on newline before line being indented.
+      (setq after
+           (save-excursion
+             (goto-char after)
+             (line-beginning-position)))
+      (wisi--delete-indent-cache (max 1 (1- after))))
+     )
+    (move-marker (wisi-cache-max action) after)
+    ))
+
+(defun wisi-reset-parser ()
+  "For ’ada-reset-parser’."
+  (wisi-invalidate-cache 'indent 0)
+  (wisi-invalidate-cache 'face 0)
+  (wisi-invalidate-cache 'navigate 0))
+
+;; 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.
+  ;;
+  ;; This change might be changing to/from a keyword; trigger
+  ;; font-lock. See test/ada_mode-interactive_common.adb Obj_1.
+  (unless wisi-indenting-p
+    (save-excursion
+      (let (word-end)
+       (goto-char end)
+       (skip-syntax-forward "w_")
+       (setq word-end (point))
+       (goto-char begin)
+       (skip-syntax-backward "w_")
+       (with-silent-modifications
+         (remove-text-properties (point) word-end '(font-lock-face nil 
fontified 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))
+
+  ;; Remove caches on inserted text, which could have caches from
+  ;; before the failed parse (or another buffer), and are in any case
+  ;; invalid. No point in removing 'fontified; that's handled by
+  ;; jit-lock.
+
+  (with-silent-modifications
+    (remove-text-properties begin end '(wisi-cache nil font-lock-face nil)))
+
+  (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)
+    (move-marker (wisi-cache-max parse-action) (point-max));; force delete 
caches
+    (wisi-invalidate-cache parse-action (point-min)))
+  )
+
+(defun wisi--run-parse ()
+  "Run the parser."
+  (unless (buffer-narrowed-p)
+    (let ((msg (when (> wisi-debug 0)
+                (format "wisi: parsing %s %s:%d ..."
+                        wisi--parse-action
+                        (buffer-name)
+                        (line-number-at-pos (point))))))
+      (when (> wisi-debug 0)
+       (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
+           (wisi-parse-current wisi--parser)
+           (setq wisi-parse-failed nil)
+           (move-marker (wisi-cache-max) (point))
+           )
+       (wisi-parse-error
+        (cl-ecase wisi--parse-action
+          (face
+           ;; caches set by failed parse are ok
+           (wisi--delete-face-cache (wisi-cache-max)))
+
+          (navigate
+           ;; parse partially resets caches before and after wisi-cache-max
+           (move-marker (wisi-cache-max) (point-min))
+           (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)))
+       )
+
+      (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 0)
+       (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 (pos error-on-fail parse-action)
+  "Ensure cached data for PARSE-ACTION is valid at least up to POS in current 
buffer."
+  (if (< (point-max) wisi-size-threshold)
+      (let ((wisi--parse-action parse-action))
+       (wisi--check-change)
+
+       ;; Now we can rely on wisi-cache-max.
+
+       ;; If wisi-cache-max = pos, then there is no cache at pos; need parse
+       (when (and (not wisi-inhibit-parse)
+                  (wisi-parse-try)
+                  (<= (wisi-cache-max) pos))
+
+         ;; Don't keep retrying failed parse until text changes again.
+         (wisi-set-parse-try nil)
+
+         (wisi--run-parse))
+
+       ;; 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 skipped due to ‘wisi-size-threshold’"))))
+
+(defun wisi-fontify-region (_begin end)
+  "For `jit-lock-functions'."
+  (wisi-validate-cache 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-max) t 'navigate) ;; ensure there is a next 
cache to move to
+    (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) 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) 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) 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) 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-indent-region (begin end)
+  "For `indent-region-function', using the wisi indentation engine."
+  (let ((wisi--parse-action 'indent)
+       (parse-required nil)
+       (end-mark (copy-marker end))
+       (prev-indent-failed wisi-indent-failed))
+
+    (wisi--check-change)
+
+    ;; Always indent the line containing BEGIN.
+    (save-excursion
+      (goto-char begin)
+      (setq begin (line-beginning-position))
+
+      (when (bobp) (forward-line))
+      (while (and (not parse-required)
+                 (<= (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 buffer, or fails and leaves valid caches
+    ;; untouched.
+    (when (and parse-required
+              (wisi-parse-try))
+
+      (wisi-set-parse-try nil)
+      (wisi--run-parse)
+
+      ;; If there were errors corrected, the indentation is
+      ;; potentially ambiguous; see test/ada_mode-interactive_2.adb
+      (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)
+           (funcall wisi-indent-region-fallback begin end)))
+
+      (save-excursion
+       ;; Apply cached indents.
+       (goto-char begin)
+       (let ((wisi-indenting-p t))
+         (while (and (not (eobp))
+                     (<= (point) end-mark)) ;; end-mark can be at the start of 
an empty line
+           (indent-line-to (if (bobp) 0 (get-text-property (1- (point)) 
'wisi-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)
+         (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))
+
+    (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
+                  ;; FIXME: ensure at beginning of error message line.
+                  (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-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-parse-buffer (&optional parse-action)
+  (interactive)
+  (unless parse-action (setq parse-action 'indent))
+  (wisi-set-parse-try t parse-action)
+  (move-marker (wisi-cache-max parse-action) (point-max));; force delete caches
+  (wisi-invalidate-cache parse-action (point-min))
+
+  (cl-ecase parse-action
+    (face
+     (with-silent-modifications
+       (remove-text-properties
+       (point-min) (point-max)
+       (list
+        'font-lock-face nil
+        'fontified nil)))
+     (wisi-validate-cache (point-max) t parse-action)
+     (when (fboundp 'font-lock-ensure) (font-lock-ensure))) ;; emacs < 25
+
+    (navigate
+     (wisi-validate-cache (point-max) t parse-action))
+
+    (indent
+     (wisi-indent-region (point-min) (point-max)))
+    ))
+
+(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)
+       (move-marker (wisi-cache-max 'indent) (point-max));; force delete caches
+       (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)
+  (move-marker (wisi-cache-max 'indent) (point-max));; force delete caches
+  (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 and face caches, and applied faces, at point."
+  (interactive)
+  (message "%s:%s:%s:%s"
+          (wisi-get-cache (point))
+          (get-text-property (point) 'wisi-face)
+          (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)))
+    ))
+
+(defun wisi-show-cache-max (action)
+  (push-mark)
+  (goto-char (wisi-cache-max action)))
+
+;;;;; 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--cache-max
+       (list
+        (cons 'face (copy-marker (point-min)))
+        (cons 'navigate (copy-marker (point-min)))
+        (cons 'indent (copy-marker (point-min)))))
+
+  (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
new file mode 100644
index 0000000..7569ffb
--- /dev/null
+++ b/wisitoken-bnf-generate.adb
@@ -0,0 +1,522 @@
+--  Abstract :
+--
+--  Parser for Wisi grammar files, producing Ada or Elisp source
+--  files for a parser.
+--
+--  Copyright (C) 2012 - 2015, 2017, 2018 Stephen Leake.  All Rights Reserved.
+--
+--  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 MERCHANTABILITY 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.Command_Line;
+with Ada.Directories;
+with Ada.Exceptions;
+with Ada.Real_Time;
+with Ada.Strings.Fixed;
+with Ada.Strings.Maps;
+with Ada.Strings.Unbounded;
+with Ada.Text_IO;
+with GNAT.Traceback.Symbolic;
+with WisiToken.BNF.Generate_Utils;
+with WisiToken.BNF.Output_Ada;
+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.Parse.LR.Parser_No_Recover; -- for reading BNF file
+with WisiToken.Productions;
+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;
+      First : Boolean := True;
+   begin
+      --  verbosity meaning is actually determined by output choice;
+      --  they should be consistent with this description.
+      Put_Line (Standard_Error, "version 1.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);
+      Put_Line (Standard_Error, "The following grammar file directives control 
parser generation:");
+      Put_Line (Standard_Error,
+                "%generate <algorithm> <output language> [<lexer>] 
[<interface>] [text_rep]");
+      Put_Line (Standard_Error, "   specify one of each generate parameter. 
May be repeated.");
+      Put (Standard_Error, "   algorithm: ");
+      for I of Generate_Algorithm_Image loop
+         if First then
+            First := False;
+         else
+            Put (Standard_Error, " | ");
+         end if;
+         Put (Standard_Error, I.all);
+      end loop;
+      New_Line (Standard_Error);
+
+      Put (Standard_Error, "   output language: ");
+      First := True;
+      for I of Output_Language_Image loop
+         if First then
+            First := False;
+         else
+            Put (Standard_Error, " | ");
+         end if;
+         Put (Standard_Error, I.all);
+      end loop;
+      New_Line (Standard_Error);
+
+      Put_Line (Standard_Error, "   interface: interface Process | Module");
+      Put_Line (Standard_Error, "      only valid with Ada_Emacs:");
+      Put_Line (Standard_Error, "      Process is for an external subprocess 
communicating with Emacs.");
+      Put_Line (Standard_Error, "      Module  is for a dynamically loaded 
Emacs module.");
+      Put (Standard_Error, "   lexer: ");
+      First := True;
+      for I of Output_Language_Image loop
+         if First then
+            First := False;
+         else
+            Put (Standard_Error, " | ");
+         end if;
+         Put (Standard_Error, I.all);
+      end loop;
+      New_Line (Standard_Error);
+      Put_Line
+        (Standard_Error, "   text_rep: output LR parse table in a text file, 
not as source code; for large tables");
+
+      New_Line (Standard_Error);
+      Put_Line (Standard_Error, "options:");
+      Put_Line (Standard_Error, "  --help: show this help");
+      Put_Line (Standard_Error, "  -v level: sets verbosity (default 0):");
+      Put_Line (Standard_Error, "     0 - only error messages to standard 
error");
+      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, "  --suffix <string>; appended to grammar file 
name");
+      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");
+
+   end Put_Usage;
+
+   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;
+   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;
+   Grammar_Parser : WisiToken.Parse.LR.Parser_No_Recover.Parser;
+
+   Do_Time : Boolean := False;
+
+   procedure Use_Input_File (File_Name : in String)
+   is
+      use Ada.Strings.Unbounded;
+      use Ada.Text_IO;
+   begin
+      Output_File_Name_Root := +Ada.Directories.Base_Name (File_Name) & Suffix;
+
+      Wisitoken_Grammar_Main.Create_Parser
+        (Parser    => Grammar_Parser,
+         Trace     => Trace'Unchecked_Access,
+         User_Data => Input_Data'Unchecked_Access);
+
+      Grammar_Parser.Lexer.Reset_With_File (File_Name);
+
+      declare
+         Language_Name_Dir   : constant Integer := Ada.Strings.Fixed.Index
+           (File_Name, Ada.Strings.Maps.To_Set ("/\"), Going => 
Ada.Strings.Backward);
+         Language_Name_Ext   : constant Integer := Ada.Strings.Fixed.Index 
(File_Name, ".wy");
+      begin
+         Language_Name := +WisiToken.BNF.Output_Elisp_Common.Elisp_Name_To_Ada
+           (File_Name
+              ((if Language_Name_Dir = 0
+                then File_Name'First
+                else Language_Name_Dir + 1) ..
+                 Language_Name_Ext - 1),
+            Append_ID => False,
+            Trim      => 0);
+      end;
+   exception
+   when Name_Error | Use_Error =>
+      raise Name_Error with "input file '" & File_Name & "' could not be 
opened.";
+   end Use_Input_File;
+
+begin
+   declare
+      use Ada.Command_Line;
+      Arg_Next : Integer := 1;
+   begin
+      loop
+         exit when Argument (Arg_Next)(1) /= '-';
+
+         --   --help, -v first, then alphabetical
+
+         if Argument (Arg_Next) = "--help" then
+            Put_Usage;
+            return;
+
+         elsif Argument (Arg_Next) = "-v" then
+            Arg_Next  := Arg_Next + 1;
+            WisiToken.Trace_Generate := Integer'Value (Argument (Arg_Next));
+            Arg_Next  := Arg_Next + 1;
+
+         elsif Argument (Arg_Next) = "--generate" then
+            Arg_Next  := Arg_Next + 1;
+            declare
+               Tuple : Generate_Tuple;
+               Done  : Boolean := False;
+            begin
+               begin
+                  Tuple.Gen_Alg := Generate_Algorithm'Value (Argument 
(Arg_Next));
+                  Arg_Next     := Arg_Next + 1;
+               exception
+               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);
+                  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;
+
+               Add (Command_Generate_Set, Tuple);
+            end;
+
+         elsif Argument (Arg_Next) = "--suffix" then
+            Arg_Next := Arg_Next + 1;
+            Suffix   := +Argument (Arg_Next);
+            Arg_Next := Arg_Next + 1;
+
+         elsif Argument (Arg_Next) = "--test_main" then
+            Arg_Next  := Arg_Next + 1;
+            Test_Main := True;
+
+         elsif Argument (Arg_Next) = "--time" then
+            Arg_Next := Arg_Next + 1;
+            Do_Time  := True;
+
+         else
+            raise User_Error with "invalid argument '" & Argument (Arg_Next) & 
"'";
+         end if;
+      end loop;
+
+      Use_Input_File (Argument (Arg_Next));
+
+      if Arg_Next /= Argument_Count then
+         raise User_Error with "arg count" & Integer'Image (Argument_Count) &
+           " different from expected count" & Integer'Image (Arg_Next);
+      end if;
+   end;
+
+   begin
+      Grammar_Parser.Parse;
+   exception
+   when WisiToken.Syntax_Error =>
+      Grammar_Parser.Put_Errors;
+      raise;
+   when E : WisiToken.Parse_Error =>
+      WisiToken.Generate.Put_Error (Ada.Exceptions.Exception_Message (E));
+      raise;
+   end;
+
+   declare
+      use all type Ada.Strings.Unbounded.Unbounded_String;
+      use Ada.Text_IO;
+
+      --  Create a .parse_table file unless verbosity > 0
+      Parse_Table_File : File_Type;
+
+      Generate_Set    : Generate_Set_Access;
+      Multiple_Tuples : Boolean;
+
+      Lexer_Done : Lexer_Set := (others => False);
+
+      --  In general, all of the data in Generate_Utils.Generate_Data
+      --  depends on the generate tuple parameters. However, if
+      --  'If_Lexer_Present' is false, then they don't depend on the lexer,
+      --  and if 'If_Parser_Present' is false, then they don't depend on the
+      --  Gen_Alg, except for the parser table. But it's not worth trying to
+      --  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
+         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;
+         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;
+
+      end Parse_Check;
+
+   begin
+      if Command_Generate_Set = null then
+         --  Get the first quad from the input file
+         Parse_Check (None, None);
+
+         if Input_Data.Generate_Set = null then
+            raise User_Error with
+              WisiToken.Generate.Error_Message
+                (Input_Data.Grammar_Lexer.File_Name, 1,
+                 "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);
+      else
+         Generate_Set := Command_Generate_Set;
+      end if;
+
+      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);
+
+         declare
+            use Ada.Real_Time;
+
+            Time_Start : Time;
+            Time_End   : Time;
+
+            Generate_Data : aliased WisiToken.BNF.Generate_Utils.Generate_Data 
:=
+              WisiToken.BNF.Generate_Utils.Initialize (Input_Data);
+
+            Packrat_Data : WisiToken.Generate.Packrat.Data
+              (Generate_Data.Descriptor.First_Terminal, 
Generate_Data.Descriptor.First_Nonterminal,
+               Generate_Data.Descriptor.Last_Nonterminal);
+         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
+                  WisiToken.BNF.Output_Ada_Common.Create_re2c
+                    (Input_Data, Tuple, Generate_Data, -Output_File_Name_Root, 
Input_Data.User_Names.Regexps);
+               end if;
+            end if;
+
+            if WisiToken.Trace_Generate = 0 and Tuple.Gen_Alg /= External then
+               Create
+                 (Parse_Table_File, Out_File,
+                  -Output_File_Name_Root & "_" & To_Lower 
(Generate_Algorithm'Image (Tuple.Gen_Alg)) &
+                    (if Input_Data.If_Lexer_Present
+                     then "_" & Lexer_Image (Input_Data.User_Lexer).all
+                     else "") &
+                    ".parse_table");
+               Set_Output (Parse_Table_File);
+            end if;
+
+            case Tuple.Gen_Alg is
+            when LALR =>
+
+               Time_Start := Clock;
+
+               Generate_Data.LR_Parse_Table := 
WisiToken.Generate.LR.LALR_Generate.Generate
+                 (Generate_Data.Grammar,
+                  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);
+
+               if Do_Time then
+                  Time_End := Clock;
+
+                  Put_Line
+                    (Standard_Error,
+                     "LALR " & Lexer_Image (Tuple.Lexer).all & " generate 
time:" &
+                       Duration'Image (To_Duration (Time_End - Time_Start)));
+               end if;
+
+               Generate_Data.Parser_State_Count :=
+                 Generate_Data.LR_Parse_Table.State_Last - 
Generate_Data.LR_Parse_Table.State_First + 1;
+               WisiToken.BNF.Generate_Utils.Count_Actions (Generate_Data);
+               WisiToken.BNF.Generate_Utils.Put_Stats (Input_Data, 
Generate_Data);
+
+            when LR1 =>
+               Time_Start := Clock;
+
+               Generate_Data.LR_Parse_Table := 
WisiToken.Generate.LR.LR1_Generate.Generate
+                 (Generate_Data.Grammar,
+                  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);
+
+               if Do_Time then
+                  Time_End := Clock;
+
+                  Put_Line
+                    (Standard_Error,
+                     "LR1 " & Lexer_Image (Tuple.Lexer).all & " generate 
time:" &
+                       Duration'Image (To_Duration (Time_End - Time_Start)));
+               end if;
+
+               Generate_Data.Parser_State_Count :=
+                 Generate_Data.LR_Parse_Table.State_Last - 
Generate_Data.LR_Parse_Table.State_First + 1;
+               WisiToken.BNF.Generate_Utils.Count_Actions (Generate_Data);
+               WisiToken.BNF.Generate_Utils.Put_Stats (Input_Data, 
Generate_Data);
+
+            when Packrat_Generate_Algorithm =>
+               --  The only significant computation done for Packrat is First, 
done
+               --  in Initialize; not worth timing.
+
+               Packrat_Data := WisiToken.Generate.Packrat.Initialize
+                 (Input_Data.Grammar_Lexer.File_Name, Generate_Data.Grammar, 
Generate_Data.Source_Line_Map,
+                  Generate_Data.Descriptor.First_Terminal);
+
+               Put_Line ("Tokens:");
+               WisiToken.Put_Tokens (Generate_Data.Descriptor.all);
+               New_Line;
+               Put_Line ("Productions:");
+               WisiToken.Productions.Put (Generate_Data.Grammar, 
Generate_Data.Descriptor.all);
+
+               Packrat_Data.Check_All (Generate_Data.Descriptor.all);
+
+            when External =>
+               null;
+            end case;
+
+            if WisiToken.Trace_Generate = 0 and Tuple.Gen_Alg /= External then
+               Set_Output (Standard_Output);
+               Close (Parse_Table_File);
+            end if;
+
+            if WisiToken.Generate.Error then
+               raise WisiToken.Grammar_Error with "errors: aborting";
+            end if;
+
+            case Tuple.Gen_Alg is
+            when LR_Generate_Algorithm =>
+               if Tuple.Text_Rep then
+                  WisiToken.Generate.LR.Put_Text_Rep
+                    (Generate_Data.LR_Parse_Table.all,
+                     -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);
+               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);
+
+            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 Elisp_Lang =>
+               WisiToken.BNF.Output_Elisp (Input_Data, -Output_File_Name_Root, 
Generate_Data, Packrat_Data, Tuple);
+
+            end case;
+         end;
+      end loop;
+   end;
+exception
+when WisiToken.Syntax_Error | WisiToken.Parse_Error =>
+   --  error message already output
+   Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);
+
+when E : User_Error =>
+   declare
+      use Ada.Command_Line;
+      use Ada.Exceptions;
+      use Ada.Text_IO;
+   begin
+      Put_Line (Standard_Error, Exception_Message (E));
+      Put_Command_Line (Ada_Comment);
+      Set_Exit_Status (Failure);
+      Put_Usage;
+   end;
+
+when E : WisiToken.Grammar_Error =>
+   --  error message not already output
+   declare
+      use Ada.Command_Line;
+      use Ada.Exceptions;
+      use Ada.Text_IO;
+   begin
+      Put_Line (Standard_Error, Exception_Message (E));
+      Set_Exit_Status (Failure);
+   end;
+
+when E :  others =>
+   --  IMPROVEME: for some exceptions, Error message already output via 
wisi.utils.Put_Error
+   declare
+      use Ada.Text_IO;
+      use Ada.Exceptions;
+      use Ada.Command_Line;
+   begin
+      Put_Line (Standard_Error, Exception_Name (E) & ": " & Exception_Message 
(E));
+      Put_Line (Standard_Error, GNAT.Traceback.Symbolic.Symbolic_Traceback 
(E));
+      Set_Exit_Status (Failure);
+   end;
+
+end WisiToken.BNF.Generate;
diff --git a/wisitoken-bnf-generate_grammar.adb 
b/wisitoken-bnf-generate_grammar.adb
new file mode 100644
index 0000000..03ef661
--- /dev/null
+++ b/wisitoken-bnf-generate_grammar.adb
@@ -0,0 +1,86 @@
+--  Abstract :
+--
+--  Output Ada source code to recreate Grammar.
+--
+--  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);
+
+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_packrat.adb 
b/wisitoken-bnf-generate_packrat.adb
new file mode 100644
index 0000000..ee43b16
--- /dev/null
+++ b/wisitoken-bnf-generate_packrat.adb
@@ -0,0 +1,331 @@
+--  Abstract :
+--
+--  Generate Ada code for a Packrat parser.
+--
+--  References:
+--
+--  See wisitoken-parse-packrat.ads.
+--
+--  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);
+
+with Ada.Text_IO; use Ada.Text_IO;
+with WisiToken.BNF.Generate_Utils;
+with WisiToken.Generate.Packrat;
+with WisiToken.Productions;
+procedure WisiToken.BNF.Generate_Packrat
+  (Data          : in WisiToken.Generate.Packrat.Data;
+   Generate_Data : in WisiToken.BNF.Generate_Utils.Generate_Data)
+is
+   use WisiToken.Generate;
+
+   Descriptor   : WisiToken.Descriptor renames Generate_Data.Descriptor.all;
+   Action_Names : Names_Array_Array renames Generate_Data.Action_Names.all;
+
+   subtype Terminal is Token_ID range Descriptor.First_Terminal .. 
Descriptor.Last_Terminal;
+
+   --  FIXME: optimize memoizing? small productions not worth the memory cost?
+   --  or just use langkit space optimization.
+
+   function Parser_Name (Nonterm : in Token_ID) return String
+   is begin
+      return "Parse_" & Image (Nonterm, Descriptor);
+   end Parser_Name;
+
+   procedure Put_Parser_Spec (Name : in String)
+   is begin
+      Indent_Line ("function " & Name);
+      Indent_Start ("  (Parser : in out Generated.Parser; Last_Pos : in 
Base_Token_Index) return Result_Type");
+   end Put_Parser_Spec;
+
+   function Var_Suffix (I, J : in Integer) return String
+   is begin
+      return Trimmed_Image (I) & '_' & Trimmed_Image (J);
+   end Var_Suffix;
+
+   procedure Generate_Parser_Body (Prod : in Productions.Instance)
+   is
+      use all type Ada.Containers.Count_Type;
+
+      Result_ID : constant String := Trimmed_Image (Prod.LHS);
+   begin
+      --  We use gotos and function scope vars rather than nested if/declare
+      --  to avoid excessive indenting for long productions.
+
+      Put_Parser_Spec (Parser_Name (Prod.LHS)); New_Line;
+      Indent_Line ("is");
+      Indent := Indent + 3;
+
+      Indent_Line ("Descriptor : WisiToken.Descriptor renames 
Parser.Trace.Descriptor.all;");
+      Indent_Line ("Start_Pos  : constant Token_Index := Last_Pos + 1; --  
first token in current nonterm");
+      Indent_Line ("Pos        : Base_Token_Index := Last_Pos; --  last token 
parsed.");
+
+      for RHS_Index in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
+         declare
+            RHS : Productions.Right_Hand_Side renames Prod.RHSs (RHS_Index);
+         begin
+            for Token_Index in RHS.Tokens.First_Index .. RHS.Tokens.Last_Index 
loop
+               if RHS.Tokens (Token_Index) in Descriptor.First_Terminal .. 
Descriptor.Last_Terminal then
+                  Indent_Line ("Pos_" & Var_Suffix (RHS_Index, Token_Index) & 
"  : Token_Index;");
+               else
+                  Indent_Line ("Memo_" & Var_Suffix (RHS_Index, Token_Index) & 
" : Memo_Entry;");
+               end if;
+            end loop;
+         end;
+      end loop;
+
+      if Data.Direct_Left_Recursive (Prod.LHS) then
+         Indent_Line ("Pos_Recurse_Last : Base_Token_Index := Last_Pos;");
+         Indent_Line ("Result_Recurse   : Memo_Entry;");
+      end if;
+
+      Indent := Indent - 3;
+      Indent_Line ("begin");
+      Indent := Indent + 3;
+
+      Indent_Line ("if Pos = Parser.Terminals.Last_Index then");
+      Indent_Line ("   return (State => Failure);");
+      Indent_Line ("end if;");
+      Indent_Line ("declare");
+      Indent_Line ("   Memo : Memo_Entry renames Parser.Derivs (" & Result_ID 
& ")(Start_Pos);");
+      Indent_Line ("begin");
+      Indent := Indent + 3;
+      Indent_Line ("case Memo.State is");
+      Indent_Line ("when Success =>");
+      Indent_Line ("   return Parser.Derivs (" & Result_ID & ")(Start_Pos);");
+      Indent_Line ("when Failure =>");
+
+      --  FIXME: Could simplify this when not doing left recursion
+      Indent_Line ("   goto RHS_" & Trimmed_Image (Prod.RHSs.Last_Index) & 
"_Fail;");
+
+      Indent_Line ("when No_Result =>");
+      Indent_Line ("   if Memo.Recursive then");
+      Indent_Start ("      raise Recursive with Image (" & Result_ID & ", 
Descriptor) &");
+      Put_Line (" Token_Index'Image (Start_Pos) & "": recursive"";");
+      Indent_Line ("   end if;");
+      Indent_Line ("   Memo.Recursive := True;");
+      Indent_Line ("end case;");
+      Indent := Indent - 3;
+      Indent_Line ("end;");
+      New_Line;
+
+      if Data.Direct_Left_Recursive (Prod.LHS) then
+         --  This is the top of the 'while' loop in [warth 2008] figure 3 
Grow-LR.
+         Indent_Line ("Parser.Derivs (" & Result_ID & ").Replace_Element 
(Start_Pos, (State => Failure));");
+         Indent_Line ("<<Recurse_Start>>");
+      end if;
+
+      for RHS_Index in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
+         declare
+            RHS : Productions.Right_Hand_Side renames Prod.RHSs (RHS_Index);
+
+            procedure Finish
+            is begin
+               if Data.Direct_Left_Recursive (Prod.LHS) then
+                  Indent_Line ("Result_Recurse :=");
+                  Indent := Indent + 2;
+               else
+                  Indent_Line ("Parser.Derivs (" & Result_ID & 
").Replace_Element");
+                  Indent_Line ("  (Start_Pos,");
+                  Indent := Indent + 3;
+               end if;
+               Indent_Line ("(State              => Success,");
+               Indent_Line (" Result             => Parser.Tree.Add_Nonterm");
+
+               Indent := Indent + 3;
+               Indent_Line ("(Production      => (" & Result_ID & ", " & 
Trimmed_Image (RHS_Index) & "),");
+               Indent_Line
+                 (" Action          => " &
+                    (if Action_Names (Prod.LHS) = null or else Action_Names 
(Prod.LHS)(RHS_Index) = null
+                     then "null,"
+                     else Action_Names (Prod.LHS)(RHS_Index).all & 
"'Access,"));
+
+               if RHS.Tokens.Length = 0 then
+                  Indent_Line (" Children        => (1 .. 0 => 
Syntax_Trees.Invalid_Node_Index),");
+
+               elsif RHS.Tokens.Length = 1 then
+                  Indent_Start (" Children        => ");
+                  if RHS.Tokens (RHS.Tokens.First_Index) in Terminal then
+                     Put ("(1 => Tree_Index (Pos_" & Var_Suffix (RHS_Index, 
RHS.Tokens.First_Index) & ")),");
+                  else
+                     Put ("(1 => Memo_" & Var_Suffix (RHS_Index, 
RHS.Tokens.First_Index) & ".Result),");
+                  end if;
+
+               else
+                  Indent_Line (" Children        =>");
+
+                  for Token_Index in RHS.Tokens.First_Index .. 
RHS.Tokens.Last_Index loop
+                     if RHS.Tokens (Token_Index) in Terminal then
+                        Indent_Start
+                          ((if Token_Index = RHS.Tokens.First_Index
+                            then "  ("
+                            else "   ") &
+                             "Tree_Index (Pos_" & Var_Suffix (RHS_Index, 
Token_Index) & ")");
+                     else
+                        Indent_Start
+                          ((if Token_Index = RHS.Tokens.First_Index
+                            then "  ("
+                            else "   ") &
+                             "Memo_" & Var_Suffix (RHS_Index, Token_Index) & 
".Result");
+                     end if;
+                     if Token_Index = RHS.Tokens.Last_Index then
+                        Put_Line ("),");
+                     else
+                        Put_Line (",");
+                     end if;
+                  end loop;
+               end if;
+
+               Indent_Line (" Default_Virtual => False),");
+               Indent := Indent - 3;
+               Indent_Start (" Last_Token      => Pos)");
+
+               if Data.Direct_Left_Recursive (Prod.LHS) then
+                  Put_Line (";");
+                  Indent := Indent - 2;
+                  Indent_Line ("goto Finish;");
+               else
+                  Put_Line (");");
+                  Indent := Indent - 3;
+                  Indent_Line ("goto Succeed;");
+               end if;
+            end Finish;
+
+         begin
+            Indent_Wrap_Comment (Productions.Image (Prod.LHS, RHS_Index, 
RHS.Tokens, Descriptor), Ada_Comment);
+            Indent_Line ("Pos := Last_Pos;");
+
+            if RHS.Tokens.Length = 0 then
+               Finish;
+            else
+               for Token_Index in RHS.Tokens.First_Index .. 
RHS.Tokens.Last_Index loop
+                  declare
+                     ID      : constant String := Trimmed_Image (RHS.Tokens 
(Token_Index));
+                     Var_Suf : constant String := Var_Suffix (RHS_Index, 
Token_Index);
+                  begin
+                     if RHS.Tokens (Token_Index) in Terminal then
+                        Indent_Line ("if Parser.Terminals (Pos + 1).ID = " & 
ID & " then");
+                        Indent := Indent + 3;
+                        Indent_Line ("Pos := Pos + 1;");
+                        Indent_Line ("Pos_" & Var_Suf & " := Pos;");
+                        if Token_Index = RHS.Tokens.Last_Index then
+                           Finish;
+                        end if;
+                        Indent := Indent - 3;
+                        Indent_Line ("else");
+                        Indent_Line ("   goto RHS_" & Trimmed_Image 
(RHS_Index) & "_Fail;");
+                        Indent_Line ("end if;");
+
+                     else -- nonterminal
+                        Indent_Line
+                          ("Memo_" & Var_Suf & " := Parse_" & Image 
(RHS.Tokens (Token_Index), Descriptor) &
+                             " (Parser, Pos);");
+                        Indent_Line ("case Result_States'(Memo_" & Var_Suf & 
".State) is");
+                        Indent_Line ("when Success =>");
+                        Indent := Indent + 3;
+                        Indent_Line ("Pos := Memo_" & Var_Suf & 
".Last_Token;");
+                        if Token_Index = RHS.Tokens.Last_Index then
+                           Finish;
+                        end if;
+                        Indent := Indent - 3;
+                        Indent_Line ("when Failure =>");
+                        Indent_Line ("   goto RHS_" & Trimmed_Image 
(RHS_Index) & "_Fail;");
+                        Indent_Line ("end case;");
+                     end if;
+                  end;
+               end loop;
+            end if;
+
+            Indent_Line ("<<RHS_" & Trimmed_Image (RHS_Index) & "_Fail>>");
+            New_Line;
+         end;
+      end loop;
+
+      --  We get here if the last alternative fails.
+      if Data.Direct_Left_Recursive (Prod.LHS) then
+         Indent_Line ("Result_Recurse := (State => Failure);");
+      else
+         Indent_Line ("Parser.Derivs (" & Result_ID & ").Replace_Element 
(Start_Pos, (State => Failure));");
+         Indent_Line ("return Parser.Derivs (" & Result_ID & ")(Start_Pos);");
+      end if;
+
+      if Data.Direct_Left_Recursive (Prod.LHS) then
+         Indent_Line ("<<Finish>>");
+         Indent_Line ("if Result_Recurse.State = Success then");
+         Indent := Indent + 3;
+         Indent_Line ("if Pos > Pos_Recurse_Last then");
+         --  made progress, try again
+         Indent := Indent + 3;
+         Indent_Line ("Parser.Derivs (" & Result_ID & ").Replace_Element 
(Start_Pos, Result_Recurse);");
+         Indent_Line ("Pos_Recurse_Last := Pos;");
+         Indent_Line ("if WisiToken.Trace_Parse > Detail then");
+         Indent_Line ("   Parser.Trace.Put_Line");
+         Indent_Line
+           ("     (Parser.Tree.Image (Result_Recurse.Result, Descriptor, 
Include_Children => True));");
+         Indent_Line ("end if;");
+         Indent_Line ("goto Recurse_Start;");
+         Indent := Indent - 3;
+         Indent_Line ("elsif Pos = Pos_Recurse_Last and then 
Parser.Tree.Is_Empty (Result_Recurse.Result) then");
+         --  Parse succeeded producing an empty nonterm; don't try again. This
+         --  special case is not in [warth 2008].
+         Indent_Line ("   Parser.Derivs (8).Replace_Element (Start_Pos, 
Result_Recurse);");
+         Indent_Line ("end if;");
+         Indent := Indent - 3;
+         Indent_Line ("end if;");
+      end if;
+      New_Line;
+
+      if not Data.Direct_Left_Recursive (Prod.LHS) then
+         Indent_Line ("<<Succeed>>");
+         Indent_Line ("if WisiToken.Trace_Parse > Detail then");
+         Indent := Indent + 3;
+         Indent_Line ("Parser.Trace.Put_Line");
+         Indent_Line ("  (Parser.Tree.Image");
+         Indent_Line
+           ("    (Parser.Derivs (" & Result_ID & ")(Start_Pos).Result, 
Descriptor, Include_Children => True));");
+         Indent := Indent - 3;
+         Indent_Line ("end if;");
+      end if;
+
+      Indent_Line ("return Parser.Derivs (" & Result_ID & ")(Start_Pos);");
+      Indent := Indent - 3;
+      Indent_Line ("end " & Parser_Name (Prod.LHS) & ";");
+      New_Line;
+   end Generate_Parser_Body;
+
+begin
+   Indent_Line ("use WisiToken;");
+   Indent_Line ("use WisiToken.Parse.Packrat;");
+   Indent_Line ("use WisiToken.Parse.Packrat.Generated;");
+
+   for Prod of Data.Grammar loop
+      Put_Parser_Spec (Parser_Name (Prod.LHS)); Put_Line (";");
+   end loop;
+   New_Line;
+
+   for Prod of Data.Grammar loop
+      Generate_Parser_Body (Prod);
+   end loop;
+
+   Indent_Line ("function Parse_wisitoken_accept_1");
+   Indent_Line
+     --  WORKAROUND: using Parse.Packrat.Parser'Class here generates GNAT bug 
box with GPL 2018
+     ("  (Parser : in out WisiToken.Parse.Base_Parser'Class; Last_Pos : in 
Base_Token_Index) return Result_Type");
+   Indent_Line ("is begin");
+   Indent_Line ("   return Parse_wisitoken_accept (Generated.Parser (Parser), 
Last_Pos);");
+   Indent_Line ("end Parse_wisitoken_accept_1;");
+   New_Line;
+
+end WisiToken.BNF.Generate_Packrat;
diff --git a/wisitoken-bnf-generate_utils.adb b/wisitoken-bnf-generate_utils.adb
new file mode 100644
index 0000000..01bc419
--- /dev/null
+++ b/wisitoken-bnf-generate_utils.adb
@@ -0,0 +1,818 @@
+--  Abstract :
+--
+--  see spec
+--
+--  Copyright (C) 2014, 2015, 2017, 2018  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
+--  published by the Free Software Foundation; either version 3, or (at
+--  your option) any later version. This program is distributed in the
+--  hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+--  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+--  PURPOSE. See the GNU General Public License for more details. You
+--  should have received a copy of the GNU General Public License
+--  distributed with this program; see file COPYING. If not, write to
+--  the Free Software Foundation, 51 Franklin Street, Suite 500, Boston,
+--  MA 02110-1335, USA.
+
+pragma License (GPL);
+
+with Ada.Exceptions;
+with Ada.Text_IO;
+with WisiToken.Generate; use WisiToken.Generate;
+with WisiToken.Syntax_Trees;
+with WisiToken.Wisi_Ada;
+package body WisiToken.BNF.Generate_Utils is
+
+   --  For Constant_Reference
+   Aliased_EOI_Name              : aliased constant 
Ada.Strings.Unbounded.Unbounded_String := +EOI_Name;
+   Aliased_WisiToken_Accept_Name : aliased constant 
Ada.Strings.Unbounded.Unbounded_String :=
+     +WisiToken_Accept_Name;
+
+   --  body specs, as needed.
+
+   ----------
+   --  Body subprograms
+
+   function Find_Kind (Data : aliased Generate_Data; Target_Kind : in String) 
return Token_ID
+   is begin
+      for Cursor in All_Tokens (Data).Iterate loop
+         if Kind (Cursor) = Target_Kind then
+            return ID (Cursor);
+         end if;
+      end loop;
+      return Invalid_Token_ID;
+   end Find_Kind;
+
+   function Name_1 (Cursor : in Token_Cursor) return String
+   is begin
+      --   This function is used to compute LR1_descriptor.Image
+      case Cursor.Kind is
+      when Non_Grammar_Kind =>
+         declare
+            Kind_Ref : constant 
WisiToken.BNF.Token_Lists.Constant_Reference_Type :=
+              WisiToken.BNF.Token_Lists.Constant_Reference 
(Cursor.Data.Tokens.Non_Grammar, Cursor.Token_Kind);
+
+            Item_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference (Kind_Ref.Element.Tokens, 
Cursor.Token_Item);
+         begin
+            return -Item_Ref.Element.Name;
+         end;
+
+      when Terminals_Keywords =>
+         declare
+            Keyword_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference 
(Cursor.Data.Tokens.Keywords, Cursor.Keyword);
+         begin
+            return -Keyword_Ref.Element.Name;
+         end;
+
+      when Terminals_Others =>
+         declare
+            Kind_Ref : constant 
WisiToken.BNF.Token_Lists.Constant_Reference_Type :=
+              WisiToken.BNF.Token_Lists.Constant_Reference 
(Cursor.Data.Tokens.Tokens, Cursor.Token_Kind);
+
+            Item_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference (Kind_Ref.Element.Tokens, 
Cursor.Token_Item);
+         begin
+            return -Item_Ref.Element.Name;
+         end;
+
+      when EOI =>
+         return EOI_Name;
+
+      when WisiToken_Accept =>
+         return WisiToken_Accept_Name;
+
+      when Nonterminal =>
+         declare
+            Rule_Ref : constant Rule_Lists.Constant_Reference_Type := 
Rule_Lists.Constant_Reference
+              (Cursor.Data.Tokens.Rules, Cursor.Nonterminal);
+         begin
+            return -Rule_Ref.Element.Left_Hand_Side;
+         end;
+
+      when Done =>
+         raise SAL.Programmer_Error with "token cursor is done";
+      end case;
+   end Name_1;
+
+   procedure To_Grammar
+     (Data             : aliased in out Generate_Data;
+      Source_File_Name :         in     String;
+      Start_Token      :         in     String)
+   is
+      use WisiToken.Wisi_Ada;
+      Descriptor : WisiToken.Descriptor renames Data.Descriptor.all;
+   begin
+      Data.Grammar.Set_First (Descriptor.First_Nonterminal);
+      Data.Grammar.Set_Last (Descriptor.Last_Nonterminal);
+      Data.Source_Line_Map.Set_First (Descriptor.First_Nonterminal);
+      Data.Source_Line_Map.Set_Last (Descriptor.Last_Nonterminal);
+
+      Data.Action_Names := new Names_Array_Array (Descriptor.First_Nonterminal 
.. Descriptor.Last_Nonterminal);
+      Data.Check_Names  := new Names_Array_Array (Descriptor.First_Nonterminal 
.. Descriptor.Last_Nonterminal);
+
+      pragma Assert (Descriptor.Accept_ID = Descriptor.First_Nonterminal);
+      begin
+         Data.Grammar (Descriptor.Accept_ID) :=
+           Descriptor.Accept_ID <= Only
+             (Find_Token_ID (Data, Start_Token) & Descriptor.EOF_ID + 
WisiToken.Syntax_Trees.Null_Action);
+
+         Data.Source_Line_Map (Descriptor.Accept_ID).Line := 
Line_Number_Type'First;
+         Data.Source_Line_Map (Descriptor.Accept_ID).RHS_Map.Set_First (0);
+         Data.Source_Line_Map (Descriptor.Accept_ID).RHS_Map.Set_Last (0);
+         Data.Source_Line_Map (Descriptor.Accept_ID).RHS_Map (0) := 
Line_Number_Type'First;
+      exception
+      when Not_Found =>
+         Put_Error
+           (Error_Message
+              (Source_File_Name, 1, "start token '" & (Start_Token) & "' not 
found; need %start?"));
+      end;
+
+      for Rule of Data.Tokens.Rules loop
+         declare
+            RHS_Index        : Natural := 0;
+            RHSs             : WisiToken.Productions.RHS_Arrays.Vector;
+            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;
+         begin
+            LHS := Find_Token_ID (Data, -Rule.Left_Hand_Side);
+
+            RHSs.Set_First (RHS_Index);
+            RHSs.Set_Last (Natural (Rule.Right_Hand_Sides.Length) - 1);
+
+            Data.Source_Line_Map (LHS).Line := Rule.Source_Line;
+            Data.Source_Line_Map (LHS).RHS_Map.Set_First (RHSs.First_Index);
+            Data.Source_Line_Map (LHS).RHS_Map.Set_Last (RHSs.Last_Index);
+
+            for Right_Hand_Side of Rule.Right_Hand_Sides loop
+               declare
+                  use Ada.Strings.Unbounded;
+                  use all type Ada.Containers.Count_Type;
+                  Tokens : WisiToken.Token_ID_Arrays.Vector;
+                  I      : Integer := 1;
+               begin
+                  if Right_Hand_Side.Tokens.Length > 0 then
+                     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);
+                        I := I + 1;
+                     end loop;
+                  end if;
+                  RHSs (RHS_Index) := (Tokens => Tokens, Action => null, Check 
=> null);
+                  if Length (Right_Hand_Side.Action) > 0 then
+                     Action_All_Empty := False;
+                     Action_Names (RHS_Index) := new String'
+                       (-Rule.Left_Hand_Side & '_' & WisiToken.Trimmed_Image 
(RHS_Index));
+                  end if;
+                  if Length (Right_Hand_Side.Check) > 0 then
+                     Check_All_Empty := False;
+                     Check_Names (RHS_Index) := new String'
+                       (-Rule.Left_Hand_Side & '_' & WisiToken.Trimmed_Image 
(RHS_Index) & "_check");
+                  end if;
+
+                  Data.Source_Line_Map (LHS).RHS_Map (RHS_Index) := 
Right_Hand_Side.Source_Line;
+               exception
+               when E : Not_Found =>
+                  --  From "&"
+                  Put_Error
+                    (Error_Message
+                       (Source_File_Name, Right_Hand_Side.Source_Line, 
Ada.Exceptions.Exception_Message (E)));
+               end;
+               RHS_Index := RHS_Index + 1;
+            end loop;
+
+            Data.Grammar (LHS) := LHS <= RHSs;
+            if not Action_All_Empty then
+               Data.Action_Names (LHS) := new Names_Array'(Action_Names);
+            end if;
+            if not Check_All_Empty then
+               Data.Check_Names (LHS) := new Names_Array'(Check_Names);
+            end if;
+
+         exception
+         when E : Not_Found =>
+            --  From Find_Token_ID (left_hand_side)
+            Put_Error
+              (Error_Message
+                 (Source_File_Name, Rule.Source_Line, 
Ada.Exceptions.Exception_Message (E)));
+         end;
+      end loop;
+
+      WisiToken.Generate.Check_Consistent (Data.Grammar, Descriptor, 
Source_File_Name);
+   end To_Grammar;
+
+   ----------
+   --  Public subprograms, declaration order
+
+   function Initialize (Input_Data : aliased in 
WisiToken_Grammar_Runtime.User_Data_Type) return Generate_Data
+   is
+      EOF_ID : constant Token_ID := Token_ID
+        (Count (Input_Data.Tokens.Non_Grammar) + Count 
(Input_Data.Tokens.Tokens)) + Token_ID
+          (Input_Data.Tokens.Keywords.Length) + Token_ID'First;
+   begin
+      return Result : aliased Generate_Data :=
+        (Tokens => Input_Data.Tokens'Access,
+
+         Descriptor => new WisiToken.Descriptor
+           (First_Terminal    =>
+              (if Count (Input_Data.Tokens.Non_Grammar) > 0
+               then Token_ID (Count (Input_Data.Tokens.Non_Grammar)) + 
Token_ID'First
+               else Token_ID'First),
+            Last_Terminal     => EOF_ID,
+            EOF_ID            => EOF_ID,
+            Accept_ID         => EOF_ID + 1,
+            First_Nonterminal => EOF_ID + 1,
+            Last_Nonterminal  => EOF_ID + 1 + Token_ID 
(Input_Data.Tokens.Rules.Length)),
+
+         others => <>)
+      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 LR1                                   => 
Result.Descriptor.Last_Terminal,
+            when LALR                                  => 
Result.Descriptor.First_Nonterminal,
+            when Packrat_Generate_Algorithm | External => Invalid_Token_ID);
+
+         for Cursor in All_Tokens (Result).Iterate loop
+            Result.Descriptor.Image (ID (Cursor)) := new String'(Name_1 
(Cursor));
+         end loop;
+
+         for ID in Result.Descriptor.Image'Range loop
+            if ID in Result.Descriptor.First_Terminal .. 
Result.Descriptor.Last_Terminal then
+               if Result.Descriptor.Image (ID).all'Length > 
Result.Descriptor.Terminal_Image_Width then
+                  Result.Descriptor.Terminal_Image_Width := 
Result.Descriptor.Image (ID).all'Length;
+               end if;
+            end if;
+
+            if Result.Descriptor.Image (ID).all'Length > 
Result.Descriptor.Image_Width then
+               Result.Descriptor.Image_Width := Result.Descriptor.Image 
(ID).all'Length;
+            end if;
+         end loop;
+
+         To_Grammar (Result, Input_Data.Grammar_Lexer.File_Name, 
-Input_Data.Language_Params.Start_Token);
+      end return;
+   end Initialize;
+
+   function Find_Token_ID (Data : aliased in Generate_Data; Token : in String) 
return Token_ID
+   is begin
+      for Cursor in All_Tokens (Data).Iterate loop
+         if Name (Cursor) = Token then
+            return ID (Cursor);
+         end if;
+      end loop;
+      raise Not_Found with "token '" & Token & "' not found";
+   end Find_Token_ID;
+
+   function All_Tokens (Data : aliased in Generate_Data) return Token_Container
+   is begin
+      return (Data => Data'Access);
+   end All_Tokens;
+
+   function Constant_Reference
+     (Container : aliased in Token_Container'Class;
+      Cursor    :         in Token_Cursor)
+     return Token_Constant_Reference_Type
+   is begin
+      case Cursor.Kind is
+      when Non_Grammar_Kind =>
+         declare
+            Token_Ref : constant 
WisiToken.BNF.Token_Lists.Constant_Reference_Type :=
+              WisiToken.BNF.Token_Lists.Constant_Reference 
(Container.Data.Tokens.Non_Grammar, Cursor.Token_Kind);
+
+            Item_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference (Token_Ref.Element.Tokens, 
Cursor.Token_Item);
+         begin
+            return (Element => Item_Ref.Element.all.Name'Access);
+         end;
+
+      when Terminals_Keywords =>
+         declare
+            Keyword_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference 
(Container.Data.Tokens.Keywords, Cursor.Keyword);
+         begin
+            return (Element => Keyword_Ref.Element.all.Name'Access);
+         end;
+
+      when Terminals_Others =>
+         declare
+            Token_Ref : constant 
WisiToken.BNF.Token_Lists.Constant_Reference_Type :=
+              WisiToken.BNF.Token_Lists.Constant_Reference 
(Container.Data.Tokens.Tokens, Cursor.Token_Kind);
+
+            Item_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference (Token_Ref.Element.Tokens, 
Cursor.Token_Item);
+         begin
+            return (Element => Item_Ref.Element.all.Name'Access);
+         end;
+
+      when EOI =>
+         return (Element => Aliased_EOI_Name'Access);
+
+      when WisiToken_Accept =>
+         return (Element => Aliased_WisiToken_Accept_Name'Access);
+
+      when Nonterminal =>
+         declare
+            Rule_Ref : constant Rule_Lists.Constant_Reference_Type := 
Rule_Lists.Constant_Reference
+              (Container.Data.Tokens.Rules, Cursor.Nonterminal);
+         begin
+            return (Element => Rule_Ref.Element.all.Left_Hand_Side'Access);
+         end;
+
+      when Done =>
+         raise SAL.Programmer_Error with "token cursor is done";
+      end case;
+   end Constant_Reference;
+
+   type Token_Access_Constant is access constant Token_Container;
+   type Iterator is new Iterator_Interfaces.Forward_Iterator with record
+      Container    : Token_Access_Constant;
+      Non_Grammar  : Boolean;
+      Nonterminals : Boolean;
+   end record;
+
+   overriding function First (Object : Iterator) return Token_Cursor;
+   overriding function Next (Object : Iterator; Position : Token_Cursor) 
return Token_Cursor;
+
+   overriding function First (Object : Iterator) return Token_Cursor
+   is begin
+      return First (Object.Container.Data.all, Object.Non_Grammar, 
Object.Nonterminals);
+   end First;
+
+   overriding function Next (Object  : Iterator; Position : Token_Cursor) 
return Token_Cursor
+   is
+      Next_Position : Token_Cursor := Position;
+   begin
+      Next (Next_Position, Object.Nonterminals);
+      return Next_Position;
+   end Next;
+
+   function Iterate
+     (Container    : aliased    Token_Container;
+      Non_Grammar  :         in Boolean := True;
+      Nonterminals :         in Boolean := True)
+     return Iterator_Interfaces.Forward_Iterator'Class
+   is begin
+      return Iterator'(Container'Access, Non_Grammar, Nonterminals);
+   end Iterate;
+
+   function Next_Kind_Internal
+     (Cursor       : in out Token_Cursor;
+      Nonterminals : in     Boolean)
+     return Boolean
+   is begin
+      --  Advance Cursor to the next kind; return True if any of that
+      --  kind exist, or kind is Done; False otherwise.
+      case Cursor.Kind is
+      when Non_Grammar_Kind =>
+
+         Cursor :=
+           (Data        => Cursor.Data,
+            Kind        => Terminals_Keywords,
+            ID          => Cursor.Data.Descriptor.First_Terminal,
+            Token_Kind  => WisiToken.BNF.Token_Lists.No_Element,
+            Token_Item  => String_Pair_Lists.No_Element,
+            Keyword     => Cursor.Data.Tokens.Keywords.First,
+            Nonterminal => Rule_Lists.No_Element);
+
+         return String_Pair_Lists.Has_Element (Cursor.Keyword);
+
+      when Terminals_Keywords =>
+
+         Cursor :=
+           (Data        => Cursor.Data,
+            Kind        => Terminals_Others,
+            ID          => Cursor.ID,
+            Token_Kind  => Cursor.Data.Tokens.Tokens.First,
+            Token_Item  => String_Pair_Lists.No_Element,
+            Keyword     => String_Pair_Lists.No_Element,
+            Nonterminal => Rule_Lists.No_Element);
+
+         if WisiToken.BNF.Token_Lists.Has_Element (Cursor.Token_Kind) then
+            Cursor.Token_Item := Cursor.Data.Tokens.Tokens 
(Cursor.Token_Kind).Tokens.First;
+            return WisiToken.BNF.String_Pair_Lists.Has_Element 
(Cursor.Token_Item);
+         else
+            return False;
+         end if;
+
+      when Terminals_Others =>
+         Cursor :=
+           (Data        => Cursor.Data,
+            Kind        => EOI,
+            ID          => Cursor.ID,
+            Token_Kind  => WisiToken.BNF.Token_Lists.No_Element,
+            Token_Item  => String_Pair_Lists.No_Element,
+            Keyword     => String_Pair_Lists.No_Element,
+            Nonterminal => Rule_Lists.No_Element);
+
+         return True;
+
+      when EOI =>
+         if Nonterminals then
+            if Rule_Lists.Has_Element (Cursor.Data.Tokens.Rules.First) then
+               Cursor :=
+                 (Data        => Cursor.Data,
+                  Kind        => WisiToken_Accept,
+                  ID          => Cursor.ID,
+                  Token_Kind  => WisiToken.BNF.Token_Lists.No_Element,
+                  Token_Item  => String_Pair_Lists.No_Element,
+                  Keyword     => String_Pair_Lists.No_Element,
+                  Nonterminal => Rule_Lists.No_Element);
+            else
+               Cursor.Kind := Done;
+            end if;
+            return True;
+         else
+            Cursor.Kind := Done;
+            return True;
+         end if;
+
+      when WisiToken_Accept =>
+         Cursor :=
+           (Data        => Cursor.Data,
+            Kind        => Nonterminal,
+            ID          => Cursor.ID,
+            Token_Kind  => WisiToken.BNF.Token_Lists.No_Element,
+            Token_Item  => String_Pair_Lists.No_Element,
+            Keyword     => String_Pair_Lists.No_Element,
+            Nonterminal => Cursor.Data.Tokens.Rules.First);
+
+         --  Can't get here with no rules
+         return True;
+
+      when Nonterminal =>
+         Cursor.Kind := Done;
+
+         return True;
+
+      when Done =>
+         return True;
+      end case;
+   end Next_Kind_Internal;
+
+   function First
+     (Data         : aliased in Generate_Data;
+      Non_Grammar  :         in Boolean;
+      Nonterminals :         in Boolean)
+     return Token_Cursor
+   is
+      Cursor : Token_Cursor :=
+        (Data        => Data'Access,
+         Kind        => Non_Grammar_Kind,
+         ID          => Token_ID'First,
+         Token_Kind  => Data.Tokens.Non_Grammar.First,
+         Token_Item  => String_Pair_Lists.No_Element,
+         Keyword     => String_Pair_Lists.No_Element,
+         Nonterminal => Rule_Lists.No_Element);
+   begin
+      if Non_Grammar then
+         if WisiToken.BNF.Token_Lists.Has_Element (Cursor.Token_Kind) then
+            Cursor.Token_Item := Cursor.Data.Tokens.Non_Grammar 
(Cursor.Token_Kind).Tokens.First;
+            if WisiToken.BNF.String_Pair_Lists.Has_Element (Cursor.Token_Item) 
then
+               return Cursor;
+            end if;
+         end if;
+      end if;
+
+      --  There are no non_grammar tokens, or Non_Grammar false
+      loop
+         exit when Next_Kind_Internal (Cursor, Nonterminals);
+      end loop;
+      return Cursor;
+   end First;
+
+   procedure Next (Cursor : in out Token_Cursor; Nonterminals : in Boolean)
+   is begin
+      Cursor.ID := Cursor.ID + 1;
+
+      case Cursor.Kind is
+      when Non_Grammar_Kind =>
+         String_Pair_Lists.Next (Cursor.Token_Item);
+         if String_Pair_Lists.Has_Element (Cursor.Token_Item) then
+            return;
+         else
+            WisiToken.BNF.Token_Lists.Next (Cursor.Token_Kind);
+
+            if WisiToken.BNF.Token_Lists.Has_Element (Cursor.Token_Kind) then
+               Cursor.Token_Item := Cursor.Data.Tokens.Non_Grammar 
(Cursor.Token_Kind).Tokens.First;
+               if String_Pair_Lists.Has_Element (Cursor.Token_Item) then
+                  return;
+               end if;
+            end if;
+         end if;
+
+         loop
+            exit when Next_Kind_Internal (Cursor, Nonterminals);
+         end loop;
+         return;
+
+      when Terminals_Keywords =>
+         --  Keywords before other terminals, so they have precedence over 
Identifiers
+
+         String_Pair_Lists.Next (Cursor.Keyword);
+         if String_Pair_Lists.Has_Element (Cursor.Keyword) then
+            return;
+         end if;
+
+         loop
+            exit when Next_Kind_Internal (Cursor, Nonterminals);
+         end loop;
+         return;
+
+      when Terminals_Others =>
+         WisiToken.BNF.String_Pair_Lists.Next (Cursor.Token_Item);
+         if WisiToken.BNF.String_Pair_Lists.Has_Element (Cursor.Token_Item) 
then
+            return;
+         else
+            WisiToken.BNF.Token_Lists.Next (Cursor.Token_Kind);
+            if WisiToken.BNF.Token_Lists.Has_Element (Cursor.Token_Kind) then
+               Cursor.Token_Item := Cursor.Data.Tokens.Tokens 
(Cursor.Token_Kind).Tokens.First;
+               if WisiToken.BNF.String_Pair_Lists.Has_Element 
(Cursor.Token_Item) then
+                  return;
+               end if;
+            end if;
+         end if;
+
+         loop
+            exit when Next_Kind_Internal (Cursor, Nonterminals);
+         end loop;
+         return;
+
+      when EOI =>
+         if Next_Kind_Internal (Cursor, Nonterminals) then
+            return;
+         else
+            raise SAL.Programmer_Error;
+         end if;
+
+      when WisiToken_Accept =>
+         if Next_Kind_Internal (Cursor, Nonterminals) then
+            return;
+         else
+            raise SAL.Programmer_Error;
+         end if;
+
+      when Nonterminal =>
+         Rule_Lists.Next (Cursor.Nonterminal);
+         if not Rule_Lists.Has_Element (Cursor.Nonterminal) then
+            Cursor.Kind := Done;
+         end if;
+
+      when Done =>
+         null;
+      end case;
+   end Next;
+
+   function Is_Done (Cursor : in Token_Cursor) return Boolean
+   is begin
+      return Cursor.Kind = Done;
+   end Is_Done;
+
+   function ID (Cursor : in Token_Cursor) return Token_ID
+   is begin
+      return Cursor.ID;
+   end ID;
+
+   function Name (Cursor : in Token_Cursor) return String
+   is begin
+      return Cursor.Data.Descriptor.Image (Cursor.ID).all;
+   end Name;
+
+   function Kind (Cursor : in Token_Cursor) return String
+   is begin
+      case Cursor.Kind is
+      when Non_Grammar_Kind =>
+         return -Token_Lists.Constant_Reference 
(Cursor.Data.Tokens.Non_Grammar, Cursor.Token_Kind).Kind;
+
+      when Terminals_Keywords =>
+         return "keyword";
+
+      when Terminals_Others =>
+         return -Token_Lists.Constant_Reference (Cursor.Data.Tokens.Tokens, 
Cursor.Token_Kind).Kind;
+
+      when EOI =>
+         return "EOI";
+
+      when WisiToken_Accept =>
+         return "accept";
+
+      when Nonterminal =>
+            return "nonterminal";
+
+      when Done =>
+         raise SAL.Programmer_Error with "token cursor is done";
+      end case;
+   end Kind;
+
+   function Value (Cursor : in Token_Cursor) return String
+   is begin
+      case Cursor.Kind is
+      when Non_Grammar_Kind =>
+         declare
+            Token_Ref : constant 
WisiToken.BNF.Token_Lists.Constant_Reference_Type :=
+              WisiToken.BNF.Token_Lists.Constant_Reference 
(Cursor.Data.Tokens.Non_Grammar, Cursor.Token_Kind);
+
+            Item_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference (Token_Ref.Element.Tokens, 
Cursor.Token_Item);
+         begin
+            return -Item_Ref.Element.Value;
+         end;
+
+      when Terminals_Keywords =>
+         declare
+            Keyword_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference 
(Cursor.Data.Tokens.Keywords, Cursor.Keyword);
+         begin
+            return -Keyword_Ref.Element.Value;
+         end;
+
+      when Terminals_Others =>
+         declare
+            Token_Ref : constant 
WisiToken.BNF.Token_Lists.Constant_Reference_Type :=
+              WisiToken.BNF.Token_Lists.Constant_Reference 
(Cursor.Data.Tokens.Tokens, Cursor.Token_Kind);
+
+            Item_Ref : constant String_Pair_Lists.Constant_Reference_Type :=
+              String_Pair_Lists.Constant_Reference (Token_Ref.Element.Tokens, 
Cursor.Token_Item);
+         begin
+            return -Item_Ref.Element.Value;
+         end;
+
+      when EOI | WisiToken_Accept | Nonterminal =>
+            return "";
+
+      when Done =>
+         raise SAL.Programmer_Error with "token cursor is done";
+      end case;
+   end Value;
+
+   function To_Conflicts
+     (Data             : aliased in out Generate_Data;
+      Conflicts        :         in     WisiToken.BNF.Conflict_Lists.List;
+      Source_File_Name :         in     String)
+     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 :=
+              (Conflict_Parse_Actions'Value (-Item.Action_A),
+               Find_Token_ID (Data, -Item.LHS_A),
+               Conflict_Parse_Actions'Value (-Item.Action_B),
+               Find_Token_ID (Data, -Item.LHS_B),
+               -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)));
+         end;
+      end loop;
+      return Result;
+   end To_Conflicts;
+
+   function To_Nonterminal_ID_Set
+     (Data : aliased in Generate_Data;
+      Item :         in String_Lists.List)
+     return Token_ID_Set
+   is
+      Result : Token_ID_Set := (Data.Descriptor.First_Nonterminal .. 
Data.Descriptor.Last_Nonterminal => False);
+   begin
+      for Token of Item loop
+         Result (Find_Token_ID (Data, Token)) := True;
+      end loop;
+      return Result;
+   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)
+     return WisiToken.Parse.LR.McKenzie_Param_Type
+   is
+      use Ada.Strings.Unbounded;
+
+      Result : WisiToken.Parse.LR.McKenzie_Param_Type :=
+        --  We use an aggregate, and overwrite some below, so the compiler
+        --  reminds us to change this when we modify McKenzie_Param_Type.
+        (Data.Descriptor.First_Terminal,
+         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;
+   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;
+      end loop;
+      for Pair of Item.Insert loop
+         Result.Insert (Find_Token_ID (Data, -Pair.Name)) := Natural'Value 
(-Pair.Value);
+      end loop;
+      for Pair of Item.Push_Back loop
+         Result.Push_Back (Find_Token_ID (Data, -Pair.Name)) := Natural'Value 
(-Pair.Value);
+      end loop;
+
+      return Result;
+   end To_McKenzie_Param;
+
+   procedure Count_Actions (Data : in out Generate_Utils.Generate_Data)
+   is begin
+      Data.Table_Actions_Count := 0;
+      for State_Index in Data.LR_Parse_Table.States'Range loop
+         Data.Table_Actions_Count := Data.Table_Actions_Count +
+           Actions_Length (Data.LR_Parse_Table.States (State_Index)) + 1;
+      end loop;
+   end Count_Actions;
+
+   procedure Put_Stats
+     (Input_Data    : in WisiToken_Grammar_Runtime.User_Data_Type;
+      Generate_Data : in Generate_Utils.Generate_Data)
+   is
+      use Ada.Text_IO;
+   begin
+      New_Line;
+      Put_Line
+        (Integer'Image (Input_Data.Rule_Count) & " rules," &
+           Integer'Image (Input_Data.Action_Count) & " user actions," &
+           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
+   is
+      use all type WisiToken.Parse.LR.Action_Node_Ptr;
+      Node : Parse.LR.Action_Node_Ptr := State.Action_List;
+   begin
+      return Result : Integer := 0
+      do
+         loop
+            exit when Node = null;
+            Result := Result + 1;
+            Node := Node.Next;
+            exit when Node.Next = null; -- don't count Error
+         end loop;
+      end return;
+   end Actions_Length;
+
+end WisiToken.BNF.Generate_Utils;
diff --git a/wisitoken-bnf-generate_utils.ads b/wisitoken-bnf-generate_utils.ads
new file mode 100644
index 0000000..d7347de
--- /dev/null
+++ b/wisitoken-bnf-generate_utils.ads
@@ -0,0 +1,176 @@
+--  Abstract :
+--
+--  Utilities for translating input file structures to WisiToken
+--  structures needed for LALR.Generate.
+--
+--  Copyright (C) 2014, 2015, 2017, 2018 Stephen Leake All Rights Reserved.
+--
+--  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 MERCHANTABILITY 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.Iterator_Interfaces;
+with WisiToken.Generate.LR;
+with WisiToken.Parse.LR;
+with WisiToken.Productions;
+with WisiToken_Grammar_Runtime;
+package WisiToken.BNF.Generate_Utils is
+
+   EOI_Name : constant String := "Wisi_EOI";
+   --  EOI_Name is used for EOF_ID token; it must match Emacs ada-mode
+   --  wisi.el wisi-eoi-term. It must be a valid Ada identifier when
+   --  "_ID" is appended.
+
+   WisiToken_Accept_Name : constant String := "wisitoken_accept";
+
+   type Generate_Data is limited record
+      Tokens     : access constant WisiToken.BNF.Tokens;
+      Descriptor : access WisiToken.Descriptor;
+      Grammar    : WisiToken.Productions.Prod_Arrays.Vector;
+
+      Action_Names : access Names_Array_Array;
+      Check_Names  : access Names_Array_Array;
+      --  Names of subprograms for each grammar semantic action and check;
+      --  non-null only if there is an action or check in the grammar.
+
+      Start_ID        : WisiToken.Token_ID;
+      Source_Line_Map : WisiToken.Productions.Source_Line_Maps.Vector;
+
+      --  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;
+   end record;
+
+   function Initialize (Input_Data : aliased in 
WisiToken_Grammar_Runtime.User_Data_Type) return Generate_Data;
+
+   function Find_Token_ID (Data : aliased in Generate_Data; Token : in String) 
return Token_ID;
+
+   type Token_Container (Data : not null access constant Generate_Data) is 
tagged null record
+   with
+     Constant_Indexing => Constant_Reference,
+     Default_Iterator  => Iterate,
+     Iterator_Element  => Ada.Strings.Unbounded.Unbounded_String;
+   --  We need a container type to define an iterator; the actual data is
+   --  in Data.Tokens. The Iterator_Element is given by Token_Name below.
+
+   function All_Tokens (Data : aliased in Generate_Data) return 
Token_Container;
+
+   type Token_Constant_Reference_Type
+     (Element : not null access constant 
Ada.Strings.Unbounded.Unbounded_String)
+     is null record
+   with Implicit_Dereference => Element;
+
+   type Token_Cursor is private;
+   --  Iterate thru Keywords, Tokens, Rules in a canonical order:
+   --
+   --  1. Non_Grammar
+   --  2. Keywords
+   --  3. other terminal tokens, in declaration order
+   --  4. EOI
+   --  5. Accept
+   --  6. Nonterminals
+   --
+   --  Within each group, tokens occur in the order they were declared in
+   --  the grammar file.
+
+   function Constant_Reference
+     (Container : aliased in Token_Container'Class;
+      Cursor    :         in Token_Cursor)
+     return Token_Constant_Reference_Type;
+
+   function Is_Done (Cursor : in Token_Cursor) return Boolean;
+   function Has_Element (Cursor : in Token_Cursor) return Boolean is (not 
Is_Done (Cursor));
+   package Iterator_Interfaces is new Ada.Iterator_Interfaces (Token_Cursor, 
Has_Element);
+   function Iterate
+     (Container    : aliased    Token_Container;
+      Non_Grammar  :         in Boolean := True;
+      Nonterminals :         in Boolean := True)
+     return Iterator_Interfaces.Forward_Iterator'Class;
+
+   function First
+     (Data         : aliased in Generate_Data;
+      Non_Grammar  :         in Boolean;
+      Nonterminals :         in Boolean)
+     return Token_Cursor;
+   procedure Next (Cursor : in out Token_Cursor; Nonterminals : in Boolean);
+
+   function ID (Cursor : in Token_Cursor) return Token_ID;
+
+   function Name (Cursor : in Token_Cursor) return String;
+   --  Return the token name from the .wy file:
+   --  Keywords: Keywords (i).name
+   --  Tokens  : Tokens (i).Tokens (j).name
+   --  Rules   : Rules (i).Left_Hand_Side
+
+   function Kind (Cursor : in Token_Cursor) return String;
+   --  Return the token kind from the .wy file:
+   --  Keywords: "keyword"
+   --  Tokens  : Tokens (i).Kind
+   --  Rules   : "nonterminal"
+
+   function Value (Cursor : in Token_Cursor) return String;
+   --  Return the token value from the .wy file:
+   --  Keywords: Keywords (i).value
+   --  Tokens  : Tokens (i).Tokens (j).Value
+   --  Rules   : "" - they have no Value
+
+   function To_Conflicts
+     (Data             : aliased in out Generate_Data;
+      Conflicts        :         in     WisiToken.BNF.Conflict_Lists.List;
+      Source_File_Name :         in     String)
+     return WisiToken.Generate.LR.Conflict_Lists.List;
+   --  Not included in Initialize because algorithms have no conflicts.
+
+   function To_Nonterminal_ID_Set
+     (Data : aliased in Generate_Data;
+      Item :         in String_Lists.List)
+     return Token_ID_Set;
+
+   function To_McKenzie_Param
+     (Data             : aliased in Generate_Data;
+      Item             :         in McKenzie_Recover_Param_Type;
+      Source_File_Name :         in String)
+     return WisiToken.Parse.LR.McKenzie_Param_Type;
+
+   procedure Count_Actions (Data : in out Generate_Utils.Generate_Data);
+
+   procedure Put_Stats
+     (Input_Data    : in WisiToken_Grammar_Runtime.User_Data_Type;
+      Generate_Data : in Generate_Utils.Generate_Data);
+
+   function Actions_Length (State : in Parse.LR.Parse_State) return Integer;
+   --  Not including Error.
+
+private
+
+   type Token_Cursor_Kind is
+     (Non_Grammar_Kind, Terminals_Keywords, Terminals_Others, EOI, 
WisiToken_Accept, Nonterminal, Done);
+
+   type Token_Cursor is record
+      Data        : not null access constant Generate_Data;
+      Kind        : Token_Cursor_Kind;
+      ID          : Token_ID;
+      Token_Kind  : WisiToken.BNF.Token_Lists.Cursor; -- Non_Grammar or 
Tokens, depending on Kind
+      Token_Item  : String_Pair_Lists.Cursor;
+      Keyword     : String_Pair_Lists.Cursor;
+      Nonterminal : Rule_Lists.Cursor;
+   end record;
+
+end WisiToken.BNF.Generate_Utils;
diff --git a/wisitoken-bnf-output_ada.adb b/wisitoken-bnf-output_ada.adb
new file mode 100644
index 0000000..ee24c7e
--- /dev/null
+++ b/wisitoken-bnf-output_ada.adb
@@ -0,0 +1,436 @@
+--  Abstract :
+--
+--  Output Ada code implementing the grammar defined by input
+--  parameters, and a parser for that grammar. The grammar parser
+--  actions must be Ada.
+--
+--  Copyright (C) 2017, 2018 Stephen Leake.  All Rights Reserved.
+--
+--  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 MERCHANTABILITY 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.Fixed;
+with Ada.Text_IO; use Ada.Text_IO;
+with GNAT.Regexp;
+with WisiToken.BNF.Generate_Packrat;
+with WisiToken.BNF.Generate_Utils;
+with WisiToken.BNF.Output_Ada_Common; use WisiToken.BNF.Output_Ada_Common;
+with WisiToken.Generate.Packrat;
+with WisiToken_Grammar_Runtime;
+procedure WisiToken.BNF.Output_Ada
+  (Input_Data            :         in WisiToken_Grammar_Runtime.User_Data_Type;
+   Output_File_Name_Root :         in String;
+   Generate_Data         : aliased in 
WisiToken.BNF.Generate_Utils.Generate_Data;
+   Packrat_Data          :         in WisiToken.Generate.Packrat.Data;
+   Tuple                 :         in Generate_Tuple;
+   Test_Main             :         in Boolean;
+   Multiple_Tuples       :         in Boolean)
+is
+   Common_Data : Output_Ada_Common.Common_Data := 
WisiToken.BNF.Output_Ada_Common.Initialize
+     (Input_Data, Tuple, Output_File_Name_Root, Check_Interface => False);
+
+   Gen_Alg_Name : constant String :=
+     (if Test_Main or Multiple_Tuples
+      then "_" & Generate_Algorithm_Image (Common_Data.Generate_Algorithm).all
+      else "");
+
+   function Symbol_Regexp (Item : in String) return String
+   is begin
+      --  Return a regular expression string that matches Item as a symbol;
+      --  it must be preceded and followed by non-symbol characters.
+      --
+      --  GNAT.Regexp does not have a char for 'end of string', so we hope
+      --  that doesn't occur. Sigh.
+      return ".*[ (\.]" & Item & "[ );\.,].*";
+   end Symbol_Regexp;
+
+   procedure Create_Ada_Actions_Body
+     (Action_Names : not null access WisiToken.Names_Array_Array;
+      Check_Names  : not null access WisiToken.Names_Array_Array;
+      Package_Name : in              String)
+   is
+      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);
+      Nonterm_Regexp   : constant Regexp := Compile (Symbol_Regexp 
("Nonterm"), Case_Sensitive   => False);
+      Tokens_Regexp    : constant Regexp := Compile (Symbol_Regexp ("Tokens"), 
Case_Sensitive    => False);
+
+      Body_File : File_Type;
+   begin
+      Create (Body_File, Out_File, File_Name);
+      Set_Output (Body_File);
+      Indent := 1;
+      Put_File_Header (Ada_Comment, Use_Tuple => True, Tuple => Tuple);
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Actions_Body_Context));
+      New_Line;
+
+      Put_Line ("package body " & Package_Name & " is");
+      Indent := Indent + 3;
+      New_Line;
+
+      if Input_Data.Check_Count > 0 then
+         Indent_Line ("use WisiToken.Semantic_Checks;");
+         New_Line;
+      end if;
+
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Actions_Body_Pre));
+
+      --  generate Action and Check subprograms.
+
+      for Rule of Input_Data.Tokens.Rules loop
+         --  No need for a Token_Cursor here, since we only need the
+         --  nonterminals.
+         declare
+            use Ada.Strings.Unbounded;
+
+            LHS_ID    : constant WisiToken.Token_ID := Find_Token_ID 
(Generate_Data, -Rule.Left_Hand_Side);
+            RHS_Index : Integer                     := 0;
+
+            function Is_Elisp (Action : in Unbounded_String) return Boolean
+            is begin
+               return Length (Action) >= 6 and then
+                 (Slice (Action, 1, 6) = "(progn" or
+                    Slice (Action, 1, 5) = "wisi-");
+            end Is_Elisp;
+
+         begin
+            for RHS of Rule.Right_Hand_Sides loop
+               if Length (RHS.Action) > 0 and then not Is_Elisp (RHS.Action) 
then
+                  declare
+                     Line : constant String := -RHS.Action;
+                     --  Actually multiple lines; we assume the formatting is 
adequate.
+
+                     Name : constant String := Action_Names 
(LHS_ID)(RHS_Index).all;
+
+                     Unref_User_Data : Boolean := True;
+                     Unref_Tree      : Boolean := True;
+                     Unref_Nonterm   : Boolean := True;
+                     Unref_Tokens    : Boolean := True;
+                     Need_Comma      : Boolean := False;
+
+                     procedure Check_Unref (Line : in String)
+                     is begin
+                        if Match (Line, User_Data_Regexp) then
+                           Unref_User_Data := False;
+                        end if;
+                        if Match (Line, Tree_Regexp) then
+                           Unref_Tree := False;
+                        end if;
+                        if Match (Line, Nonterm_Regexp) then
+                           Unref_Nonterm := False;
+                        end if;
+                        if Match (Line, Tokens_Regexp) then
+                           Unref_Tokens := False;
+                        end if;
+                     end Check_Unref;
+
+                  begin
+                     Check_Unref (Line);
+                     Indent_Line ("procedure " & Name);
+                     Indent_Line (" (User_Data : in out 
WisiToken.Syntax_Trees.User_Data_Type'Class;");
+                     Indent_Line ("  Tree      : in out 
WisiToken.Syntax_Trees.Tree;");
+                     Indent_Line ("  Nonterm   : in     
WisiToken.Syntax_Trees.Valid_Node_Index;");
+                     Indent_Line ("  Tokens    : in     
WisiToken.Syntax_Trees.Valid_Node_Index_Array)");
+                     Indent_Line ("is");
+
+                     if Unref_User_Data or Unref_Tree or Unref_Nonterm or 
Unref_Tokens then
+                        Indent_Start ("   pragma Unreferenced (");
+
+                        if Unref_User_Data then
+                           Put ((if Need_Comma then ", " else "") & 
"User_Data");
+                           Need_Comma := True;
+                        end if;
+                        if Unref_Tree then
+                           Put ((if Need_Comma then ", " else "") & "Tree");
+                           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;
+                        Put_Line (");");
+                     end if;
+
+                     Indent_Line ("begin");
+                     Indent := Indent + 3;
+
+                     Indent_Line (Line);
+                     Indent := Indent - 3;
+                     Indent_Line ("end " & Name & ";");
+                     New_Line;
+                  end;
+               end if;
+
+               if Length (RHS.Check) > 0 and then not Is_Elisp (RHS.Check) then
+                  declare
+                     use Ada.Strings.Fixed;
+                     Line          : constant String  := -RHS.Check;
+                     Name          : constant String  := Check_Names 
(LHS_ID)(RHS_Index).all;
+                     Unref_Lexer   : constant Boolean := 0 = Index (Line, 
"Lexer");
+                     Unref_Nonterm : constant Boolean := 0 = Index (Line, 
"Nonterm");
+                     Unref_Tokens  : constant Boolean := 0 = Index (Line, 
"Tokens");
+                  begin
+                     Indent_Line ("function " & Name);
+                     Indent_Line (" (Lexer   : access constant 
WisiToken.Lexer.Instance'Class;");
+                     Indent_Line ("  Nonterm : in out 
WisiToken.Recover_Token;");
+                     Indent_Line ("  Tokens  : in     
WisiToken.Recover_Token_Array)");
+                     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;
+
+                     Indent_Line ("begin");
+                     Indent := Indent + 3;
+                     Indent_Line (Line);
+                     Indent := Indent - 3;
+                     Indent_Line ("end " & Name & ";");
+                     New_Line;
+                  end;
+               end if;
+
+               RHS_Index := RHS_Index + 1;
+            end loop;
+         end;
+      end loop;
+
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Actions_Body_Post));
+
+      Put_Line ("end " & Package_Name & ";");
+      Close (Body_File);
+
+      Set_Output (Standard_Output);
+
+   end Create_Ada_Actions_Body;
+
+   procedure Create_Ada_Main_Body
+     (Actions_Package_Name : in String;
+      Main_Package_Name    : in String)
+   is
+      use WisiToken.Generate;
+
+      File_Name         : constant String := To_Lower (Main_Package_Name) & 
".adb";
+      re2c_Package_Name : constant String := -Common_Data.Lower_File_Name_Root 
& "_re2c_c";
+
+      Body_File : File_Type;
+   begin
+      Create (Body_File, Out_File, File_Name);
+      Set_Output (Body_File);
+      Indent := 1;
+
+      Put_File_Header (Ada_Comment, Use_Tuple => True, Tuple => Tuple);
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+
+      if (case Common_Data.Generate_Algorithm is
+          when LR_Generate_Algorithm => Input_Data.Action_Count > 0 or 
Input_Data.Check_Count > 0,
+          when Packrat_Generate_Algorithm | External => 
Input_Data.Action_Count > 0)
+      then
+         Put_Line ("with " & Actions_Package_Name & "; use " & 
Actions_Package_Name & ";");
+      end if;
+
+      case Common_Data.Lexer is
+      when None | Elisp_Lexer =>
+         null;
+
+      when re2c_Lexer =>
+         Put_Line ("with WisiToken.Lexer.re2c;");
+         Put_Line ("with " & re2c_Package_Name & ";");
+      end case;
+
+      case Common_Data.Generate_Algorithm is
+      when LR_Generate_Algorithm =>
+         if Tuple.Text_Rep then
+            Put_Line ("with WisiToken.Productions;");
+         end if;
+
+      when Packrat_Gen =>
+         Put_Line ("with WisiToken.Parse.Packrat.Generated;");
+
+      when Packrat_Proc =>
+         Put_Line ("with WisiToken.Parse.Packrat.Procedural;");
+         Put_Line ("with WisiToken.Productions;");
+
+      when External =>
+         null;
+      end case;
+
+      Put_Line ("package body " & Main_Package_Name & " is");
+      Indent := Indent + 3;
+      New_Line;
+
+      case Common_Data.Lexer is
+      when None | Elisp_Lexer =>
+         null;
+
+      when re2c_Lexer =>
+         Indent_Line ("package Lexer is new WisiToken.Lexer.re2c");
+         Indent_Line ("  (" & re2c_Package_Name & ".New_Lexer,");
+         Indent_Line ("   " & re2c_Package_Name & ".Free_Lexer,");
+         Indent_Line ("   " & re2c_Package_Name & ".Reset_Lexer,");
+         Indent_Line ("   " & re2c_Package_Name & ".Next_Token);");
+         New_Line;
+      end case;
+
+      case Common_Data.Generate_Algorithm is
+      when LR_Generate_Algorithm =>
+         LR_Create_Create_Parser (Input_Data, Common_Data, Generate_Data);
+
+      when Packrat_Gen =>
+         WisiToken.BNF.Generate_Packrat (Packrat_Data, Generate_Data);
+
+         Packrat_Create_Create_Parser (Common_Data, Generate_Data, 
Packrat_Data);
+
+      when Packrat_Proc =>
+         Packrat_Create_Create_Parser (Common_Data, Generate_Data, 
Packrat_Data);
+
+      when External =>
+         External_Create_Create_Grammar (Generate_Data);
+      end case;
+
+      Put_Line ("end " & Main_Package_Name & ";");
+      Close (Body_File);
+      Set_Output (Standard_Output);
+   end Create_Ada_Main_Body;
+
+   procedure Create_Ada_Test_Main
+     (Actions_Package_Name : in String;
+      Main_Package_Name    : in String)
+   is
+      use WisiToken.Generate;
+
+      Generic_Package_Name : constant String :=
+        (case Common_Data.Generate_Algorithm is
+         when LR_Generate_Algorithm =>
+           (if Input_Data.Language_Params.Error_Recover then
+              (if Common_Data.Text_Rep
+               then "Gen_LR_Text_Rep_Parser_Run"
+               else "Gen_LR_Parser_Run")
+            else
+              (if Common_Data.Text_Rep
+               then "Gen_LR_Text_Rep_Parser_No_Recover_Run"
+               else "Gen_LR_Parser_No_Recover_Run")),
+
+         when Packrat_Generate_Algorithm => "Gen_Packrat_Parser_Run",
+         when External => raise SAL.Programmer_Error);
+
+      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
+        (Output_File_Name_Root);
+
+      File_Name : constant String := To_Lower (Unit_Name) & ".ads";
+
+      File : File_Type;
+   begin
+      Create (File, Out_File, File_Name);
+      Set_Output (File);
+      Indent := 1;
+
+      Put_File_Header (Ada_Comment, Use_Tuple => True, Tuple => Tuple);
+      --  no Copyright_License; just a test file
+      New_Line;
+
+      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 & ";");
+      end if;
+
+      Put_Line ("procedure " & Unit_Name & " is new " & Generic_Package_Name);
+      Put_Line ("  (" & Actions_Package_Name & ".Descriptor,");
+      if Common_Data.Text_Rep then
+         Put_Line ("   """ & Output_File_Name_Root & "_" &
+                     To_Lower (Generate_Algorithm_Image (Tuple.Gen_Alg).all) &
+                     "_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,");
+      end if;
+      Put_Line (Main_Package_Name & ".Create_Parser);");
+      Close (File);
+      Set_Output (Standard_Output);
+   end Create_Ada_Test_Main;
+
+begin
+   case Common_Data.Lexer is
+   when None | re2c_Lexer =>
+      null;
+
+   when Elisp_Lexer =>
+      raise User_Error with WisiToken.Generate.Error_Message
+        (Input_Data.Grammar_Lexer.File_Name, 1, "Ada output language does not 
support " & Lexer_Image
+           (Common_Data.Lexer).all & " lexer");
+   end case;
+
+   case Tuple.Interface_Kind is
+   when None  =>
+      null;
+
+   when Module | Process =>
+      raise User_Error with WisiToken.Generate.Error_Message
+        (Input_Data.Grammar_Lexer.File_Name, 1, "Ada output language does not 
support setting Interface");
+   end case;
+
+   declare
+      Main_Package_Name    : constant String := File_Name_To_Ada 
(Output_File_Name_Root & Gen_Alg_Name) & "_Main";
+      Actions_Package_Name : constant String := File_Name_To_Ada 
(Output_File_Name_Root) & "_Actions";
+   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);
+      end if;
+
+      Create_Ada_Actions_Spec
+        (Output_File_Name_Root & "_actions.ads", Actions_Package_Name, 
Input_Data, Common_Data, Generate_Data);
+
+      if Tuple.Gen_Alg = External then
+         Create_External_Main_Spec (Main_Package_Name, Tuple, Input_Data);
+         Create_Ada_Main_Body (Actions_Package_Name, Main_Package_Name);
+      else
+         Create_Ada_Main_Body (Actions_Package_Name, Main_Package_Name);
+
+         Create_Ada_Main_Spec (To_Lower (Main_Package_Name) & ".ads", 
Main_Package_Name, Input_Data, Common_Data);
+
+         if Test_Main then
+            Create_Ada_Test_Main (Actions_Package_Name, Main_Package_Name);
+         end if;
+      end if;
+   end;
+
+exception
+when others =>
+   Set_Output (Standard_Output);
+   raise;
+end WisiToken.BNF.Output_Ada;
diff --git a/wisitoken-bnf-output_ada_common.adb 
b/wisitoken-bnf-output_ada_common.adb
new file mode 100644
index 0000000..3267ebb
--- /dev/null
+++ b/wisitoken-bnf-output_ada_common.adb
@@ -0,0 +1,1407 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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 (GPL);
+
+with Ada.Strings.Fixed;
+with Ada.Text_IO; use Ada.Text_IO;
+with System.Multiprocessors;
+with WisiToken.BNF.Generate_Grammar;
+with WisiToken.BNF.Utils;
+with WisiToken.Generate; use WisiToken.Generate;
+with WisiToken.Parse.LR;
+with WisiToken.Productions;
+with WisiToken.Syntax_Trees;
+package body WisiToken.BNF.Output_Ada_Common is
+
+   --  Body subprograms, alphabetical
+
+   function Duplicate_Reduce (State : in Parse.LR.Parse_State) return Boolean
+   is
+      use Parse.LR;
+      Node        : Action_Node_Ptr       := State.Action_List;
+      Action_Node : Parse_Action_Node_Ptr := Node.Action;
+      First       : Boolean               := True;
+      Action      : Reduce_Action_Rec;
+   begin
+      loop
+         Action_Node := Node.Action;
+         if Action_Node.Next /= null then
+            --  conflict
+            return False;
+         elsif Action_Node.Item.Verb /= Reduce then
+            return False;
+         end if;
+
+         if First then
+            Action := Action_Node.Item;
+            First  := False;
+         else
+            if not Equal (Action, Action_Node.Item) then
+               return False;
+            end if;
+         end if;
+         Node := Node.Next;
+         exit when Node.Next = null; --  Last entry is Error.
+      end loop;
+      return True;
+   end Duplicate_Reduce;
+
+   function Symbols_Image (State : in Parse.LR.Parse_State) return String
+   is
+      use Ada.Strings.Unbounded;
+      use Parse.LR;
+
+      Result     : Unbounded_String;
+      Need_Comma : Boolean          := False;
+      Node       : Action_Node_Ptr  := State.Action_List;
+   begin
+      if Generate_Utils.Actions_Length (State) = 1 then
+         return "(1 => " & Token_ID'Image (Node.Symbol) & ")";
+      else
+         Result := +"(";
+         loop
+            Result := Result &
+              (if Need_Comma then ", " else "") &
+              Trimmed_Image (Node.Symbol);
+            Need_Comma := True;
+            Node := Node.Next;
+            exit when Node.Next = null; -- last is Error
+         end loop;
+         Result := Result & ")";
+         return -Result;
+      end if;
+   end Symbols_Image;
+
+   ----------
+   --  Public subprograms in alphabetical order
+
+   procedure Create_Ada_Actions_Spec
+     (Output_File_Name :         in String;
+      Package_Name     :         in String;
+      Input_Data       :         in WisiToken_Grammar_Runtime.User_Data_Type;
+      Common_Data      :         in Output_Ada_Common.Common_Data;
+      Generate_Data    : aliased in WisiToken.BNF.Generate_Utils.Generate_Data)
+   is
+      use Generate_Utils;
+
+      Descriptor  : WisiToken.Descriptor renames Generate_Data.Descriptor.all;
+      Spec_File : File_Type;
+      Paren_Done  : Boolean      := False;
+      Cursor      : Token_Cursor := First (Generate_Data, Non_Grammar => True, 
Nonterminals => True);
+   begin
+      Create (Spec_File, Out_File, Output_File_Name);
+      Set_Output (Spec_File);
+      Indent := 1;
+
+      Put_File_Header
+        (Ada_Comment, Use_Tuple => True, Tuple =>
+           (Common_Data.Generate_Algorithm, Common_Data.Output_Language, 
Common_Data.Lexer, Common_Data.Interface_Kind,
+            Common_Data.Text_Rep));
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+
+      if not (Input_Data.Action_Count > 0 or Input_Data.Check_Count > 0) then
+         Put_Line ("with WisiToken;");
+      end if;
+      if Input_Data.Action_Count > 0 then
+         Put_Line ("with WisiToken.Syntax_Trees;");
+      end if;
+      if Input_Data.Check_Count > 0 then
+         Put_Line ("with WisiToken.Lexer;");
+         Put_Line ("with WisiToken.Semantic_Checks;");
+      end if;
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Actions_Spec_Context));
+      Put_Line ("package " & Package_Name & " is");
+      Indent := Indent + 3;
+      New_Line;
+
+      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 := 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 ("EOF_ID                        =>" & 
WisiToken.Token_ID'Image (Descriptor.EOF_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_Start ("  (");
+      Indent := Indent + 3;
+      loop
+         exit when Is_Done (Cursor);
+         if Paren_Done then
+            Indent_Start ("new String'(""" & (Name (Cursor)));
+         else
+            Put ("new String'(""" & (Name (Cursor)));
+            Paren_Done := True;
+         end if;
+         Next (Cursor, Nonterminals => True);
+         if Is_Done (Cursor) then
+            Put_Line (""")),");
+         else
+            Put_Line ("""),");
+         end if;
+      end loop;
+
+      Indent := Indent - 3;
+      Indent_Line ("Terminal_Image_Width =>" & Integer'Image 
(Descriptor.Terminal_Image_Width) & ",");
+      Indent_Line ("Image_Width          =>" & Integer'Image 
(Descriptor.Image_Width) & ",");
+      Indent_Line ("Last_Lookahead       =>" & WisiToken.Token_ID'Image 
(Descriptor.Last_Lookahead) & ");");
+      Indent := Indent - 3;
+      New_Line;
+
+      if Input_Data.Language_Params.Declare_Enums then
+         Paren_Done := False;
+
+         Cursor := First (Generate_Data, Non_Grammar => True, Nonterminals => 
True);
+         Indent_Line ("type Token_Enum_ID is");
+         Indent_Start ("  (");
+         Indent := Indent + 3;
+         loop
+            exit when Is_Done (Cursor);
+            if Paren_Done then
+               Indent_Start (To_Token_Ada_Name (Name (Cursor)));
+            else
+               Put (To_Token_Ada_Name (Name (Cursor)));
+               Paren_Done := True;
+            end if;
+            Next (Cursor, Nonterminals => True);
+            if Is_Done (Cursor) then
+               Put_Line (");");
+            else
+               Put_Line (",");
+            end if;
+         end loop;
+
+         Indent := Indent - 3;
+         New_Line;
+
+         Indent_Line ("type Token_Enum_ID_Array is array (Positive range <>) 
of Token_Enum_ID;");
+         Indent_Line ("use all type WisiToken.Token_ID;");
+         Indent_Line ("function ""+"" (Item : in Token_Enum_ID) return 
WisiToken.Token_ID");
+         Indent_Line ("  is (WisiToken.Token_ID'First + Token_Enum_ID'Pos 
(Item));");
+
+         Indent_Line ("function To_Token_Enum (Item : in WisiToken.Token_ID) 
return Token_Enum_ID");
+         Indent_Line ("  is (Token_Enum_ID'Val (Item - 
WisiToken.Token_ID'First));");
+         Indent_Line ("function ""-"" (Item : in WisiToken.Token_ID) return 
Token_Enum_ID renames To_Token_Enum;");
+         New_Line;
+
+      end if;
+
+      for Name_List of Generate_Data.Action_Names.all loop
+         if Name_List /= null then
+            for Name of Name_List.all loop
+               if Name /= null then
+                  Indent_Line ("procedure " & Name.all);
+                  Indent_Line (" (User_Data : in out 
WisiToken.Syntax_Trees.User_Data_Type'Class;");
+                  Indent_Line ("  Tree      : in out 
WisiToken.Syntax_Trees.Tree;");
+                  Indent_Line ("  Nonterm   : in     
WisiToken.Syntax_Trees.Valid_Node_Index;");
+                  Indent_Line ("  Tokens    : in     
WisiToken.Syntax_Trees.Valid_Node_Index_Array);");
+               end if;
+            end loop;
+         end if;
+      end loop;
+
+      for Name_List of Generate_Data.Check_Names.all loop
+         if Name_List /= null then
+            for Name of Name_List.all loop
+               if Name /= null then
+                  Indent_Line ("function " & Name.all);
+                  Indent_Line (" (Lexer   : access constant 
WisiToken.Lexer.Instance'Class;");
+                  Indent_Line ("  Nonterm : in out WisiToken.Recover_Token;");
+                  Indent_Line ("  Tokens  : in     
WisiToken.Recover_Token_Array)");
+                  Indent_Line (" return 
WisiToken.Semantic_Checks.Check_Status;");
+               end if;
+            end loop;
+         end if;
+      end loop;
+
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Actions_Spec_Post));
+
+      Put_Line ("end " & Package_Name & ";");
+      Close (Spec_File);
+      Set_Output (Standard_Output);
+
+   end Create_Ada_Actions_Spec;
+
+   procedure Create_Ada_Main_Spec
+     (Output_File_Name  : in String;
+      Main_Package_Name : in String;
+      Input_Data        : in WisiToken_Grammar_Runtime.User_Data_Type;
+      Common_Data       : in Output_Ada_Common.Common_Data)
+   is
+      Lower_Package_Name : constant String := To_Lower (Main_Package_Name);
+
+      Spec_File : File_Type;
+
+      procedure LR_Process
+      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;");
+         else
+            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");
+
+         if Common_Data.Text_Rep then
+            Put_Line (";");
+            Indent_Line ("   Text_Rep_File_Name : in String);");
+         else
+            Put_Line (");");
+         end if;
+         New_Line;
+      end LR_Process;
+
+      procedure Packrat_Process
+      is begin
+         Indent_Line ("function Create_Parser");
+         Indent_Line ("  (Trace     : not null access WisiToken.Trace'Class;");
+         Indent_Line ("   User_Data : in     
WisiToken.Syntax_Trees.User_Data_Access)");
+         Indent_Line ("  return WisiToken.Parse.Base_Parser'Class;");
+         New_Line;
+      end Packrat_Process;
+
+   begin
+      if Common_Data.Generate_Algorithm = External then
+         raise SAL.Programmer_Error;
+      end if;
+
+      Create (Spec_File, Out_File, Output_File_Name);
+      Set_Output (Spec_File);
+      Indent := 1;
+
+      Put_File_Header
+        (Ada_Comment, Use_Tuple => True, Tuple =>
+           (Common_Data.Generate_Algorithm, Common_Data.Output_Language, 
Common_Data.Lexer, Common_Data.Interface_Kind,
+            Common_Data.Text_Rep));
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+
+      case Common_Data.Output_Language is
+      when Ada_Lang =>
+         Put_Line ("with WisiToken.Syntax_Trees;");
+
+      when Ada_Emacs_Lang =>
+         case Common_Data.Interface_Kind is
+         when Process =>
+            Put_Line ("with WisiToken.Syntax_Trees;");
+
+         when Module =>
+            Put_Line ("with Emacs_Module_Aux;");
+            Put_Line ("with emacs_module_h;");
+            Put_Line ("with Interfaces.C;");
+            Put_Line ("with WisiToken.Semantic_State;");
+         end case;
+      end case;
+
+      case Common_Data.Generate_Algorithm is
+      when LR_Generate_Algorithm =>
+         if Input_Data.Language_Params.Error_Recover then
+            Put_Line ("with WisiToken.Parse.LR.Parser;");
+         else
+            Put_Line ("with WisiToken.Parse.LR.Parser_No_Recover;");
+         end if;
+
+      when Packrat_Generate_Algorithm =>
+         Put_Line ("with WisiToken.Parse;");
+
+      when External =>
+         null;
+      end case;
+
+      Put_Line ("package " & Main_Package_Name & " is");
+      Indent := Indent + 3;
+      New_Line;
+
+      case Common_Data.Output_Language is
+      when Ada_Lang =>
+         case Common_Data.Generate_Algorithm is
+         when LR_Generate_Algorithm =>
+            LR_Process;
+         when Packrat_Generate_Algorithm =>
+            Packrat_Process;
+         when External =>
+            null;
+         end case;
+
+      when Ada_Emacs_Lang =>
+         case Common_Data.Interface_Kind is
+         when Process =>
+            case Common_Data.Generate_Algorithm is
+            when LR_Generate_Algorithm =>
+               LR_Process;
+            when Packrat_Generate_Algorithm =>
+               Packrat_Process;
+            when External =>
+               null;
+            end case;
+
+         when Module =>
+            Indent_Line ("function Parse (Env : 
Emacs_Module_Aux.Emacs_Env_Access) return emacs_module_h.emacs_value;");
+            Indent_Line ("pragma Export (C, Parse, """ & Lower_Package_Name & 
"_wisi_module_parse"");");
+            Indent_Line ("function Init (Env : 
Emacs_Module_Aux.Emacs_Env_Access) return Interfaces.C.int;");
+            Indent_Line ("pragma Export (C, Init, """ & Lower_Package_Name & 
"_wisi_module_parse_init"");");
+            New_Line;
+
+         end case;
+      end case;
+
+      Put_Line ("end " & Main_Package_Name & ";");
+      Close (Spec_File);
+      Set_Output (Standard_Output);
+   end Create_Ada_Main_Spec;
+
+   procedure Create_External_Main_Spec
+     (Main_Package_Name    : in String;
+      Tuple                : in Generate_Tuple;
+      Input_Data           : in WisiToken_Grammar_Runtime.User_Data_Type)
+   is
+      File_Name : constant String := To_Lower (Main_Package_Name) & ".ads";
+      Spec_File : File_Type;
+   begin
+      Create (Spec_File, Out_File, File_Name);
+      Set_Output (Spec_File);
+      Indent := 1;
+
+      Put_File_Header (Ada_Comment, Use_Tuple => True, Tuple => Tuple);
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+
+      Put_Line ("with WisiToken.Productions;");
+      Put_Line ("package " & Main_Package_Name & " is");
+      Indent := Indent + 3;
+      New_Line;
+
+      Indent_Line ("function Create_Grammar return 
WisiToken.Productions.Prod_Arrays.Vector;");
+
+      Indent := Indent - 3;
+      Put_Line ("end " & Main_Package_Name & ";");
+      Close (Spec_File);
+      Set_Output (Standard_Output);
+   end Create_External_Main_Spec;
+
+   procedure Create_LR_Parser_Core_1
+     (Common_Data   : in Output_Ada_Common.Common_Data;
+      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;
+
+      Table : WisiToken.Parse.LR.Parse_Table_Ptr renames 
Generate_Data.LR_Parse_Table;
+      Line  : Unbounded_String;
+
+      procedure Append (Item : in String)
+      is begin
+         Line := Line & Item;
+      end Append;
+
+      procedure Put (Label : in String; Item : in Token_ID_Array_Natural)
+      is begin
+         Indent_Line (Label & " =>");
+         Indent_Start ("  (");
+         Indent := Indent + 3;
+         Line := +"";
+         for I in Item'Range loop
+            Append (Trimmed_Image (Item (I)));
+
+            if I = Item'Last then
+               Append ("),");
+
+            else
+               Append (", ");
+            end if;
+         end loop;
+         Indent_Wrap (-Line);
+         Indent := Indent - 3;
+      end Put;
+
+   begin
+      Indent_Line ("McKenzie_Param : constant McKenzie_Param_Type :=");
+      Indent_Line ("  (First_Terminal    =>" & Token_ID'Image 
(Table.McKenzie_Param.First_Terminal) & ",");
+      Indent := Indent + 3;
+      Indent_Line ("Last_Terminal     =>" & Token_ID'Image 
(Table.McKenzie_Param.Last_Terminal) & ",");
+      Indent_Line ("First_Nonterminal =>" & Token_ID'Image 
(Table.McKenzie_Param.First_Nonterminal) & ",");
+      Indent_Line ("Last_Nonterminal  =>" & Token_ID'Image 
(Table.McKenzie_Param.Last_Nonterminal) & ",");
+      Put ("Insert", Table.McKenzie_Param.Insert);
+      Put ("Delete", Table.McKenzie_Param.Delete);
+      Put ("Push_Back", Table.McKenzie_Param.Push_Back);
+      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) & ");");
+      Indent := Indent - 3;
+      New_Line;
+
+      if Common_Data.Text_Rep then
+         Indent_Line ("function Productions return 
WisiToken.Productions.Prod_Arrays.Vector");
+         Indent_Line ("is begin");
+         Indent := Indent + 3;
+         Indent_Line ("return Prods : WisiToken.Productions.Prod_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) & ");");
+
+         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;
+                     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;
+         end loop;
+         Indent := Indent - 3;
+         Indent_Line ("end return;");
+         Indent := Indent - 3;
+         Indent_Line ("end Productions;");
+         New_Line;
+      end if;
+   end Create_LR_Parser_Core_1;
+
+   procedure Create_LR_Parser_Table
+     (Input_Data    : in WisiToken_Grammar_Runtime.User_Data_Type;
+      Generate_Data : in WisiToken.BNF.Generate_Utils.Generate_Data)
+   is
+      use all type Ada.Containers.Count_Type;
+      use Ada.Strings.Unbounded;
+
+      Table            : WisiToken.Parse.LR.Parse_Table_Ptr renames 
Generate_Data.LR_Parse_Table;
+      Lines_Per_Subr   : constant := 1000;
+      Subr_Count       : Integer  := 1;
+      Last_Subr_Closed : Boolean  := False;
+      Line             : Unbounded_String;
+
+      procedure Append (Item : in String)
+      is begin
+         Line := Line & Item;
+      end Append;
+   begin
+      --  Optimize source structure for GNAT compile time; one subroutine
+      --  with thousands of "Table.States (*) := ..." takes forever to
+      --  compile (apparently depending on available memory). But hundreds
+      --  of subroutines, containing the same lines in chunks of 1000,
+      --  compiles in acceptable time.
+
+      Indent_Line ("declare");
+      Indent := Indent + 3;
+
+      Indent_Line ("procedure Subr_" & Trimmed_Image (Subr_Count));
+      Indent_Line ("is begin");
+      Indent     := Indent + 3;
+      Line_Count := 0;
+
+      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
+            if Duplicate_Reduce (Table.States (State_Index)) then
+               declare
+                  Action : constant Reduce_Action_Rec := Node.Action.Item;
+               begin
+                  Set_Col (Indent);
+                  Line := +"Add_Action (Table.States (" & Trimmed_Image 
(State_Index) & "), " &
+                    Symbols_Image (Table.States (State_Index)) & ", " &
+                    Image (Action.Production) & "," &
+                    Count_Type'Image (Action.Token_Count) & ", ";
+
+                  Append
+                    ((if Generate_Data.Action_Names (Action.Production.LHS) = 
null then "null"
+                      elsif Generate_Data.Action_Names
+                        (Action.Production.LHS)(Action.Production.RHS) = null 
then "null"
+                      else Generate_Data.Action_Names
+                        (Action.Production.LHS)(Action.Production.RHS).all & 
"'Access"));
+                  Append (", ");
+                  Append
+                    ((if Generate_Data.Check_Names (Action.Production.LHS) = 
null then "null"
+                      elsif Generate_Data.Check_Names
+                        (Action.Production.LHS)(Action.Production.RHS) = null 
then "null"
+                      else Generate_Data.Check_Names
+                        (Action.Production.LHS)(Action.Production.RHS).all & 
"'Access"));
+
+                  Indent_Wrap (-Line & ");");
+                  Line_Count := Line_Count + 1;
+                  Indent     := Base_Indent;
+               end;
+
+            else
+               loop
+                  exit when Node = null;
+                  Set_Col (Indent);
+                  declare
+                     Action_Node : Parse_Action_Node_Ptr := Node.Action;
+                  begin
+                     case Action_Node.Item.Verb is
+                     when Shift =>
+                        Line := +"Add_Action (Table.States (" & Trimmed_Image 
(State_Index) & "), " &
+                          Trimmed_Image (Node.Symbol);
+                        Append (", ");
+                        Append (Trimmed_Image (Action_Node.Item.State));
+
+                     when Reduce | Accept_It =>
+                        Line := +"Add_Action (Table.States (" & Trimmed_Image 
(State_Index) & "), " &
+                          Trimmed_Image (Node.Symbol);
+                        if Action_Node.Item.Verb = Reduce then
+                           Append (", Reduce");
+                        else
+                           Append (", Accept_It");
+                        end if;
+                        Append (", ");
+                        Append (Image (Action_Node.Item.Production) & ",");
+                        Append (Count_Type'Image 
(Action_Node.Item.Token_Count) & ", ");
+                        Append
+                          ((if Generate_Data.Action_Names 
(Action_Node.Item.Production.LHS) = null then "null"
+                            elsif Generate_Data.Action_Names
+                              
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS) = null
+                            then "null"
+                            else Generate_Data.Action_Names
+                              
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS).all &
+                               "'Access"));
+                        Append (", ");
+                        Append
+                          ((if Generate_Data.Check_Names 
(Action_Node.Item.Production.LHS) = null then "null"
+                            elsif Generate_Data.Check_Names
+                              
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS) = null
+                            then "null"
+                            else Generate_Data.Check_Names
+                              
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS).all &
+                               "'Access"));
+
+                     when Parse.LR.Error =>
+                        Line := +"Add_Error (Table.States (" & Trimmed_Image 
(State_Index) & ")";
+                     end case;
+
+                     Action_Node := Action_Node.Next;
+                     if Action_Node /= null then
+                        --  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 (", ");
+                           Append (Image (Action_Node.Item.Production) & ",");
+                           Append (Count_Type'Image 
(Action_Node.Item.Token_Count) & ", ");
+                           Append
+                             ((if Generate_Data.Action_Names 
(Action_Node.Item.Production.LHS) = null then "null"
+                               elsif Generate_Data.Action_Names
+                                 
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS) = null
+                               then "null"
+                               else Generate_Data.Action_Names
+                                 
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS).all &
+                                  "'Access"));
+                           Append (", ");
+                           Append
+                             ((if Generate_Data.Check_Names 
(Action_Node.Item.Production.LHS) = null then "null"
+                               elsif Generate_Data.Check_Names
+                                 
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS) = null
+                               then "null"
+                               else Generate_Data.Check_Names
+                                 
(Action_Node.Item.Production.LHS)(Action_Node.Item.Production.RHS).all &
+                                  "'Access"));
+
+                        when others =>
+                           raise SAL.Programmer_Error with "conflict second 
action verb: " &
+                             Parse.LR.Parse_Action_Verbs'Image 
(Action_Node.Item.Verb);
+                        end case;
+                     end if;
+                  end;
+                  Indent_Wrap (-Line & ");");
+                  Line_Count := Line_Count + 1;
+                  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
+               exit when Node = null;
+               Set_Col (Indent);
+               Put ("Add_Goto (Table.States (" & Trimmed_Image (State_Index) & 
"), ");
+               Put_Line (Trimmed_Image (Symbol (Node)) & ", " & Trimmed_Image 
(State (Node)) & ");");
+               Line_Count := Line_Count + 1;
+               Node := Next (Node);
+            end loop;
+         end Gotos;
+
+         if Table.States (State_Index).Minimal_Complete_Actions.Length > 0 then
+            Indent_Wrap
+              ("Set_Minimal_Action (Table.States (" & Trimmed_Image 
(State_Index) & ").Minimal_Complete_Actions, " &
+                 WisiToken.Parse.LR.Image (Table.States 
(State_Index).Minimal_Complete_Actions, Strict => True) & ");");
+         end if;
+
+         if Line_Count > Lines_Per_Subr then
+            Line_Count := 0;
+            Indent := Indent - 3;
+            Indent_Line ("end Subr_" & Trimmed_Image (Subr_Count) & ";");
+
+            if State_Index < Table.States'Last then
+               Subr_Count := Subr_Count + 1;
+               Last_Subr_Closed := False;
+               Indent_Line ("procedure Subr_" & Trimmed_Image (Subr_Count));
+               Indent_Line ("is begin");
+               Indent := Indent + 3;
+            else
+               Last_Subr_Closed := True;
+            end if;
+         end if;
+
+      end loop Declare_Subroutines;
+
+      if not Last_Subr_Closed then
+         Indent := Indent - 3;
+         Indent_Line ("end Subr_" & Trimmed_Image (Subr_Count) & ";");
+      end if;
+
+      Indent := Indent - 3;
+      Indent_Line ("begin");
+      Indent := Indent + 3;
+
+      for Subr in 1 .. Subr_Count loop
+         Indent_Line ("Subr_" & Trimmed_Image (Subr) & ";");
+      end loop;
+      Indent := Indent - 3;
+      Indent_Line ("end;");
+   end Create_LR_Parser_Table;
+
+   procedure LR_Create_Create_Parser
+     (Input_Data    :         in     WisiToken_Grammar_Runtime.User_Data_Type;
+      Common_Data   :         in out Output_Ada_Common.Common_Data;
+      Generate_Data : aliased in     
WisiToken.BNF.Generate_Utils.Generate_Data)
+   is
+      Table : WisiToken.Parse.LR.Parse_Table_Ptr renames 
Generate_Data.LR_Parse_Table;
+   begin
+      Indent_Line ("procedure Create_Parser");
+      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
+              ("   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;
+         Indent_Line ("   Trace                        : not null access 
WisiToken.Trace'Class;");
+         Indent_Start ("   User_Data                    : in     
WisiToken.Syntax_Trees.User_Data_Access");
+
+      when Module =>
+         Indent_Line ("  (Parser              :    out 
WisiToken.Parse.LR.Parser.Parser;");
+         Indent_Line ("   Env                 : in     Emacs_Env_Access;");
+         Indent_Start ("   Lexer_Elisp_Symbols : in     
Lexers.Elisp_Array_Emacs_Value");
+      end case;
+
+      if Common_Data.Text_Rep then
+         Put_Line (";");
+         Indent_Line ("   Text_Rep_File_Name : in String)");
+      else
+         Put_Line (")");
+      end if;
+
+      Indent_Line ("is");
+      Indent := Indent + 3;
+
+      Indent_Line ("use WisiToken.Parse.LR;");
+
+      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 := Indent - 3;
+         Indent_Line ("begin");
+         Indent := Indent + 3;
+
+      else
+         if Input_Data.Language_Params.Error_Recover then
+            Create_LR_Parser_Core_1 (Common_Data, Generate_Data);
+         end if;
+
+         Indent_Line ("Table : constant Parse_Table_Ptr := new Parse_Table");
+         Indent_Line ("  (State_First       => 0,");
+         Indent := Indent + 3;
+         Indent_Line ("State_Last        =>" & State_Index'Image 
(Table.State_Last) & ",");
+         Indent_Line ("First_Terminal    =>" & Token_ID'Image 
(Table.First_Terminal) & ",");
+         Indent_Line ("Last_Terminal     =>" & Token_ID'Image 
(Table.Last_Terminal) & ",");
+         Indent_Line ("First_Nonterminal =>" & Token_ID'Image 
(Table.First_Nonterminal) & ",");
+         Indent_Line ("Last_Nonterminal  =>" & Token_ID'Image 
(Table.Last_Nonterminal) & ");");
+         Indent := Indent - 3;
+
+         Indent := Indent - 3;
+         Indent_Line ("begin");
+         Indent := Indent + 3;
+         if Input_Data.Language_Params.Error_Recover then
+            Indent_Line ("Table.McKenzie_Param := McKenzie_Param;");
+         end if;
+         Create_LR_Parser_Table (Input_Data, Generate_Data);
+         New_Line;
+      end if;
+
+      if Input_Data.Language_Params.Error_Recover then
+         Indent_Line ("WisiToken.Parse.LR.Parser.New_Parser");
+      else
+         Indent_Line ("WisiToken.Parse.LR.Parser_No_Recover.New_Parser");
+      end if;
+      Indent_Line ("  (Parser,");
+      case Common_Data.Interface_Kind is
+      when Process =>
+         Indent_Line ("   Trace,");
+         Indent_Line ("   Lexer.New_Lexer (Trace),");
+         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_String_ID_Set,");
+         end if;
+         Indent_Line ("   User_Data,");
+         Indent_Line ("   Max_Parallel         => 15,");
+         Indent_Line ("   Terminate_Same_State => True);");
+
+      when Module =>
+         Indent_Line ("   Lexer.New_Lexer (Env, Lexer_Elisp_Symbols),");
+         Indent_Line ("   Table, Max_Parallel => 15, Terminate_Same_State => 
True);");
+
+      end case;
+      Indent := Indent - 3;
+      Indent_Line ("end Create_Parser;");
+   end LR_Create_Create_Parser;
+
+   procedure Packrat_Create_Create_Parser
+     (Common_Data   :         in out Output_Ada_Common.Common_Data;
+      Generate_Data : aliased in     
WisiToken.BNF.Generate_Utils.Generate_Data;
+      Packrat_Data  :         in     WisiToken.Generate.Packrat.Data)
+   is
+      use Ada.Strings.Unbounded;
+
+      Text     : Unbounded_String;
+      Need_Bar : Boolean := True;
+   begin
+      Indent_Line ("function Create_Parser");
+      Indent_Line ("  (Trace     : not null access WisiToken.Trace'Class;");
+      Indent_Line ("   User_Data : in     
WisiToken.Syntax_Trees.User_Data_Access)");
+      Indent_Line ("  return WisiToken.Parse.Base_Parser'Class");
+
+      case Packrat_Generate_Algorithm'(Common_Data.Generate_Algorithm) is
+      when Packrat_Gen =>
+         Indent_Line ("is begin");
+         Indent := Indent + 3;
+         Indent_Line ("return Parser : 
WisiToken.Parse.Packrat.Generated.Parser do");
+         Indent := Indent + 3;
+         Indent_Line ("Parser.Trace := Trace;");
+         Indent_Line ("Parser.Lexer := Lexer.New_Lexer (Trace);");
+         Indent_Line ("Parser.User_Data := User_Data;");
+         Indent_Line ("Parser.Parse_WisiToken_Accept := 
Parse_wisitoken_accept_1'Access;");
+         Indent := Indent - 3;
+         Indent_Line ("end return;");
+
+      when Packrat_Proc =>
+         Indent_Line ("is");
+         Indent := Indent + 3;
+         Indent_Line ("use WisiToken;");
+         Indent_Line ("use WisiToken.Productions;");
+         Indent_Line ("Grammar               : Prod_Arrays.Vector;");
+         Indent_Line
+           ("Direct_Left_Recursive : constant WisiToken.Token_ID_Set (" &
+              Trimmed_Image (Generate_Data.Grammar.First_Index) & " .. " &
+              Trimmed_Image (Generate_Data.Grammar.Last_Index) & ") :=");
+
+         Need_Bar := False;
+         if Any (Packrat_Data.Direct_Left_Recursive) then
+            for I in Packrat_Data.Direct_Left_Recursive'Range loop
+               if Packrat_Data.Direct_Left_Recursive (I) then
+                  if Need_Bar then
+                     Text := Text & " | ";
+                  else
+                     Need_Bar := True;
+                  end if;
+                  Text := Text & Trimmed_Image (I);
+               end if;
+            end loop;
+            Indent_Start ("  (");
+            Indent := Indent + 3;
+            Indent_Wrap (-Text & " => True,");
+            Indent_Line ("others => False);");
+            Indent := Indent - 3;
+         else
+            Indent_Line ("  (others => False);");
+         end if;
+         Indent := Indent - 3;
+         Indent_Line ("begin");
+         Indent := Indent + 3;
+         WisiToken.BNF.Generate_Grammar (Generate_Data.Grammar, 
Generate_Data.Action_Names.all);
+
+         Indent_Line ("return WisiToken.Parse.Packrat.Procedural.Create");
+         Indent_Line
+           ("  (Grammar, Direct_Left_Recursive, " & Trimmed_Image 
(Generate_Data.Descriptor.Accept_ID) &
+              ", Trace, Lexer.New_Lexer (Trace), User_Data);");
+      end case;
+      Indent := Indent - 3;
+      Indent_Line ("end Create_Parser;");
+      New_Line;
+   end Packrat_Create_Create_Parser;
+
+   procedure External_Create_Create_Grammar
+     (Generate_Data : in WisiToken.BNF.Generate_Utils.Generate_Data)
+   is begin
+      Indent_Line ("function Create_Grammar return 
WisiToken.Productions.Prod_Arrays.Vector");
+      Indent_Line ("is");
+      Indent_Line ("   use WisiToken;");
+      Indent_Line ("   use WisiToken.Productions;");
+      Indent_Line ("begin");
+      Indent := Indent + 3;
+      Indent_Line ("return Grammar : WisiToken.Productions.Prod_Arrays.Vector 
do");
+      Indent := Indent + 3;
+      WisiToken.BNF.Generate_Grammar (Generate_Data.Grammar, 
Generate_Data.Action_Names.all);
+      Indent := Indent - 3;
+      Indent_Line ("end return;");
+      Indent := Indent - 3;
+      Indent_Line ("end Create_Grammar;");
+   end External_Create_Create_Grammar;
+
+   procedure Create_re2c
+     (Input_Data            :         in 
WisiToken_Grammar_Runtime.User_Data_Type;
+      Tuple                 :         in Generate_Tuple;
+      Generate_Data         : aliased in 
WisiToken.BNF.Generate_Utils.Generate_Data;
+      Output_File_Name_Root :         in String;
+      Elisp_Regexps         :         in WisiToken.BNF.String_Pair_Lists.List)
+   is
+      use Ada.Strings.Fixed;
+      use Generate_Utils;
+      use WisiToken.BNF.Utils;
+      File : File_Type;
+   begin
+      Create (File, Out_File, Output_File_Name_Root & ".re2c");
+      Set_Output (File);
+      Indent := 1;
+
+      Put_File_Header (C_Comment, " -*- mode: C -*-", Use_Tuple => True, Tuple 
=> Tuple);
+      Put_Raw_Code (C_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+
+      Indent_Line ("#include <stddef.h>"); -- size_t
+      Indent_Line ("#include <stdio.h>"); -- printf
+      Indent_Line ("#include <stdlib.h>"); -- malloc
+      New_Line;
+
+      Indent_Line ("typedef struct wisi_lexer");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+      Indent_Line ("unsigned char* buffer;           // input text, in utf-8 
encoding");
+      Indent_Line ("unsigned char* buffer_last;      // last byte in buffer");
+      Indent_Line ("unsigned char* cursor;           // current byte");
+      Indent_Line ("unsigned char* byte_token_start; // byte position at start 
of current token");
+      Indent_Line ("size_t         char_pos;         // character position of 
current character");
+      Indent_Line ("size_t         char_token_start; // character position at 
start of current token");
+      Indent_Line ("int            line;             // 1 indexed");
+      Indent_Line ("int            line_token_start; // line at start of 
current token");
+      Indent_Line ("unsigned char* marker;           // saved cursor");
+      Indent_Line ("size_t         marker_pos;       // saved character 
position");
+      Indent_Line ("size_t         marker_line;      // saved line ");
+      Indent_Line ("unsigned char* context;          // saved cursor");
+      Indent_Line ("size_t         context_pos;      // saved character 
position");
+      Indent_Line ("int            context_line;     // saved line");
+      Indent_Line ("int            verbosity;");
+      New_Line;
+      Indent := Indent - 3;
+      Indent_Line ("} wisi_lexer;");
+      New_Line;
+      Indent_Line ("#define YYCTYPE unsigned char");
+      New_Line;
+
+      --  Status values:
+      Indent_Line ("#define NO_ERROR 0");
+      Indent_Line ("#define ERROR_unrecognized_character 1");
+
+      ----------
+      --  new_lexer, free_lexer, reset_lexer
+
+      --  It's normal to increment lexer->cursor one past the end of input,
+      --  but not to read that character. To support memory mapped files, we
+      --  enforce this strictly; YYPEEK returns EOT (end of text) when
+      --  reading past end of buffer; that's how we recognize the end of
+      --  text token.
+
+      Indent_Line ("wisi_lexer* " & Output_File_Name_Root & "_new_lexer");
+      Indent_Line ("   (unsigned char* input, size_t length, int verbosity)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+      Indent_Line ("wisi_lexer* result  = malloc (sizeof (wisi_lexer));");
+      Indent_Line ("result->buffer      = input;");
+      Indent_Line ("result->buffer_last = input + length - 1;");
+      Indent_Line ("result->cursor      = input;");
+      Indent_Line ("result->char_pos    = 1;");
+      Indent_Line ("result->line        = (*result->cursor == 0x0A) ? 2 : 1;");
+      Indent_Line ("result->verbosity   = verbosity;");
+      Indent_Line ("return result;");
+      Indent := Indent - 3;
+      Indent_Line ("}");
+      New_Line;
+
+      Indent_Line ("void");
+      Indent_Line (Output_File_Name_Root & "_free_lexer(wisi_lexer** lexer)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+      Indent_Line ("free(*lexer);");
+      Indent_Line ("*lexer = 0;");
+      Indent := Indent - 3;
+      Indent_Line ("}");
+      New_Line;
+
+      Indent_Line ("void");
+      Indent_Line (Output_File_Name_Root & "_reset_lexer(wisi_lexer* lexer)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+      Indent_Line ("lexer->cursor   = lexer->buffer;");
+      Indent_Line ("lexer->char_pos = 1;");
+      Indent_Line ("lexer->line     = (*lexer->cursor == 0x0A) ? 2 : 1;");
+      Indent := Indent - 3;
+      Indent_Line ("}");
+      New_Line;
+
+      ----------
+      --  next_token utils
+
+      Indent_Line ("static void debug(wisi_lexer* lexer, int state, unsigned 
char ch)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+      Indent_Line ("if (lexer->verbosity > 0)");
+      Indent_Line ("   {");
+      Indent_Line ("   if (ch < ' ')");
+      Indent_Line ("      printf (""lexer: %d, 0x%x\n"", state, ch);");
+      Indent_Line ("   else");
+      Indent_Line ("      printf (""lexer: %d, '%c' 0x%x\n"", state, ch, 
ch);");
+      Indent_Line ("   }");
+      Indent := Indent - 3;
+      Indent_Line ("}");
+      Indent_Line ("#define YYDEBUG(state, ch) debug(lexer, state, ch)");
+
+      --  YYCURSOR is only used in calls of YYDEBUG; we can't define it as
+      --  YYPEEK because it is used as '*YYCURSOR'.
+      Indent_Line ("#define YYCURSOR lexer->cursor");
+      New_Line;
+
+      Indent_Line ("#define YYPEEK() (lexer->cursor <= lexer->buffer_last) ? 
*lexer->cursor : 4");
+      New_Line;
+
+      --  Don't count UTF-8 continuation bytes, or first byte of DOS newline
+      Indent_Line ("#define DO_COUNT ((*lexer->cursor & 0xC0) != 0xC0) && 
(*lexer->cursor != 0x0D)");
+      New_Line;
+
+      Indent_Line ("static void skip(wisi_lexer* lexer)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+      Indent_Line ("if (lexer->cursor <= lexer->buffer_last) 
++lexer->cursor;");
+      Indent_Line ("if (lexer->cursor <= lexer->buffer_last)");
+      Indent_Line ("   if (DO_COUNT) ++lexer->char_pos;");
+      Indent_Line ("if (*lexer->cursor == 0x0A) ++lexer->line;");
+      Indent := Indent - 3;
+      Indent_Line ("}");
+      Indent_Start ("#define YYSKIP() skip(lexer)");
+      New_Line;
+
+      Indent_Line ("#define YYBACKUP() lexer->marker = lexer->cursor; 
lexer->marker_pos = lexer->char_pos;" &
+                     "lexer->marker_line = lexer->line");
+      Indent_Line ("#define YYRESTORE() lexer->cursor = lexer->marker; 
lexer->char_pos = lexer->marker_pos;" &
+                     "lexer->line = lexer->marker_line");
+      Indent_Line ("#define YYBACKUPCTX() lexer->context = lexer->cursor; 
lexer->context_pos = lexer->char_pos;" &
+                     "lexer->context_line = lexer->line");
+      Indent_Line ("#define YYRESTORECTX() lexer->cursor = lexer->context; 
lexer->char_pos = lexer->context_pos;" &
+                     "lexer->line = lexer->context_line");
+      New_Line;
+
+      if Is_In (Input_Data.Tokens.Tokens, "delimited-text") then
+         Indent_Line ("static void skip_to(wisi_lexer* lexer, char* target)");
+         Indent_Line ("{");
+         Indent_Line ("  int i;");
+         New_Line;
+         Indent_Line ("  while (lexer->cursor <= lexer->buffer_last)");
+         Indent_Line ("    {");
+         Indent_Line ("      if (*lexer->cursor == target[0])");
+         Indent_Line ("      {");
+         Indent_Line ("        i = 0;");
+         Indent_Line ("        do");
+         Indent_Line ("          i++;");
+         Indent_Line ("        while (0 != target[i] &&");
+         Indent_Line ("               lexer->cursor + i <= lexer->buffer_last 
&&");
+         Indent_Line ("               *(lexer->cursor + i) == target[i]);");
+         New_Line;
+         Indent_Line ("        if (0 == target[i])");
+         Indent_Line ("          {");
+         Indent_Line ("            for (i = 0; 0 != target[i]; i++)");
+         Indent_Line ("               skip(lexer);");
+         Indent_Line ("            break;");
+         Indent_Line ("          }");
+         Indent_Line ("      }");
+         Indent_Line ("      skip(lexer);");
+         Indent_Line ("    };");
+         Indent_Line ("}");
+         New_Line;
+      end if;
+
+      ----------
+      --  next_token
+      Indent_Line ("int " & Output_File_Name_Root & "_next_token");
+      Indent_Line ("  (wisi_lexer* lexer,");
+      Indent_Line ("   int* id,");
+      Indent_Line ("   size_t* byte_position,");
+      Indent_Line ("   size_t* byte_length,");
+      Indent_Line ("   size_t* char_position,");
+      Indent_Line ("   size_t* char_length,");
+      Indent_Line ("   int*    line_start)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+
+      Indent_Line ("int status = NO_ERROR;");
+      Indent_Line ("*id = -1;"); --  Token_ID'First = 0; see dragon_4_43.wy
+
+      Indent_Line ("if (lexer->cursor > lexer->buffer_last)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+      Indent_Line ("*id            =" & WisiToken.Token_ID'Image 
(Generate_Data.Descriptor.EOF_ID) & ";");
+      Indent_Line ("*byte_position = lexer->buffer_last - lexer->buffer + 1;");
+      Indent_Line ("*byte_length   = 0;");
+      Indent_Line ("*char_position = lexer->char_token_start;");
+      Indent_Line ("*char_length   = 0;");
+      Indent_Line ("*line_start    = lexer->line;");
+      Indent_Line ("return status;");
+      Indent := Indent - 3;
+      Indent_Line ("}");
+      New_Line;
+
+      Indent_Line ("lexer->byte_token_start = lexer->cursor;");
+      Indent_Line ("if (DO_COUNT)");
+      Indent_Line ("   lexer->char_token_start = lexer->char_pos;");
+      Indent_Line ("else");
+      Indent_Line ("   lexer->char_token_start = lexer->char_pos + 1;");
+      Indent_Line ("if (*lexer->cursor == 0x0A)");
+      Indent_Line ("   lexer->line_token_start = lexer->line-1;");
+      Indent_Line ("else");
+      Indent_Line ("   lexer->line_token_start = lexer->line;");
+      New_Line;
+
+      Indent_Line ("while (*id == -1 && status == 0)");
+      Indent_Line ("{");
+      Indent := Indent + 3;
+
+      Put_Line ("/*!re2c");
+      Indent_Line ("re2c:yyfill:enable   = 0;");
+      New_Line;
+
+      --  Regexps used in definitions
+      for Pair of Input_Data.Tokens.Regexps loop
+         Indent_Line (-Pair.Name & " = " & (-Pair.Value) & ";");
+      end loop;
+      New_Line;
+
+      --  definitions
+      for I in All_Tokens (Generate_Data).Iterate (Non_Grammar => True, 
Nonterminals => False) loop
+
+         declare
+            Val : constant String :=
+              (if Is_Present (Elisp_Regexps, Value (I))
+               then Value (Elisp_Regexps, Value (I))
+               else Value (I));
+         begin
+            if 0 /= Index (Source => Val, Pattern => "/") then
+               --  trailing context syntax; forbidden in definitions
+               null;
+
+            elsif Kind (I) = "EOI" then
+               Indent_Line (Name (I) & " = [\x04];");
+
+            elsif Kind (I) = "delimited-text" then
+               --  not declared in definitions
+               null;
+
+            elsif Kind (I) = "keyword" and 
Input_Data.Language_Params.Case_Insensitive then
+               Indent_Line (Name (I) & " = '" & Strip_Quotes (Val) & "';");
+
+            else
+               --  Other kinds have values that are regular expressions, in 
re2c syntax
+               Indent_Line (Name (I) & " = " & Val & ";");
+            end if;
+         end;
+      end loop;
+      New_Line;
+
+      --  lexer rules
+      for I in All_Tokens (Generate_Data).Iterate (Non_Grammar => True, 
Nonterminals => False) loop
+         declare
+            Val : constant String :=
+              (if Is_Present (Elisp_Regexps, Value (I))
+               then Value (Elisp_Regexps, Value (I))
+               else Value (I));
+         begin
+
+            if Kind (I) = "non-reporting" then
+               Indent_Line (Name (I) & " { lexer->byte_token_start = 
lexer->cursor;");
+               Indent_Line ("    lexer->char_token_start = lexer->char_pos;");
+               Indent_Line ("    if (*lexer->cursor == 0x0A)");
+               Indent_Line ("       lexer->line_token_start = lexer->line-1;");
+               Indent_Line ("    else");
+               Indent_Line ("       lexer->line_token_start = lexer->line;");
+               Indent_Line ("    continue; }");
+
+            elsif Kind (I) = "delimited-text" then
+               --  Val contains the start and end strings, separated by space
+               declare
+                  Start_Last : constant Integer := Index (Val, " ");
+               begin
+                  Indent_Line
+                    (Val (1 .. Start_Last - 1) & " {*id = " & 
WisiToken.Token_ID'Image (ID (I)) &
+                       "; skip_to(lexer, " & Val (Start_Last + 1 .. Val'Last) 
& "); continue;}");
+               end;
+
+            elsif 0 /= Index (Source => Val, Pattern => "/") then
+               Indent_Line (Val & " {*id = " & WisiToken.Token_ID'Image (ID 
(I)) & "; continue;}");
+
+            else
+               Indent_Line (Name (I) & " {*id = " & WisiToken.Token_ID'Image 
(ID (I)) & "; continue;}");
+            end if;
+         end;
+      end loop;
+      New_Line;
+
+      --  Default action
+      Indent_Line ("* {status = ERROR_unrecognized_character; continue;}");
+
+      Put_Line ("*/");
+      Indent_Line ("}");
+      Indent := Indent - 3;
+
+      Indent_Line ("*byte_position = lexer->byte_token_start - lexer->buffer + 
1;");
+      Indent_Line ("*byte_length   = lexer->cursor - 
lexer->byte_token_start;");
+      Indent_Line ("*char_position = lexer->char_token_start;");
+      Indent_Line ("if (DO_COUNT)");
+      Indent_Line ("   *char_length = lexer->char_pos - 
lexer->char_token_start;");
+      Indent_Line ("else");
+      Indent_Line ("   *char_length = lexer->char_pos - 
lexer->char_token_start + 1;");
+      Indent_Line ("*line_start     = lexer->line_token_start;");
+      Indent_Line ("return status;");
+      Indent_Line ("}");
+      Indent := Indent - 3;
+      Set_Output (Standard_Output);
+      Close (File);
+
+      declare
+         Ada_Name : constant String := Output_File_Name_Root & "_re2c_c";
+         --  Output_File_Name_Root is the file name of the grammar file -
+         --  assume it is a legal Ada name.
+      begin
+         Create (File, Out_File, Output_File_Name_Root & "_re2c_c.ads");
+         Set_Output (File);
+         Indent := 1;
+         Put_File_Header (Ada_Comment, Use_Tuple => True, Tuple => Tuple);
+         Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+         New_Line;
+
+         Put_Line ("with Interfaces.C;");
+         Put_Line ("with WisiToken;");
+         Put_Line ("with System;");
+         Put_Line ("package " & Ada_Name & " is");
+         Indent := Indent + 3;
+         New_Line;
+
+         Indent_Line ("function New_Lexer");
+         Indent_Line ("  (Buffer    : in System.Address;");
+         Indent_Line ("   Length    : in Interfaces.C.size_t;");
+         Indent_Line ("   Verbosity : in Interfaces.C.int)");
+         Indent_Line ("  return System.Address");
+         Indent_Line ("with Import        => True,");
+         Indent_Line ("     Convention    => C,");
+         Indent_Line ("     External_Name => """ & Output_File_Name_Root & 
"_new_lexer"";");
+         Indent_Line ("--  Create the lexer object, passing it the full text 
to process.");
+         New_Line;
+         Indent_Line ("procedure Free_Lexer (Lexer : in out System.Address)");
+         Indent_Line ("with Import        => True,");
+         Indent_Line ("     Convention    => C,");
+         Indent_Line ("     External_Name => """ & Output_File_Name_Root & 
"_free_lexer"";");
+         Indent_Line ("--  Free the lexer object");
+         New_Line;
+
+         Indent_Line ("procedure Reset_Lexer (Lexer : in System.Address)");
+         Indent_Line ("with Import        => True,");
+         Indent_Line ("     Convention    => C,");
+         Indent_Line ("     External_Name => """ & Output_File_Name_Root & 
"_reset_lexer"";");
+         New_Line;
+
+         Indent_Line ("function Next_Token");
+         Indent_Line ("  (Lexer         : in     System.Address;");
+         Indent_Line ("   ID            :    out WisiToken.Token_ID;");
+         Indent_Line ("   Byte_Position :    out Interfaces.C.size_t;");
+         Indent_Line ("   Byte_Length   :    out Interfaces.C.size_t;");
+         Indent_Line ("   Char_Position :    out Interfaces.C.size_t;");
+         Indent_Line ("   Char_Length   :    out Interfaces.C.size_t;");
+         Indent_Line ("   Line_Start    :    out Interfaces.C.int)");
+         Indent_Line ("  return Interfaces.C.int");
+         Indent_Line ("with Import        => True,");
+         Indent_Line ("     Convention    => C,");
+         Indent_Line ("     External_Name => """ & Output_File_Name_Root & 
"_next_token"";");
+         New_Line;
+
+         Indent := Indent - 3;
+         Put_Line ("end " & Ada_Name & ";");
+         Set_Output (Standard_Output);
+         Close (File);
+      end;
+   end Create_re2c;
+
+   function File_Name_To_Ada (File_Name : in String) return String
+   is
+      Result : String := File_Name;
+   begin
+      Result (Result'First) := To_Upper (Result (Result'First));
+      for I in Result'Range loop
+         if Result (I) = '-' then
+            Result (I) := '.';
+            Result (I + 1) := To_Upper (Result (I + 1));
+         elsif Result (I) = '_' then
+            Result (I + 1) := To_Upper (Result (I + 1));
+         end if;
+      end loop;
+      return Result;
+   end File_Name_To_Ada;
+
+   function Initialize
+     (Input_Data        : in WisiToken_Grammar_Runtime.User_Data_Type;
+      Tuple             : in Generate_Tuple;
+      Output_File_Root  : in String;
+      Check_Interface   : in Boolean)
+     return Common_Data
+   is begin
+      return Data : Common_Data do
+         Data.Generate_Algorithm := Tuple.Gen_Alg;
+
+         Data.Output_Language := Ada_Output_Language (Tuple.Out_Lang);
+
+         if Tuple.Gen_Alg = External or else Input_Data.User_Lexer in 
Valid_Lexer then
+            Data.Lexer := Input_Data.User_Lexer;
+         else
+            raise SAL.Programmer_Error with "tuple.alg " & 
Generate_Algorithm'Image (Tuple.Gen_Alg) &
+              " input_data.user_lexer " & Lexer_Image 
(Input_Data.User_Lexer).all;
+         end if;
+
+         if Check_Interface then
+            if Tuple.Interface_Kind in Valid_Interface then
+               Data.Interface_Kind := Valid_Interface (Tuple.Interface_Kind);
+            else
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, 1, "Interface_Kind 
not set"));
+            end if;
+         else
+            Data.Interface_Kind := Process;
+         end if;
+
+         Data.Text_Rep := Tuple.Text_Rep;
+
+         Data.Lower_File_Name_Root := +To_Lower (Output_File_Root);
+      end return;
+   end Initialize;
+
+   function To_Token_Ada_Name (WY_Name : in String) return String
+   is
+      --  Convert WY_Name to a valid Ada identifier:
+      --
+      --  Add "_ID" to avoid collision with Ada reserved words
+      --
+      --  Replace '-' with '_'
+      Image : String := WY_Name;
+   begin
+      for I in Image'Range loop
+         if Image (I) = '-' then
+            Image (I) := '_';
+         end if;
+      end loop;
+      return Image & "_ID";
+   end To_Token_Ada_Name;
+
+end WisiToken.BNF.Output_Ada_Common;
diff --git a/wisitoken-bnf-output_ada_common.ads 
b/wisitoken-bnf-output_ada_common.ads
new file mode 100644
index 0000000..b4d8923
--- /dev/null
+++ b/wisitoken-bnf-output_ada_common.ads
@@ -0,0 +1,91 @@
+--  Abstract :
+--
+--  Types and operations shared by Ada and Ada_Emacs outputs.
+--
+--  Copyright (C) 2017, 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);
+
+with WisiToken.BNF.Generate_Utils;
+with WisiToken.Generate.Packrat;
+with WisiToken_Grammar_Runtime;
+package WisiToken.BNF.Output_Ada_Common is
+
+   function To_Token_Ada_Name (WY_Name : in String) return String;
+
+   type Common_Data is limited record
+      --  Validated versions of Tuple values
+      Generate_Algorithm : WisiToken.BNF.Valid_Generate_Algorithm;
+      Lexer              : Lexer_Type; --  'none' valid for Libadalang
+      Output_Language    : Ada_Output_Language;
+      Interface_Kind     : Valid_Interface;
+      Text_Rep           : Boolean;
+
+      Lower_File_Name_Root : Standard.Ada.Strings.Unbounded.Unbounded_String;
+   end record;
+
+   function Initialize
+     (Input_Data        : in WisiToken_Grammar_Runtime.User_Data_Type;
+      Tuple             : in Generate_Tuple;
+      Output_File_Root  : in String;
+      Check_Interface   : in Boolean)
+     return Common_Data;
+
+   function File_Name_To_Ada (File_Name : in String) return String;
+
+   procedure Create_Ada_Actions_Spec
+     (Output_File_Name :         in String;
+      Package_Name     :         in String;
+      Input_Data       :         in WisiToken_Grammar_Runtime.User_Data_Type;
+      Common_Data      :         in Output_Ada_Common.Common_Data;
+      Generate_Data    : aliased in 
WisiToken.BNF.Generate_Utils.Generate_Data);
+
+   procedure Create_Ada_Main_Spec
+     (Output_File_Name  : in String;
+      Main_Package_Name : in String;
+      Input_Data        : in WisiToken_Grammar_Runtime.User_Data_Type;
+      Common_Data       : in Output_Ada_Common.Common_Data)
+   with Pre => Common_Data.Generate_Algorithm /= External;
+
+   procedure Create_External_Main_Spec
+     (Main_Package_Name    : in String;
+      Tuple                : in Generate_Tuple;
+      Input_Data           : in WisiToken_Grammar_Runtime.User_Data_Type);
+
+   procedure LR_Create_Create_Parser
+     (Input_Data    :         in     WisiToken_Grammar_Runtime.User_Data_Type;
+      Common_Data   :         in out Output_Ada_Common.Common_Data;
+      Generate_Data : aliased in     
WisiToken.BNF.Generate_Utils.Generate_Data);
+   --  If not Common_Data.Text_Rep, includes LR parse table in generated
+   --  source. Otherwise, includes call to LR.Get_Text_Rep; caller must
+   --  call Put_Text_Rep to create file.
+
+   procedure Packrat_Create_Create_Parser
+     (Common_Data   :         in out Output_Ada_Common.Common_Data;
+      Generate_Data : aliased in     
WisiToken.BNF.Generate_Utils.Generate_Data;
+      Packrat_Data  :         in     WisiToken.Generate.Packrat.Data);
+
+   procedure External_Create_Create_Grammar
+     (Generate_Data : in WisiToken.BNF.Generate_Utils.Generate_Data);
+
+   procedure Create_re2c
+     (Input_Data            :         in 
WisiToken_Grammar_Runtime.User_Data_Type;
+      Tuple                 :         in Generate_Tuple;
+      Generate_Data         : aliased in 
WisiToken.BNF.Generate_Utils.Generate_Data;
+      Output_File_Name_Root :         in String;
+      Elisp_Regexps         :         in WisiToken.BNF.String_Pair_Lists.List);
+   --  Create_re2c is called from wisi-generate, which does not declare
+   --  Common_Data.
+
+end WisiToken.BNF.Output_Ada_Common;
diff --git a/wisitoken-bnf-output_ada_emacs.adb 
b/wisitoken-bnf-output_ada_emacs.adb
new file mode 100644
index 0000000..136c6d3
--- /dev/null
+++ b/wisitoken-bnf-output_ada_emacs.adb
@@ -0,0 +1,1519 @@
+--  Abstract :
+--
+--  Output Ada code implementing the grammar defined by input
+--  parameters, and a parser for that grammar. The parser actions
+--  assume the Emacs Ada mode wisi indentation engine
+--
+--  If run in a separate process communicating over pipes with the
+--  Emacs process, the parser actions output encoded elisp actions;
+--  the protocol is documented in Emacs Ada mode wisi-process-parse.el,
+--  function wisi-process-parse-execute.
+--
+--  If run in an Emacs dynamically loaded module, the parser actions
+--  call the elisp actions directly.
+--
+--  Copyright (C) 2012 - 2015, 2017, 2018 Stephen Leake.  All Rights Reserved.
+--
+--  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 MERCHANTABILITY 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 Ada.Strings.Fixed;
+with Ada.Strings.Maps;
+with Ada.Text_IO; use Ada.Text_IO;
+with WisiToken.BNF.Generate_Packrat;
+with WisiToken.BNF.Generate_Utils;
+with WisiToken.BNF.Output_Ada_Common; use WisiToken.BNF.Output_Ada_Common;
+with WisiToken.BNF.Output_Elisp_Common; use WisiToken.BNF.Output_Elisp_Common;
+with WisiToken.Generate.Packrat;
+with WisiToken_Grammar_Runtime;
+procedure WisiToken.BNF.Output_Ada_Emacs
+  (Input_Data            :         in WisiToken_Grammar_Runtime.User_Data_Type;
+   Output_File_Name_Root :         in String;
+   Generate_Data         : aliased in 
WisiToken.BNF.Generate_Utils.Generate_Data;
+   Packrat_Data          :         in WisiToken.Generate.Packrat.Data;
+   Tuple                 :         in Generate_Tuple;
+   Test_Main             :         in Boolean;
+   Multiple_Tuples       :         in Boolean;
+   Language_Name         :         in String)
+is
+   use all type Ada.Containers.Count_Type;
+
+   Language_Runtime_Package : constant String := "Wisi." & Language_Name;
+
+   Blank_Set : constant Ada.Strings.Maps.Character_Set := 
Ada.Strings.Maps.To_Set (" ");
+
+   Common_Data : Output_Ada_Common.Common_Data := 
WisiToken.BNF.Output_Ada_Common.Initialize
+     (Input_Data, Tuple, Output_File_Name_Root, Check_Interface => True);
+
+   Gen_Alg_Name : constant String :=
+     (if Test_Main or Multiple_Tuples
+      then "_" & WisiToken.BNF.Generate_Algorithm_Image 
(Common_Data.Generate_Algorithm).all
+      else "");
+
+   function Split_Sexp
+     (Item            : in String;
+      Input_File_Name : in String;
+      Source_Line     : in WisiToken.Line_Number_Type)
+     return String_Lists.List
+   is
+      --  Return one sexp per element. Remove comments, newlines, and outer 
'(progn )'.
+
+      use WisiToken.Generate;
+
+      Progn_Index : constant Integer := Ada.Strings.Fixed.Index (Item, 
"(progn");
+
+      Item_I : Integer := Item'First;
+
+      Buffer       : String (Item'First .. Item'Last);
+      Buffer_J     : Integer := Buffer'First;
+      Buffer_First : Integer := Buffer'First;
+      Paren_Count  : Integer := 0;
+      In_Comment   : Boolean := False;
+      Result       : String_Lists.List;
+
+      Delete_Last_Paren : Boolean := False;
+   begin
+      --  Loop thru Item, copying chars to Buffer, ignoring comments, newlines.
+
+      if 0 /= Progn_Index then
+         Item_I := Progn_Index + 6;
+
+         Delete_Last_Paren := True;
+      end if;
+
+      loop
+         exit when Item_I > Item'Last;
+
+         if In_Comment then
+            if Item (Item_I) in ASCII.CR | ASCII.LF then
+               In_Comment := False;
+            end if;
+         else
+            if Item (Item_I) = '(' then
+               if Paren_Count = 0 then
+                  Buffer_First := Buffer_J;
+               end if;
+               Paren_Count := Paren_Count + 1;
+
+               Buffer (Buffer_J) := Item (Item_I);
+               Buffer_J := Buffer_J + 1;
+
+            elsif Item (Item_I) = ')' then
+               Paren_Count := Paren_Count - 1;
+               if Paren_Count = 0 then
+                  Buffer (Buffer_J) := Item (Item_I);
+                  Result.Append (Buffer (Buffer_First .. Buffer_J));
+                  Buffer_First := Buffer'First;
+                  Buffer_J     := Buffer'First;
+
+               elsif Paren_Count = -1 then
+                  if Delete_Last_Paren then
+                     --  all done
+                     return Result;
+                  else
+                     Put_Error (Error_Message (Input_File_Name, Source_Line, 
"mismatched parens"));
+                     return String_Lists.Empty_List;
+                  end if;
+               else
+                  Buffer (Buffer_J) := Item (Item_I);
+                  Buffer_J := Buffer_J + 1;
+               end if;
+
+            elsif Item (Item_I) in ASCII.CR | ASCII.LF then
+               null;
+
+            elsif Item (Item_I) = ';' and then Item_I < Item'Last and then 
Item (Item_I + 1) = ';' then
+               In_Comment := True;
+
+            else
+               Buffer (Buffer_J) := Item (Item_I);
+               Buffer_J := Buffer_J + 1;
+            end if;
+         end if;
+         Item_I := Item_I + 1;
+      end loop;
+      if Paren_Count /= 0 then
+         Put_Error
+           (Error_Message
+              (Input_File_Name, Source_Line, "mismatched parens"));
+      end if;
+      return Result;
+   end Split_Sexp;
+
+   procedure Create_Ada_Action
+     (Name          : in String;
+      RHS           : in RHS_Type;
+      Unsplit_Lines : in Ada.Strings.Unbounded.Unbounded_String;
+      Check         : in Boolean)
+   is
+      --  Create Action (if Check = False; Lines must be RHS.Action) or
+      --  Check (if Check = True; Lines must be RHS.Check) subprogram named
+      --  Name for RHS.
+
+      use Ada.Strings;
+      use Ada.Strings.Fixed;
+      use Ada.Strings.Unbounded;
+      use WisiToken.Generate;
+
+      Sexps : constant String_Lists.List := Split_Sexp
+        (-Unsplit_Lines, Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line);
+
+      use all type Ada.Strings.Maps.Character_Set;
+
+      Space_Paren_Set : constant Ada.Strings.Maps.Character_Set :=
+        Ada.Strings.Maps.To_Set ("])") or Blank_Set;
+
+      Navigate_Lines     : String_Lists.List;
+      Face_Line          : Unbounded_String;
+      Indent_Action_Line : Unbounded_String;
+      Check_Lines        : String_Lists.List;
+
+      function Statement_Params (Params : in String) return String
+      is
+         --  Input looks like: [1 function 2 other ...]
+         Last       : Integer := Index_Non_Blank (Params); -- skip [
+         First      : Integer;
+         Second     : Integer;
+         Need_Comma : Boolean := False;
+         Result     : Unbounded_String;
+         Count      : Integer := 0;
+      begin
+         loop
+            First  := Last + 1;
+            Second := Index (Params, Blank_Set, First);
+            exit when Second = 0;
+
+            Count := Count + 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) & ")";
+
+            Need_Comma := True;
+         end loop;
+         if Count = 1 then
+            return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
+         else
+            return " (Parse_Data, Tree, Nonterm, Tokens, (" & (-Result) & "))";
+         end if;
+      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);
+      begin
+         return " (Parse_Data, Tree, Nonterm, Tokens, " &
+           Params (First .. Second - 1) & ',' & Params (Second .. Params'Last);
+      end Containing_Params;
+
+      function Motion_Params (Params : in String) return String
+      is
+         --  Input looks like: [1 [2 EXCEPTION WHEN] 3 ...]
+         --  Result: (..., Motion_Param_Array'((1, Empty_IDs) & (2, (3 & 8)) & 
(3, Empty_IDs))
+         use Generate_Utils;
+         use Ada.Strings.Maps;
+
+         Delim : constant Character_Set := To_Set ("]") or Blank_Set;
+
+         Last   : Integer          := Index_Non_Blank (Params); -- skip [
+         First  : Integer;
+         Vector : Boolean;
+         Result : Unbounded_String := +" (Parse_Data, Tree, Nonterm, Tokens, 
(";
+
+         Index_First  : Integer;
+         Index_Last   : Integer;
+         IDs          : Unbounded_String;
+         IDs_Count    : Integer;
+         Need_Comma_1 : Boolean := False;
+         Need_Comma_2 : Boolean := False;
+      begin
+         loop
+            Last := Index_Non_Blank (Params, Integer'Min (Params'Last, Last + 
1));
+
+            exit when Params (Last) = ']' or Params (Last) = ')';
+
+            Vector := Params (Last) = '[';
+            if Vector then
+               Index_First  := Last + 1;
+               Last         := Index (Params, Delim, Index_First);
+               Index_Last   := Last - 1;
+               IDs_Count    := 0;
+               IDs          := Null_Unbounded_String;
+               Need_Comma_2 := False;
+               loop
+                  exit when Params (Last) = ']';
+                  First     := Last + 1;
+                  Last      := Index (Params, Delim, First);
+                  IDs_Count := IDs_Count + 1;
+                  begin
+                     IDs := IDs & (if Need_Comma_2 then " & " else "") &
+                       Trimmed_Image (Find_Token_ID (Generate_Data, Params 
(First .. Last - 1)));
+                     Need_Comma_2 := True;
+                  exception
+                  when E : Not_Found =>
+                     Put_Error
+                       (Error_Message
+                          (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line,
+                           Ada.Exceptions.Exception_Message (E)));
+                  end;
+               end loop;
+
+               Result := Result & (if Need_Comma_1 then " & " else "") & "(" &
+                 Params (Index_First .. Index_Last) & ", " &
+                 (if IDs_Count = 1 then "+" else "") & IDs & ")";
+            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)";
+            end if;
+            Need_Comma_1 := True;
+         end loop;
+         return -(Result & "))");
+      end Motion_Params;
+
+      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 ...]
+         --  Result: ((1, 3, 1), (3, 3, 2), ...)
+         use Ada.Strings.Maps;
+         Delim : constant Character_Set := To_Set ("]") or Blank_Set;
+
+         Last       : Integer          := Index_Non_Blank (Params); -- skip [
+         First      : Integer;
+         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);
+
+            if Params (Last) = ']' then
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"invalid wisi-face-apply argument"));
+               exit;
+            end if;
+
+            First  := Index_Non_Blank (Params, Last + 1);
+            Last   := Index (Params, Delim, First);
+            Result := Result & ',' & Integer'Image
+              (Find_Elisp_ID (Input_Data.User_Names.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;
+            end if;
+
+            First  := Index_Non_Blank (Params, Last + 1);
+            Last   := Index (Params, Delim, First);
+            Result := Result & ',' &
+              Integer'Image (Find_Elisp_ID (Input_Data.User_Names.Faces, 
Params (First .. Last - 1))) & ")";
+
+            Need_Comma := True;
+         end loop;
+         if Count = 1 then
+            return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
+         else
+            return " (Parse_Data, Tree, Nonterm, Tokens, (" & (-Result) & "))";
+         end if;
+      exception
+      when E : others =>
+         Put_Error
+           (Error_Message
+              (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, "invalid 
syntax: " &
+              Ada.Exceptions.Exception_Message (E)));
+         return "";
+      end Face_Apply_Params;
+
+      function Face_Mark_Params (Params : in String) return String
+      is
+         --  Params is a vector of pairs: [1 prefix 3 suffix ...]
+         --  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 [
+         First      : Integer;
+         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);
+
+            if Params (Last) = ']' then
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"invalid wisi-face-mark argument"));
+               exit;
+            end if;
+
+            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;
+         end loop;
+         if Count = 1 then
+            return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
+         else
+            return " (Parse_Data, Tree, Nonterm, Tokens, (" & (-Result) & "))";
+         end if;
+      exception
+      when E : others =>
+         Put_Error
+           (Error_Message
+            (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, "invalid 
syntax: " &
+              Ada.Exceptions.Exception_Message (E)));
+         return "";
+      end Face_Mark_Params;
+
+      function Face_Remove_Params (Params : in String) return String
+      is
+         --  Params is a vector of token numbers: [1 3 ...]
+         --  Result: (1, 3, ...)
+         use Ada.Strings.Maps;
+         Delim : constant Character_Set := To_Set ("]") or Blank_Set;
+
+         Last       : Integer          := Index_Non_Blank (Params); -- skip [
+         First      : Integer;
+         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);
+
+            Need_Comma := True;
+         end loop;
+         if Count = 1 then
+            return " (Parse_Data, Tree, Nonterm, Tokens, (1 => " & (-Result) & 
"))";
+         else
+            return " (Parse_Data, Tree, Nonterm, Tokens, (" & (-Result) & "))";
+         end if;
+      exception
+      when E : others =>
+         Put_Error
+           (Error_Message
+            (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, "invalid 
syntax: " &
+              Ada.Exceptions.Exception_Message (E)));
+         return "";
+      end Face_Remove_Params;
+
+      function Indent_Params (Params : in String; N : in String := "") return 
String
+      is
+         --  If N is non-empty, it is the first arg in wisi-indent-action*, 
followed by ','.
+         --
+         --  Params is a vector, one item for each token in Tokens. Each item 
is one of:
+         --
+         --  - an integer; copy to output
+         --
+         --  - a symbol; convert to Ada name syntax
+         --
+         --  - 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.
+
+         use Ada.Strings.Maps;
+         use Ada.Containers;
+
+         Delim : constant Character_Set := To_Set ("])") or Blank_Set;
+
+         subtype Digit is Character range '0' .. '9';
+
+         Last          : Integer         := Index_Non_Blank (Params); -- skip [
+         Prefix        : constant String := " (Parse_Data, Tree, Nonterm, 
Tokens, " & N & "(";
+         Result        : Unbounded_String;
+         Need_Comma    : Boolean         := False;
+         Param_Count   : Count_Type      := 0;            -- in Params
+
+         function Indent_Label (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";
+            elsif Elisp_Name = "wisi-anchored%-" then return "Anchored_2";
+            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";
+            else
+               Put_Error
+                 (Error_Message
+                  (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"unrecognized wisi indent function: '" &
+                    Elisp_Name & "'"));
+               return "";
+            end if;
+         end Indent_Label;
+
+         function Ensure_Simple_Indent (Item : in String) return String
+         is begin
+            --  Return an aggregate for Simple_Indent_Param. Item can be 
anything
+            --  Expression returns except Hanging.
+
+            if Item (Item'First) = '(' then
+               --  Anchored or Language
+               return Item;
+
+            else
+               --  simple integer
+               return "(Int, " & Item & ")";
+            end if;
+         end Ensure_Simple_Indent;
+
+         function Expression (Param_First : in Integer) return String
+         is
+            --  Return a simple integer expression, or an aggregate for
+            --  Simple_Indent_Param or Indent_Param.
+            --
+            --  Handles this syntax:
+            --
+            --  integer literal:
+            --  2 => 2
+            --  -1 => -1
+            --
+            --  variable name:
+            --  ada-indent => Ada_Indent
+            --
+            --  token_id literal:
+            --  'TYPE => 13
+            --
+            --  simple expression with + - * :
+            --  (- ada-indent) => -Ada_Indent
+            --  (- ada-indent-when ada-indent) => Ada_Indent_When - Ada_Indent
+            --
+            --  if expression:
+            --  (if c a b) => (if c then a else b)
+            --
+            --  function call with expression args:
+            --  (wisi-hanging (wisi-anchored% 1 ada-indent)
+            --                (wisi-anchored% 1 (+ ada-indent 
ada-indent-broken)))
+
+            use Generate_Utils;
+
+            First : Integer := Index_Non_Blank (Params, Param_First);
+
+            Function_Name : Unbounded_String;
+            Args          : Unbounded_String;
+            Arg_Count     : Count_Type      := 0;
+         begin
+            if Params (First) in Digit or Params (First) = '-' then
+               Last := Index (Params, Delim, First);
+               return Params (First .. Last - 1);
+
+            elsif Params (First) = ''' then
+               Last := Index (Params, Delim, First);
+               return WisiToken.Trimmed_Image (Find_Token_ID (Generate_Data, 
Params (First + 1 .. Last - 1)));
+
+            elsif Params (First) = '(' then
+               First  := First + 1;
+               Last   := Index (Params, Delim, First);
+               Function_Name := +Params (First .. Last - 1);
+
+               if Length (Function_Name) = 1 then
+                  --  - + *
+                  Last := Index (Params, Delim, Last + 1);
+                  if Params (Last) = ')' then
+                     return Result : constant String := -Function_Name & 
Expression (First + 1)
+                     do
+                        Last := Last + 1; -- get past ')'
+                     end return;
+                  else
+                     Args := +Expression (First + 1);
+                     Args := Args & ' ' & Function_Name & ' ' & Expression 
(Last + 1);
+
+                     Last := Last + 1; -- get past ')'
+                     return -Args;
+                  end if;
+
+               elsif -Function_Name = "if" then
+                  Args := +Expression (Last + 1);
+                  Args := +"(if " & Args & " then " & Expression (Last + 1);
+                  Args := Args & " else " & Expression (Last + 1) & ')';
+
+                  Last := Last + 1; -- get past ')'
+                  return -Args;
+
+               elsif Is_Present (Input_Data.User_Names.Indents, 
-Function_Name) then
+                  --  Language-specific function call
+                  Function_Name := +Value (Input_Data.User_Names.Indents, 
-Function_Name);
+                  Arg_Count     := 0;
+                  loop
+                     exit when Params (Last) = ')';
+
+                     First := Last + 1;
+                     if Arg_Count = 0 then
+                        Args := +Expression (First);
+                     else
+                        Args := Args & " & " & Expression (First);
+                     end if;
+                     Arg_Count := Arg_Count + 1;
+                  end loop;
+
+                  Last := Last + 1; -- get past ')'
+
+                  return "(Language, " & (-Function_Name) & "'Access, " &
+                    (if Arg_Count = 0 then "Null_Args"
+                     elsif Arg_Count = 1 then '+' & (-Args)
+                     else -Args)
+                    & ')';
+
+               else
+                  --  wisi lisp function call
+                  Function_Name := +Indent_Label (-Function_Name);
+                  if Length (Function_Name) = 0 then
+                     --  not a recognized function
+                     Last := 1 + Index (Params, ")", Last);
+                     return "";
+
+                  elsif Slice (Function_Name, 1, 4) = "Hang" then
+                     --  Arguments are 2 Simple_Indent_Param
+                     Args := +Ensure_Simple_Indent (Expression (Last + 1));
+                     Args := Args & ", " & Ensure_Simple_Indent (Expression 
(Last + 1));
+                     Last := Last + 1; -- get past ')'
+                     return "(" & (-(Function_Name & ", " & Args)) & ")";
+                  else
+                     --  Arguments are 2 simple integer expressions
+                     Args := +Expression (Last + 1);
+                     Args := Args & ", " & Expression (Last + 1);
+                     Last := Last + 1; -- get past ')'
+                     return "(" & (-(Function_Name & ", " & Args)) & ")";
+                  end if;
+               end if;
+
+            else
+               --  Assume it is 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);
+            end if;
+         exception
+         when E : others =>
+            Put_Error
+              (Error_Message
+                 (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Ada.Exceptions.Exception_Message (E)));
+            return "";
+         end Expression;
+
+         function Ensure_Indent_Param (Item : in String) return String
+         is begin
+            --  Return an aggregate for Indent_Param. Item can be anything
+            --  Expression returns.
+            if Item'Length = 0 then
+               --  Expression could not find an indent function
+               return Item;
+
+            elsif Item'Length >= 5 and then Item (Item'First .. Item'First + 
4) = "(Hang" then
+               return Item;
+
+            elsif Item (Item'First) = '(' then
+               --  Anchored or Language
+               return "(Simple, " & Item & ")";
+
+            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;
+
+            case Params (Last) is
+            when '(' =>
+               Result := Result & "(False, " & Ensure_Indent_Param (Expression 
(Last)) & ')';
+
+            when '[' =>
+               --  vector
+               Result := Result & "(True, " & Ensure_Indent_Param (Expression 
(Last + 1));
+               Result := Result & ", " & Ensure_Indent_Param (Expression (Last 
+ 1)) & ')';
+               if Params (Last) /= ']' then
+                  Put_Error
+                    (Error_Message
+                       (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"invalid indent syntax"));
+               end if;
+               Last := Last + 1;
+
+            when others =>
+               --  integer or symbol
+               Result := Result & "(False, " & Ensure_Indent_Param (Expression 
(Last)) & ')';
+
+            end case;
+            Param_Count := Param_Count + 1;
+         end loop;
+
+         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)));
+         end if;
+
+         if Param_Count = 1 then
+            Result := Prefix & "1 => " & Result;
+         else
+            Result := Prefix & Result;
+         end if;
+
+         return -(Result & "))");
+      end Indent_Params;
+
+      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);
+      begin
+         return " (Nonterm, Tokens, " & Params (First .. Second - 1) & ',' &
+           Params (Second .. Params'Last);
+      end Merge_Names_Params;
+
+      function Match_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);
+      begin
+         return " (Lexer, Descriptor, Tokens, " &
+           Params (First .. Second - 1) & ',' &
+           Params (Second .. Params'Last - 1) & ", " &
+           (if Length (Input_Data.Language_Params.End_Names_Optional_Option) > 0
+            then -Input_Data.Language_Params.End_Names_Optional_Option
+            else "False") & ")";
+      end Match_Names_Params;
+
+      procedure Translate_Line (Line : in String)
+      is
+         Last       : constant Integer := Index (Line, Blank_Set);
+         Elisp_Name : constant String  := Line (Line'First + 1 .. Last - 1);
+      begin
+         --  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)) & ";");
+
+         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)) & ";");
+
+         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)) & ";");
+
+         elsif Elisp_Name = "wisi-face-apply-action" then
+            if Length (Face_Line) = 0 then
+               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
+                 Face_Apply_Params (Line (Last + 1 .. Line'Last)) & ";";
+            else
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"multiple face actions"));
+            end if;
+
+         elsif Elisp_Name = "wisi-face-apply-list-action" then
+            if Length (Face_Line) = 0 then
+               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
+                 Face_Apply_Params (Line (Last + 1 .. Line'Last)) & ";";
+            else
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"multiple face actions"));
+            end if;
+
+         elsif Elisp_Name = "wisi-face-mark-action" then
+            if Length (Face_Line) = 0 then
+               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
+                 Face_Mark_Params (Line (Last + 1 .. Line'Last)) & ";";
+            else
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"multiple face actions"));
+            end if;
+
+         elsif Elisp_Name = "wisi-face-remove-action" then
+            if Length (Face_Line) = 0 then
+               Face_Line := +Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
+                 Face_Remove_Params (Line (Last + 1 .. Line'Last)) & ";";
+            else
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"multiple face actions"));
+            end if;
+
+         elsif Elisp_Name = "wisi-indent-action" then
+            if Length (Indent_Action_Line) = 0 then
+               Indent_Action_Line := +"Indent_Action_0" &
+                 Indent_Params (Line (Last + 1 .. Line'Last)) & ";";
+            else
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"multiple indent actions"));
+            end if;
+
+         elsif Elisp_Name = "wisi-indent-action*" then
+            if Length (Indent_Action_Line) = 0 then
+               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;
+            else
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"multiple indent actions"));
+            end if;
+
+         elsif Elisp_Name = "wisi-propagate-name" then
+            if not Check then
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Elisp_Name & " used in action"));
+               return;
+            end if;
+            Check_Lines.Append
+              ("return " & Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
+                 " (Nonterm, Tokens, " & Line (Last + 1 .. Line'Last) & ";");
+
+         elsif Elisp_Name = "wisi-merge-names" then
+            if not Check then
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Elisp_Name & " used in action"));
+               return;
+            end if;
+            Check_Lines.Append
+              ("return " & Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
+                 Merge_Names_Params (Line (Last + 1 .. Line'Last)) & ";");
+
+         elsif Elisp_Name = "wisi-match-names" then
+            if not Check then
+               Put_Error
+                 (Error_Message
+                    (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Elisp_Name & " used in action"));
+               return;
+            end if;
+            Check_Lines.Append
+              ("return " & Elisp_Name_To_Ada (Elisp_Name, False, Trim => 5) &
+                 Match_Names_Params (Line (Last + 1 .. Line'Last)) & ";");
+
+         else
+            Put_Error
+              (Error_Message
+                 (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
"unrecognized elisp action: '" &
+                    Elisp_Name & "'"));
+         end if;
+      end Translate_Line;
+
+   begin
+      for Sexp of Sexps loop
+         begin
+            Translate_Line (Sexp);
+         exception
+         when E : Not_Found =>
+            Put_Error
+              (Error_Message
+                 (Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line, 
Ada.Exceptions.Exception_Message (E)));
+         end;
+      end loop;
+
+      if Check then
+         --  in a check
+         Indent_Line ("function " & Name);
+         Indent_Line (" (Lexer   : access constant 
WisiToken.Lexer.Instance'Class;");
+         Indent_Line ("  Nonterm : in out WisiToken.Recover_Token;");
+         Indent_Line ("  Tokens  : in     WisiToken.Recover_Token_Array)");
+         Indent_Line (" return WisiToken.Semantic_Checks.Check_Status");
+         declare
+            --  Tokens is always referenced.
+            Unref_Lexer   : constant Boolean := (for all Line of Check_Lines 
=> 0 = Index (Line, "Lexer"));
+            Unref_Nonterm : constant Boolean := (for all Line of Check_Lines 
=> 0 = Index (Line, "Nonterm"));
+         begin
+            if Unref_Lexer or Unref_Nonterm 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;
+               Indent_Line ("begin");
+            else
+               Indent_Line ("is begin");
+            end if;
+         end;
+         Indent := Indent + 3;
+         for Line of Check_Lines loop
+            Indent_Line (Line);
+         end loop;
+      else
+         --  In an action
+         Indent_Line ("procedure " & Name);
+         Indent_Line (" (User_Data : in out 
WisiToken.Syntax_Trees.User_Data_Type'Class;");
+         Indent_Line ("  Tree      : in out WisiToken.Syntax_Trees.Tree;");
+         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_Line ("begin");
+         Indent := Indent + 3;
+
+         Indent_Line ("case Parse_Data.Post_Parse_Action is");
+         Indent_Line ("when Navigate =>");
+         if Navigate_Lines.Length > 0 then
+            Indent := Indent + 3;
+            for Line of Navigate_Lines loop
+               Indent_Wrap (Line);
+            end loop;
+            Indent := Indent - 3;
+         else
+            Indent_Line ("   null;");
+         end if;
+
+         Indent_Line ("when Face =>");
+         if Length (Face_Line) > 0 then
+            Indent := Indent + 3;
+            Indent_Wrap (-Face_Line);
+            Indent := Indent - 3;
+         else
+            Indent_Line ("   null;");
+         end if;
+
+         Indent_Line ("when Indent =>");
+         if Length (Indent_Action_Line) > 0 then
+            Indent := Indent + 3;
+            Indent_Wrap (-Indent_Action_Line);
+            Indent := Indent - 3;
+         else
+            Indent_Line ("   null;");
+         end if;
+         Indent_Line ("end case;");
+      end if;
+
+      Indent := Indent - 3;
+      Indent_Line ("end " & Name & ";");
+      New_Line;
+
+   end Create_Ada_Action;
+
+   function Any_Motion_Actions return Boolean
+   is begin
+      for Rule of Input_Data.Tokens.Rules loop
+         for RHS of Rule.Right_Hand_Sides loop
+            for Sexp of Split_Sexp (-RHS.Action, 
Input_Data.Grammar_Lexer.File_Name, RHS.Source_Line) loop
+               declare
+                  Last       : constant Integer := Ada.Strings.Fixed.Index 
(Sexp, Blank_Set);
+                  Elisp_Name : constant String  := Sexp (Sexp'First + 1 .. 
Last - 1);
+               begin
+                  if Elisp_Name = "wisi-motion-action" then
+                     return True;
+                  end if;
+               end;
+            end loop;
+         end loop;
+      end loop;
+      return False;
+   end Any_Motion_Actions;
+
+   procedure Create_Ada_Actions_Body
+     (Action_Names : not null access WisiToken.Names_Array_Array;
+      Check_Names  : not null access WisiToken.Names_Array_Array;
+      Package_Name : in              String)
+   is
+      use Ada.Strings.Unbounded;
+      use Generate_Utils;
+      use WisiToken.Generate;
+
+      File_Name : constant String := Output_File_Name_Root &
+        (case Common_Data.Interface_Kind is
+         when Process => "_process_actions",
+         when Module  => "_module_actions") &
+        ".adb";
+
+      Motion_Actions : constant Boolean := Any_Motion_Actions;
+
+      Body_File : File_Type;
+
+   begin
+      Create (Body_File, Out_File, File_Name);
+      Set_Output (Body_File);
+      Indent := 1;
+      Put_File_Header (Ada_Comment, Use_Tuple => True, Tuple => Tuple);
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+
+      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.
+      end if;
+
+      case Common_Data.Interface_Kind is
+      when Process =>
+         null;
+
+      when Module =>
+         Put_Line ("with Emacs_Module_Aux; use Emacs_Module_Aux;");
+         Put_Line ("with Ada.Exceptions;");
+         Put_Line ("with Ada.Strings.Unbounded;");
+      end case;
+
+      Put_Line ("package body " & Package_Name & " is");
+      Indent := Indent + 3;
+      New_Line;
+
+      if Input_Data.Check_Count > 0 then
+         Indent_Line ("use WisiToken.Semantic_Checks;");
+      end if;
+      if Motion_Actions then
+         Indent_Line ("use all type Motion_Param_Array;");
+      end if;
+      New_Line;
+
+      --  generate Action and Check subprograms.
+
+      for Rule of Input_Data.Tokens.Rules loop
+         --  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
+         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;
+                  begin
+                     Create_Ada_Action (Name, RHS, RHS.Action, Check => False);
+                  end;
+               end if;
+
+               if Length (RHS.Check) > 0 then
+                  declare
+                     Name : constant String := Check_Names 
(LHS_ID)(Prod_Index).all;
+                  begin
+                     Create_Ada_Action (Name, RHS, RHS.Check, Check => True);
+                  end;
+               end if;
+               Prod_Index := Prod_Index + 1;
+            end loop;
+         end;
+      end loop;
+
+      Put_Line ("end " & Package_Name & ";");
+      Close (Body_File);
+
+      Set_Output (Standard_Output);
+
+   end Create_Ada_Actions_Body;
+
+   procedure Create_Ada_Main_Body
+     (Actions_Package_Name : in String;
+      Main_Package_Name    : in String)
+   is
+      use WisiToken.Generate;
+
+      File_Name : constant String := To_Lower (Main_Package_Name) & ".adb";
+      Body_File : File_Type;
+   begin
+      Create (Body_File, Out_File, File_Name);
+      Set_Output (Body_File);
+      Indent := 1;
+      Put_File_Header (Ada_Comment, Use_Tuple => True, Tuple => Tuple);
+      Put_Raw_Code (Ada_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+
+      Put_Line ("with " & Actions_Package_Name & "; use " & 
Actions_Package_Name & ";");
+
+      case Common_Data.Lexer is
+      when None | Elisp_Lexer =>
+         null;
+
+      when re2c_Lexer =>
+         Put_Line ("with WisiToken.Lexer.re2c;");
+         Put_Line ("with " & Output_File_Name_Root & "_re2c_c;");
+
+      end case;
+
+      case Common_Data.Generate_Algorithm is
+      when LR_Generate_Algorithm =>
+         if Tuple.Text_Rep then
+            Put_Line ("with WisiToken.Productions;");
+         end if;
+
+      when Packrat_Generate_Algorithm =>
+         Put_Line ("with WisiToken.Parse;");
+
+      when External =>
+         null;
+      end case;
+
+      Put_Line ("package body " & Main_Package_Name & " is");
+      Indent := Indent + 3;
+      New_Line;
+
+      case Common_Data.Lexer is
+      when None | Elisp_Lexer =>
+         null;
+
+      when re2c_Lexer =>
+         Indent_Line ("package Lexer is new WisiToken.Lexer.re2c");
+         Indent_Line ("  (" & Output_File_Name_Root & "_re2c_c.New_Lexer,");
+         Indent_Line ("   " & Output_File_Name_Root & "_re2c_c.Free_Lexer,");
+         Indent_Line ("   " & Output_File_Name_Root & "_re2c_c.Reset_Lexer,");
+         Indent_Line ("   " & Output_File_Name_Root & "_re2c_c.Next_Token);");
+         New_Line;
+      end case;
+
+      case Common_Data.Generate_Algorithm is
+      when LR_Generate_Algorithm =>
+         LR_Create_Create_Parser (Input_Data, Common_Data, Generate_Data);
+
+      when Packrat_Gen =>
+         WisiToken.BNF.Generate_Packrat (Packrat_Data, Generate_Data);
+         Packrat_Create_Create_Parser (Common_Data, Generate_Data, 
Packrat_Data);
+
+      when Packrat_Proc =>
+         Packrat_Create_Create_Parser (Common_Data, Generate_Data, 
Packrat_Data);
+
+      when External =>
+         External_Create_Create_Grammar (Generate_Data);
+      end case;
+
+      case Common_Data.Interface_Kind is
+      when Process =>
+         null;
+      when Module =>
+         Indent_Line ("Parser : LR_Parser.Instance;");
+         New_Line;
+
+         Indent_Line ("function Parse (Env : Emacs_Env_Access) return 
emacs_module_h.emacs_value");
+         Indent_Line ("is begin");
+         Indent := Indent + 3;
+         Indent_Line ("WisiToken.Trace_Parse := To_Integer (Env, Symbol_Value 
(Env, Elisp_Symbols (Wisi_Debug_ID)));");
+         Indent_Line ("Wisi_Cache_Max := To_Integer (Env, Symbol_Value (Env, 
Elisp_Symbols (Wisi_Cache_Max_ID)));");
+         Indent_Line ("Parser.Reset;");
+         Indent_Line ("Parser.Parse;");
+         Indent_Line ("return Env.Qnil;");
+         Indent := Indent - 3;
+         Indent_Line ("exception");
+         Indent_Line ("when E : WisiToken.Parse_Error | WisiToken.Syntax_Error 
=>");
+         Indent_Line ("   return To_Emacs (Env, 
Ada.Exceptions.Exception_Message (E));");
+         Indent_Line ("when E : others =>");
+         Indent_Line ("   declare");
+         Indent_Line ("      use Ada.Exceptions;");
+         Indent_Line ("   begin");
+         Indent_Line ("      return To_Emacs (Env, Exception_Name (E) & "": "" 
& Exception_Message (E));");
+         Indent_Line ("   end;");
+         Indent_Line ("end Parse;");
+         New_Line;
+
+         Indent_Line ("function Init (Env : Emacs_Env_Access) return 
Interfaces.C.int");
+         Indent_Line ("is");
+         Indent_Line ("   Lexer_Elisp_Symbols : 
Lexers.Elisp_Array_Emacs_Value;");
+         Indent_Line ("begin");
+         Indent_Line ("   " & Main_Package_Name & ".Env := Env;");
+         Indent_Line ("   Emacs_Module_Aux.Init (Env);");
+         Indent_Line ("   for I in Token_Symbols'Range loop");
+         Indent_Line ("      Token_Symbols (I) := Intern_Soft (Env, 
Token_Images (I).all);");
+         Indent_Line ("   end loop;");
+         Indent_Line ("   for I in Elisp_Symbols'Range loop");
+         Indent_Line ("      Elisp_Symbols (I) := Intern_Soft (Env, User_Names 
(I).all);");
+         Indent_Line ("   end loop;");
+         Indent_Line ("   for I in Elisp_Numbers'Range loop");
+         Indent_Line ("      Elisp_Numbers (I) := Env.make_fixnum (Env, 
emacs_module_h.int64_t (I));");
+         Indent_Line ("   end loop;");
+         Indent_Line ("   for I in Lexer_Elisp_Symbols'Range loop");
+         Indent_Line ("      Lexer_Elisp_Symbols (I) := Intern_Soft (Env, 
Lexers.User_Names (I).all);");
+         Indent_Line ("   end loop;");
+         Indent_Line ("   Parser := Create_Parser (Env, 
Lexer_Elisp_Symbols);");
+         Indent_Line ("   return 0;");
+         Indent_Line ("exception");
+         Indent_Line ("when E : others =>");
+         Indent_Line
+           ("   Signal_Error (Env, " &
+              "Ada.Exceptions.Exception_Name (E) & "": "" & 
Ada.Exceptions.Exception_Message (E), Env.Qnil);");
+         Indent_Line ("   return 1;");
+         Indent_Line ("end Init;");
+         New_Line;
+      end case;
+
+      Put_Line ("end " & Main_Package_Name & ";");
+      Close (Body_File);
+
+      Set_Output (Standard_Output);
+
+   end Create_Ada_Main_Body;
+
+   procedure Create_Process_Elisp
+   is
+      use Generate_Utils;
+      use WisiToken.Generate;
+
+      File : File_Type;
+
+      Paren_1_Done : Boolean := False;
+   begin
+      Create (File, Out_File, Output_File_Name_Root & "-process.el");
+      Set_Output (File);
+      Indent := 1;
+
+      Put_Line
+        (";;; " & Output_File_Name_Root & "-process.el --- Generated parser 
support file  -*- lexical-binding:t -*-");
+      Put_Command_Line (Elisp_Comment & "  ", Use_Tuple => True, Tuple => 
Tuple);
+      Put_Raw_Code (Elisp_Comment, Input_Data.Raw_Code (Copyright_License));
+      New_Line;
+      Put_Line ("(require 'wisi-process-parse)");
+      New_Line;
+
+      Indent_Line  ("(defconst " & Output_File_Name_Root & 
"-process-token-table");
+      Indent_Start ("  [");
+      Indent := Indent + 3;
+      for Cursor in All_Tokens (Generate_Data).Iterate loop
+         if Paren_1_Done then
+            Indent_Line (Name (Cursor));
+         else
+            Paren_1_Done := True;
+            Put_Line (Name (Cursor));
+         end if;
+
+      end loop;
+      Indent_Line ("])");
+      Indent := Indent - 3;
+      New_Line;
+
+      Output_Elisp_Common.Indent_Name_Table
+        (Output_File_Name_Root, "process-face-table", 
Input_Data.User_Names.Faces);
+
+      Put_Line ("(provide '" & Output_File_Name_Root & "-process)");
+      Set_Output (Standard_Output);
+      Close (File);
+
+   end Create_Process_Elisp;
+
+   procedure Create_Module_Elisp
+   is
+      use Ada.Strings.Unbounded;
+      use Generate_Utils;
+      use WisiToken.Generate;
+
+      Lower_Package_Name_Root : constant String := To_Lower (File_Name_To_Ada 
(Output_File_Name_Root));
+
+      function To_ID_Image (Name : in Ada.Strings.Unbounded.Unbounded_String) 
return String
+      is begin
+         --  Ada 'Val is 0 origin; Token_ID is 1 origin
+         return Token_ID'Image (-1 + Find_Token_ID (Generate_Data, -Name));
+      end To_ID_Image;
+
+      File : File_Type;
+   begin
+      Create (File, Out_File, Output_File_Name_Root & "-module.el");
+      Set_Output (File);
+      Indent := 1;
+
+      Put_Line (";; generated by WisiToken Wisi from " & 
Input_Data.Grammar_Lexer.File_Name);
+      Put_Command_Line (";; ", Use_Tuple => True, Tuple => Tuple);
+      Put_Line (";;");
+
+      --  don't need the prologue here
+
+      Put_Line ("(require 'wisi-parse-common)");
+      New_Line;
+
+      --  Lexer tables; also contain terminals for wisi-tokens
+      Indent_Keyword_Table (Output_File_Name_Root, "elisp", 
Input_Data.Tokens.Keywords, To_String'Access);
+      Indent_Keyword_Table (Output_File_Name_Root, "module", 
Input_Data.Tokens.Keywords, To_ID_Image'Access);
+      Indent_Token_Table (Output_File_Name_Root, "elisp", 
Input_Data.Tokens.Tokens, To_String'Access);
+      Indent_Token_Table (Output_File_Name_Root, "module", 
Input_Data.Tokens.Tokens, To_ID_Image'Access);
+
+      --  non-terminals. We only need the ones that actually have
+      --  actions, and thus will appear in a call to To_Emacs. But
+      --  Token_Symbols must be indexed by Token_ID, so we declare
+      --  all of them.
+      Indent_Line ("(defconst " & Output_File_Name_Root & "-module-nonterms");
+      Indent_Line (" '(");
+      Indent := Indent + 3;
+      Indent_Line (WisiToken_Accept_Name);
+      for Rule of Input_Data.Tokens.Rules loop
+         Indent_Line (-Rule.Left_Hand_Side);
+      end loop;
+      Indent_Line ("))");
+      Indent := Indent - 3;
+      New_Line;
+
+      Indent_Line
+        ("(cl-defstruct (" & Lower_Package_Name_Root &
+           "-wisi-module-parser (:include wisi-parser)))");
+      New_Line;
+      Indent_Line ("(defun " & Lower_Package_Name_Root & 
"-wisi-module-parser-make (dll-name)");
+      Indent_Line ("  (module-load dll-name)");
+      Indent_Line ("  (make-" & Lower_Package_Name_Root & 
"-wisi-module-parser))");
+      New_Line;
+
+      Indent_Line ("(defvar " & Lower_Package_Name_Root & "-module-lexer 
nil)");
+      Indent_Line
+        ("(declare-function " &
+           Lower_Package_Name_Root &
+           "-wisi-module-parse """ &
+           Lower_Package_Name_Root &
+           "-wisi-module-parse.c"")");
+      New_Line;
+
+      Indent_Line
+        ("(cl-defmethod wisi-parse-current ((parser " &
+           Lower_Package_Name_Root &
+           "-wisi-module-parser))");
+      Indent := Indent + 2;
+      Indent_Line ("(let* ((wisi-lexer " & Lower_Package_Name_Root & 
"-module-lexer)");
+      Indent_Line ("       (result (" & Lower_Package_Name_Root & 
"-wisi-module-parse)))");
+      --  Result is nil for no errors, a string for some error.
+      --  Ada code has already added line:column, but not file name
+      Indent_Line ("  (when result");
+      Indent_Line ("    (signal 'wisi-parse-error (format ""%s:%s"" 
(buffer-name) result)))))");
+      New_Line;
+      Indent := Indent - 2;
+
+      Indent_Line ("(provide '" & Output_File_Name_Root & "-module)");
+      Set_Output (Standard_Output);
+      Close (File);
+
+   end Create_Module_Elisp;
+
+   procedure Create_Module_Aux
+   is
+      use WisiToken.Generate;
+
+      Package_Name_Root       : constant String := File_Name_To_Ada 
(Output_File_Name_Root);
+      Lower_Package_Name_Root : constant String := To_Lower 
(Package_Name_Root);
+
+      File : File_Type;
+   begin
+      Create (File, Out_File, Output_File_Name_Root & 
"_wisi_module_parse.gpr");
+      Set_Output (File);
+      Indent := 1;
+      Put_Line ("-- generated by WisiToken Wisi from " & 
Input_Data.Grammar_Lexer.File_Name);
+      Put_Command_Line ("-- ", Use_Tuple => True, Tuple => Tuple);
+      Indent_Line ("with ""wisi_module_parse_common"";");
+      Indent_Line ("library project " & Package_Name_Root & 
"_Wisi_Module_Parse is");
+      New_Line;
+      Indent := Indent + 3;
+      Indent_Line ("for Languages use (""Ada"");");
+      Indent_Line ("for Source_Dirs use (""../.."", ""."");");
+      New_Line;
+      Indent_Line ("for Source_Files use");
+      Indent_Line ("  (");
+      Indent := Indent + 3;
+      Indent_Line ("""emacs_module_aux.ads"",");
+      Indent_Line ("""emacs_module_aux.adb"",");
+      Indent_Line ("""emacs_module_h.ads"",");
+      Indent_Line ("""fasttoken-lexer-wisi_elisp.adb"",");
+      Indent_Line ("""fasttoken-lexer-wisi_elisp.ads"",");
+      Indent_Line ("""" & Lower_Package_Name_Root & "_module.adb"",");
+      Indent_Line ("""" & Lower_Package_Name_Root & "_module.ads""");
+      Indent := Indent - 3;
+      Indent_Line ("  );");
+      New_Line;
+      Indent_Line ("for Object_Dir use ""libobjsjlj"";");
+      Indent_Line ("for Library_Name use """ & Lower_Package_Name_Root & 
"_wisi_module_parse"";");
+      Indent_Line ("for Library_Dir use ""libsjlj"";");
+      --  This library is linked with *_wisi_module_parse_wrapper.c to
+      --  make a dynamic library
+      Indent_Line ("for Library_Kind use ""static"";");
+      New_Line;
+      Indent_Line ("package Compiler is");
+      Indent := Indent + 3;
+      Indent_Line
+        ("for Default_Switches (""Ada"") use 
Wisi_Module_Parse_Common.Compiler'Default_Switches (""Ada"");");
+
+      --  Grammar files can get very large, so they need some special switches:
+      --
+      --  'Wisi_Module_Parse_Common.Compiler'Default_Switches' includes 
'gnatn', but that hangs
+      Indent_Line ("case Wisi_Module_Parse_Common.Build is");
+      Indent_Line ("when ""Debug"" =>");
+      Indent_Line ("   for Switches (""" & Lower_Package_Name_Root & 
"_module.adb"") use");
+      Indent_Line ("     Wisi_Module_Parse_Common.Compiler.Common_Switches &");
+      Indent_Line ("     Wisi_Module_Parse_Common.Compiler.Standard_Style &");
+      Indent_Line ("     (""-O0"");");
+      Indent_Line ("when ""Normal"" =>");
+      Indent_Line ("   for Switches (""" & Lower_Package_Name_Root & 
"_module.adb"") use");
+      Indent_Line ("     Wisi_Module_Parse_Common.Compiler.Common_Switches &");
+      Indent_Line ("     Wisi_Module_Parse_Common.Compiler.Standard_Style &");
+      Indent_Line ("     (""-O2"");");
+      Indent_Line ("end case;");
+
+      Indent := Indent - 3;
+      Indent_Line ("end Compiler;");
+      New_Line;
+      Indent_Line ("package Builder is");
+      Indent_Line
+        ("   for Default_Switches (""Ada"") use 
Wisi_Module_Parse_Common.Builder'Default_Switches (""Ada"");");
+      Indent_Line ("end Builder;");
+      Indent := Indent - 3;
+      New_Line;
+      Indent_Line ("end " & Package_Name_Root & "_Wisi_Module_Parse;");
+      Set_Output (Standard_Output);
+      Close (File);
+
+      Create (File, Out_File, Output_File_Name_Root & 
"_wisi_module_parse_agg.gpr");
+      Set_Output (File);
+      Indent := 1;
+      Put_Line ("-- generated by WisiToken Wisi from " & 
Input_Data.Grammar_Lexer.File_Name);
+      Put_Command_Line ("-- ", Use_Tuple => True, Tuple => Tuple);
+      Indent_Line ("aggregate project " & Package_Name_Root & 
"_Wisi_Module_Parse_Agg is");
+      Indent_Line ("   for Project_Path use (external (""WISI_FASTTOKEN""));");
+      Indent_Line ("   for Project_files use (""" & Lower_Package_Name_Root & 
"_wisi_module_parse.gpr"");");
+      Indent_Line ("end " & Package_Name_Root & "_Wisi_Module_Parse_Agg;");
+      Set_Output (Standard_Output);
+      Close (File);
+
+      Create (File, Out_File, Output_File_Name_Root & 
"_wisi_module_parse_wrapper.c");
+      Set_Output (File);
+      Indent := 1;
+      Put_Line ("// generated by WisiToken Wisi from " & 
Input_Data.Grammar_Lexer.File_Name);
+      Put_Command_Line ("// ", Use_Tuple => True, Tuple => Tuple);
+      Indent_Line ("//  This file is just a wrapper around the Ada code in");
+      Indent_Line ("//  *_wisi_module_parse.adb; it is needed to call 
adainit.");
+      Indent_Line ("#include <emacs_module.h>");
+      Indent_Line ("int plugin_is_GPL_compatible;");
+      Indent_Line ("extern void adainit(void);");
+      Indent_Line ("extern int " & Lower_Package_Name_Root & 
"_wisi_module_parse_init (emacs_env *env);");
+      Indent_Line ("/* Parse current buffer, using parser in current module. 
*/");
+      Indent_Line ("extern emacs_value " & Lower_Package_Name_Root & 
"_wisi_module_parse (emacs_env *env);");
+      Indent_Line ("static emacs_value Fparse (emacs_env *env, int nargs, 
emacs_value args[])");
+      Indent_Line ("{");
+      Indent_Line ("  return " & Lower_Package_Name_Root & "_wisi_module_parse 
(env);");
+      Indent_Line ("}");
+      New_Line;
+      Indent_Line ("int emacs_module_init (struct emacs_runtime *ert)");
+      Indent_Line ("{");
+      Indent_Line ("  emacs_env *env = ert->get_environment (ert);");
+      Indent_Line
+        ("  env->bind_function (env, """ & Lower_Package_Name_Root &
+           "-wisi-module-parse"", env->make_function (env, 1, 1, Fparse));");
+      Indent_Line ("  adainit();");
+      Indent_Line ("  return " & Lower_Package_Name_Root & 
"_wisi_module_parse_init (env);");
+      Indent_Line ("}");
+      Set_Output (Standard_Output);
+      Close (File);
+   end Create_Module_Aux;
+
+begin
+   case Common_Data.Lexer is
+   when None | re2c_Lexer =>
+      null;
+
+   when Elisp_Lexer =>
+      raise User_Error with WisiToken.Generate.Error_Message
+        (Input_Data.Grammar_Lexer.File_Name, 1, "Ada_Emacs output language 
does not support " &
+           Lexer_Image (Common_Data.Lexer).all & " lexer");
+   end case;
+
+   declare
+      Actions_Package_Name : constant String := File_Name_To_Ada 
(Output_File_Name_Root) &
+        (case Common_Data.Interface_Kind is
+         when Process => "_Process_Actions",
+         when Module  => "_Module_Actions");
+
+      Main_Package_Name : constant String := File_Name_To_Ada 
(Output_File_Name_Root) &
+        (case Common_Data.Interface_Kind is
+         when Process => "_Process",
+         when Module  => "_Module") &
+        Gen_Alg_Name & "_Main";
+   begin
+      Create_Ada_Actions_Body (Generate_Data.Action_Names, 
Generate_Data.Check_Names, Actions_Package_Name);
+
+      Create_Ada_Actions_Spec
+        (Output_File_Name => Output_File_Name_Root &
+           (case Common_Data.Interface_Kind is
+            when Process  => "_process_actions.ads",
+            when Module   => "_module_actions.ads"),
+         Package_Name     => Actions_Package_Name,
+         Input_Data       => Input_Data,
+         Common_Data      => Common_Data,
+         Generate_Data    => Generate_Data);
+
+      if Tuple.Gen_Alg = External then
+         Create_External_Main_Spec (Main_Package_Name, Tuple, Input_Data);
+
+         Create_Ada_Main_Body (Actions_Package_Name, Main_Package_Name);
+      else
+         Create_Ada_Main_Body (Actions_Package_Name, Main_Package_Name);
+
+         Create_Ada_Main_Spec
+           (Output_File_Name  => Output_File_Name_Root & "_" &
+              To_Lower (Interface_Type'Image (Common_Data.Interface_Kind)) &
+              To_Lower (Gen_Alg_Name) & "_main.ads",
+            Main_Package_Name => Main_Package_Name,
+            Common_Data       => Common_Data,
+            Input_Data        => Input_Data);
+      end if;
+   end;
+
+   case Common_Data.Interface_Kind is
+   when Process =>
+      Create_Process_Elisp;
+
+   when Module =>
+      Create_Module_Elisp;
+      Create_Module_Aux;
+   end case;
+exception
+when others =>
+   Set_Output (Standard_Output);
+   raise;
+end WisiToken.BNF.Output_Ada_Emacs;
diff --git a/wisitoken-bnf-output_elisp.adb b/wisitoken-bnf-output_elisp.adb
new file mode 100644
index 0000000..8317ac7
--- /dev/null
+++ b/wisitoken-bnf-output_elisp.adb
@@ -0,0 +1,293 @@
+--  Abstract :
+--
+--  Output Elisp code implementing the grammar defined by the parameters.
+--
+--  Copyright (C) 2012 - 2015, 2017, 2018 Stephen Leake.  All Rights Reserved.
+--
+--  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 MERCHANTABILITY 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.BNF.Generate_Utils;
+with WisiToken.BNF.Output_Elisp_Common;
+with WisiToken.Generate.Packrat;
+with WisiToken.Parse.LR;
+with WisiToken_Grammar_Runtime;
+procedure WisiToken.BNF.Output_Elisp
+  (Input_Data    :         in WisiToken_Grammar_Runtime.User_Data_Type;
+   Elisp_Package :         in String;
+   Generate_Data : aliased in WisiToken.BNF.Generate_Utils.Generate_Data;
+   Packrat_Data  :         in WisiToken.Generate.Packrat.Data;
+   Tuple         :         in Generate_Tuple)
+is
+   pragma Unreferenced (Packrat_Data);
+
+   procedure Action_Table (Table : in WisiToken.Parse.LR.Parse_Table; 
Descriptor : in WisiToken.Descriptor)
+   is
+      use WisiToken.Parse.LR;
+   begin
+      Put ("     [");
+      for State in Table.States'Range loop
+         if State = Table.States'First then
+            Put ("(");
+         else
+            Put ("      (");
+         end if;
+
+         Put ("(default . error)");
+
+         declare
+            Action : Action_Node_Ptr := Table.States (State).Action_List;
+         begin
+            loop
+               declare
+                  Parse_Action_Node : Parse_Action_Node_Ptr := Action.Action;
+                  Conflict          : constant Boolean      := 
Parse_Action_Node.Next /= null;
+               begin
+                  Put (" (" & Image (Action.Symbol, Descriptor) & " . ");
+
+                  if Conflict then
+                     Put ("(");
+                  end if;
+
+                  loop
+                     declare
+                        Parse_Action : Parse_Action_Rec renames 
Parse_Action_Node.Item;
+                     begin
+                        case Parse_Action.Verb is
+                        when Accept_It =>
+                           Put ("accept");
+
+                        when Error =>
+                           Put ("error");
+
+                        when Reduce =>
+                           Put
+                             ("(" & Image (Parse_Action.Production.LHS, 
Descriptor) & " ." &
+                                Integer'Image (Parse_Action.Production.RHS) & 
")");
+
+                        when Shift =>
+                           Put (State_Index'Image (Parse_Action.State));
+
+                        end case;
+
+                        if Parse_Action_Node.Next = null then
+                           if Conflict then
+                              Put (")");
+                           end if;
+                           Put (")");
+                           exit;
+                        else
+                           Put (" ");
+                           Parse_Action_Node := Parse_Action_Node.Next;
+                        end if;
+                     end;
+                  end loop;
+               end;
+
+               Action := Action.Next;
+
+               if Action.Next = null then
+                  if Action.Action.Item.Verb /= Error then
+                     raise SAL.Programmer_Error with "state" &
+                       State_Index'Image (State) & ": default action is not 
error";
+                  end if;
+                  --  let default handle it
+                  Action := null;
+               end if;
+
+               if Action = null then
+                  if State = Table.States'Last then
+                     Put (")");
+                  else
+                     Put_Line (")");
+                  end if;
+                  exit;
+               end if;
+            end loop;
+         end;
+      end loop;
+      Put_Line ("]");
+   end Action_Table;
+
+   procedure Goto_Table (Table : in WisiToken.Parse.LR.Parse_Table; Descriptor 
: in WisiToken.Descriptor)
+   is
+      use WisiToken.Parse.LR;
+
+      subtype Nonterminals is Token_ID range Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal;
+
+      function Count_Nonterminals (List : in Goto_Node_Ptr) return Integer
+      is
+         Item  : Goto_Node_Ptr := List;
+         Count : Integer       := 0;
+      begin
+         while Item /= null loop
+            if Symbol (Item) in Nonterminals then
+               Count := Count + 1;
+            end if;
+            Item := Next (Item);
+         end loop;
+         return Count;
+      end Count_Nonterminals;
+
+   begin
+      Put ("     [");
+      for State in Table.States'Range loop
+         declare
+            Nonterminal_Count : constant Integer := Count_Nonterminals 
(Table.States (State).Goto_List);
+            Gotos             : Goto_Node_Ptr    := Table.States 
(State).Goto_List;
+         begin
+            if Nonterminal_Count = 0 then
+               if State = Table.States'First then
+                  Put_Line ("nil");
+               else
+                  if State = Table.States'Last then
+                     Put ("      nil");
+                  else
+                     Put_Line ("      nil");
+                  end if;
+               end if;
+            else
+               if State = Table.States'First then
+                  Put ("(");
+               else
+                  Put ("      (");
+               end if;
+               loop
+                  if Symbol (Gotos) in Nonterminals then
+                     Put ("(" & Image (Symbol (Gotos), Descriptor) & " ." &
+                            State_Index'Image (Parse.LR.State (Gotos)) & ")");
+                  end if;
+                  Gotos := Next (Gotos);
+                  exit when Gotos = null;
+               end loop;
+               if State = Table.States'Last then
+                  Put (")");
+               else
+                  Put_Line (")");
+               end if;
+            end if;
+         end;
+      end loop;
+      Put ("]");
+   end Goto_Table;
+
+   procedure Output
+     (Elisp_Package : in String;
+      Tokens        : in WisiToken.BNF.Tokens;
+      Parser        : in WisiToken.Parse.LR.Parse_Table_Ptr;
+      Descriptor    : in WisiToken.Descriptor)
+   is
+      use Ada.Strings.Unbounded;
+      use Ada.Containers; -- count_type
+
+      Rule_Length : constant Count_Type := Tokens.Rules.Length;
+      Rule_Count  : Count_Type := 1;
+
+      RHS_Length : Count_Type;
+      RHS_Count  : Count_Type;
+   begin
+      Put_Line ("(defconst " & Elisp_Package & "-elisp-parse-table");
+      Put_Line ("   (wisi-compile-grammar");
+
+      --  nonterminal productions
+      Put ("   '((");
+      for Rule of Tokens.Rules loop
+         if Rule_Count = 1 then
+            Put ("(");
+         else
+            Put ("      (");
+         end if;
+         Put_Line (-Rule.Left_Hand_Side);
+
+         RHS_Length := Rule.Right_Hand_Sides.Length;
+         RHS_Count  := 1;
+         for RHS of Rule.Right_Hand_Sides loop
+            Put ("       ((");
+            for Token of RHS.Tokens loop
+               Put (Token & " ");
+            end loop;
+            if Length (RHS.Action) = 0 then
+               Put (")");
+            else
+               Put_Line (")");
+               Put ("        " & (-RHS.Action));
+            end if;
+
+            if RHS_Count = RHS_Length then
+               Put (")");
+            else
+               Put_Line (")");
+            end if;
+            RHS_Count := RHS_Count + 1;
+         end loop;
+         if Rule_Count = Rule_Length then
+            Put (")");
+         else
+            Put_Line (")");
+         end if;
+         Rule_Count := Rule_Count + 1;
+      end loop;
+      Put_Line (")");
+
+      Action_Table (Parser.all, Descriptor);
+      Goto_Table (Parser.all, Descriptor);
+      Put_Line ("))");
+
+      Put_Line ("  ""Parser table."")");
+   end Output;
+
+   procedure Create_Elisp (Algorithm : in LR_Generate_Algorithm)
+   is
+      use Ada.Strings.Unbounded;
+      File            : File_Type;
+      Elisp_Package_1 : constant String :=
+        (case Algorithm is
+         when LALR => Elisp_Package & "-lalr",
+         when LR1  => Elisp_Package & "-lr1");
+   begin
+      Create (File, Out_File, Elisp_Package_1 & "-elisp.el");
+      Set_Output (File);
+
+      Put_Line (";;; " & Elisp_Package_1 & "-elisp.el --- Generated parser 
support file  -*- lexical-binding:t -*-");
+      Put_Command_Line (Elisp_Comment & "  ", Use_Tuple => True, Tuple => 
Tuple);
+      Put_Raw_Code (Elisp_Comment, Input_Data.Raw_Code (Copyright_License));
+      Put_Raw_Code (Elisp_Comment, Input_Data.Raw_Code (Actions_Spec_Context));
+      New_Line;
+
+      Put_Line ("(require 'wisi)");
+      Put_Line ("(require 'wisi-compile)");
+      Put_Line ("(require 'wisi-elisp-parse)");
+      New_Line;
+      Output_Elisp_Common.Indent_Keyword_Table
+        (Elisp_Package_1, "elisp", Input_Data.Tokens.Keywords, 
To_String'Access);
+      New_Line;
+      Output_Elisp_Common.Indent_Token_Table (Elisp_Package_1, "elisp", 
Input_Data.Tokens.Tokens, To_String'Access);
+      New_Line;
+      Output (Elisp_Package_1, Input_Data.Tokens, 
Generate_Data.LR_Parse_Table, Generate_Data.Descriptor.all);
+      New_Line;
+      Put_Line ("(provide '" & Elisp_Package_1 & "-elisp)");
+      Put_Line (";; end of file");
+      Close (File);
+
+      Set_Output (Standard_Output);
+   end Create_Elisp;
+
+begin
+   Create_Elisp (Tuple.Gen_Alg);
+
+   if WisiToken.Trace_Generate > 0 then
+      WisiToken.BNF.Generate_Utils.Put_Stats (Input_Data, Generate_Data);
+   end if;
+end WisiToken.BNF.Output_Elisp;
diff --git a/wisitoken-bnf-output_elisp_common.adb 
b/wisitoken-bnf-output_elisp_common.adb
new file mode 100644
index 0000000..fc37469
--- /dev/null
+++ b/wisitoken-bnf-output_elisp_common.adb
@@ -0,0 +1,145 @@
+--  Abstract :
+--
+--  See spec
+--
+--  Copyright (C) 2012, 2013, 2015, 2017, 2018 Stephen Leake.  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
+--  published by the Free Software Foundation; either version 3, or (at
+--  your option) any later version. This program is distributed in the
+--  hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+--  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+--  PURPOSE. See the GNU General Public License for more details. You
+--  should have received a copy of the GNU General Public License
+--  distributed with this program; see file COPYING. If not, write to
+--  the Free Software Foundation, 51 Franklin Street, Suite 500, Boston,
+--  MA 02110-1335, USA.
+
+pragma License (GPL);
+
+with Ada.Text_IO;
+with WisiToken.Generate;
+package body WisiToken.BNF.Output_Elisp_Common is
+
+   function Find_Elisp_ID (List : in String_Lists.List; Elisp_Name : in 
String) return Integer
+   is
+      I : Integer := 0; -- match elisp array
+   begin
+      for Name of List loop
+         if Name = Elisp_Name then
+            return I;
+         end if;
+         I := I + 1;
+      end loop;
+      raise Not_Found with "unknown elisp name: '" & Elisp_Name & "'";
+   end Find_Elisp_ID;
+
+   function Elisp_Name_To_Ada
+     (Elisp_Name : in String;
+      Append_ID  : in Boolean;
+      Trim       : in Integer)
+     return String
+   is
+      Result : String := Elisp_Name (Elisp_Name'First + Trim .. 
Elisp_Name'Last);
+   begin
+      Result (Result'First) := To_Upper (Result (Result'First));
+      for I in Result'Range loop
+         if Result (I) = '-' then
+            Result (I) := '_';
+            Result (I + 1) := To_Upper (Result (I + 1));
+         elsif Result (I) = '_' then
+            Result (I + 1) := To_Upper (Result (I + 1));
+         end if;
+      end loop;
+      if Append_ID then
+         return Result & "_ID"; -- Some elisp names may be Ada reserved words;
+      else
+         return Result;
+      end if;
+   end Elisp_Name_To_Ada;
+
+   procedure Indent_Keyword_Table
+     (Output_File_Root : in     String;
+      Label            : in     String;
+      Keywords         : in     String_Pair_Lists.List;
+      Image            : access function (Name : in 
Ada.Strings.Unbounded.Unbounded_String) return String)
+   is
+      use Ada.Text_IO;
+      use WisiToken.Generate;
+   begin
+      Indent_Line ("(defconst " & Output_File_Root & "-" & Label & 
"-keyword-table-raw");
+      Indent_Line ("  '(");
+      Indent := Indent + 3;
+      for Pair of Keywords loop
+         Indent_Line ("(" & (-Pair.Value) & " . " & Image (Pair.Name) & ")");
+      end loop;
+      Indent_Line ("))");
+      Indent := Indent - 3;
+   end Indent_Keyword_Table;
+
+   procedure Indent_Token_Table
+     (Output_File_Root : in     String;
+      Label            : in     String;
+      Tokens           : in     Token_Lists.List;
+      Image            : access function (Name : in 
Ada.Strings.Unbounded.Unbounded_String) return String)
+   is
+      use Ada.Strings.Unbounded;
+      use Ada.Text_IO;
+      use WisiToken.Generate;
+   begin
+      Indent_Line ("(defconst " & Output_File_Root & "-" & Label & 
"-token-table-raw");
+      Indent_Line ("  '(");
+      Indent := Indent + 3;
+      for Kind of Tokens loop
+         --  We don't use All_Tokens.Iterate here, because we need the
+         --  Kind/token nested list structure, and the order is not important.
+         if not (-Kind.Kind = "line_comment" or -Kind.Kind = "whitespace") then
+            Indent_Line ("(""" & (-Kind.Kind) & """");
+            Indent := Indent + 1;
+            for Token of Kind.Tokens loop
+               if 0 = Length (Token.Value) then
+                  Indent_Line ("(" & Image (Token.Name) & ")");
+               else
+                  if -Kind.Kind = "number" then
+                     --  allow for (<token> <number-p> <require>)
+                     Indent_Line ("(" & Image (Token.Name) & " " & 
(-Token.Value) & ")");
+                  elsif -Kind.Kind = "symbol" or
+                    -Kind.Kind = "string-double" or
+                    -Kind.Kind = "string-single"
+                  then
+                     --  value not used by elisp
+                     Indent_Line ("(" & Image (Token.Name) & " . """")");
+                  else
+                     Indent_Line ("(" & Image (Token.Name) & " . " & 
(-Token.Value) & ")");
+                  end if;
+               end if;
+            end loop;
+            Indent_Line (")");
+            Indent := Indent - 1;
+         end if;
+      end loop;
+      Indent_Line ("))");
+      Indent := Indent - 3;
+   end Indent_Token_Table;
+
+   procedure Indent_Name_Table
+     (Output_File_Root : in     String;
+      Label            : in     String;
+      Names            : in     String_Lists.List)
+   is
+      use Ada.Text_IO;
+      use WisiToken.Generate;
+   begin
+      Indent_Line ("(defconst " & Output_File_Root & "-" & Label);
+      Indent_Line ("  [");
+      Indent := Indent + 3;
+      for Name of Names loop
+         Indent_Line (Name);
+      end loop;
+      Indent_Line ("])");
+      Indent := Indent - 3;
+      New_Line;
+   end Indent_Name_Table;
+
+end WisiToken.BNF.Output_Elisp_Common;
diff --git a/wisitoken-bnf-output_elisp_common.ads 
b/wisitoken-bnf-output_elisp_common.ads
new file mode 100644
index 0000000..cb4e94b
--- /dev/null
+++ b/wisitoken-bnf-output_elisp_common.ads
@@ -0,0 +1,49 @@
+--  Abstract :
+--
+--  Subprograms common to Output_Elisp and Output_Ada_Emacs
+--
+--  Copyright (C) 2012, 2013, 2015, 2017, 2018 Stephen Leake. All Rights 
Reserved.
+--
+--  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 MERCHANTABILITY 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.BNF.Output_Elisp_Common is
+
+   function Find_Elisp_ID (List : in WisiToken.BNF.String_Lists.List; 
Elisp_Name : in String) return Integer;
+
+   function Elisp_Name_To_Ada
+     (Elisp_Name : in String;
+      Append_ID  : in Boolean;
+      Trim       : in Integer)
+     return String;
+   --  Drop Trim chars from beginning of Elisp_Name, capitalize.
+
+   procedure Indent_Keyword_Table
+     (Output_File_Root : in     String;
+      Label            : in     String;
+      Keywords         : in     String_Pair_Lists.List;
+      Image            : access function (Name : in 
Ada.Strings.Unbounded.Unbounded_String) return String);
+
+   procedure Indent_Token_Table
+     (Output_File_Root : in     String;
+      Label            : in     String;
+      Tokens           : in     Token_Lists.List;
+      Image            : access function (Name : in 
Ada.Strings.Unbounded.Unbounded_String) return String);
+
+   procedure Indent_Name_Table
+     (Output_File_Root : in     String;
+      Label            : in     String;
+      Names            : in     String_Lists.List);
+
+end WisiToken.BNF.Output_Elisp_Common;
diff --git a/wisitoken-bnf-utils.adb b/wisitoken-bnf-utils.adb
new file mode 100644
index 0000000..4ff91b6
--- /dev/null
+++ b/wisitoken-bnf-utils.adb
@@ -0,0 +1,45 @@
+--  Abstract :
+--
+--  See spec
+--
+--  Copyright (C) 2012, 2013, 2015, 2017, 2018 Stephen Leake.  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
+--  published by the Free Software Foundation; either version 3, or (at
+--  your option) any later version. This program is distributed in the
+--  hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+--  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+--  PURPOSE. See the GNU General Public License for more details. You
+--  should have received a copy of the GNU General Public License
+--  distributed with this program; see file COPYING. If not, write to
+--  the Free Software Foundation, 51 Franklin Street, Suite 500, Boston,
+--  MA 02110-1335, USA.
+
+pragma License (GPL);
+
+package body WisiToken.BNF.Utils is
+
+   function Strip_Quotes (Item : in String) return String
+   is begin
+      if Item'Length < 2 then
+         return Item;
+      else
+         return Item
+           ((if Item (Item'First) = '"' then Item'First + 1 else Item'First) ..
+              (if Item (Item'Last) = '"' then Item'Last - 1 else Item'Last));
+      end if;
+   end Strip_Quotes;
+
+   function Strip_Parens (Item : in String) return String
+   is begin
+      if Item'Length < 2 then
+         return Item;
+      else
+         return Item
+           ((if Item (Item'First) = '(' then Item'First + 1 else Item'First) ..
+              (if Item (Item'Last) = ')' then Item'Last - 1 else Item'Last));
+      end if;
+   end Strip_Parens;
+
+end WisiToken.BNF.Utils;
diff --git a/wisitoken-bnf-utils.ads b/wisitoken-bnf-utils.ads
new file mode 100644
index 0000000..4d82f32
--- /dev/null
+++ b/wisitoken-bnf-utils.ads
@@ -0,0 +1,29 @@
+--  Abstract :
+--
+--  Utilities for generating source code from  Wisi source files
+--
+--  Copyright (C) 2012, 2013, 2015, 2017, 2018 Stephen Leake. All Rights 
Reserved.
+--
+--  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 MERCHANTABILITY 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.BNF.Utils is
+
+   function Strip_Quotes (Item : in String) return String;
+   --  Remove leading and trailing '"', if any.
+
+   function Strip_Parens (Item : in String) return String;
+   --  Remove leading and trailing '()', if any.
+
+end WisiToken.BNF.Utils;
diff --git a/wisitoken-bnf.adb b/wisitoken-bnf.adb
new file mode 100644
index 0000000..cd49de7
--- /dev/null
+++ b/wisitoken-bnf.adb
@@ -0,0 +1,337 @@
+--  Abstract :
+--
+--  see spec
+--
+--  Copyright (C) 2012 - 2015, 2017, 2018 Stephen Leake.  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
+--  published by the Free Software Foundation; either version 3, or (at
+--  your option) any later version. This program is distributed in the
+--  hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+--  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+--  PURPOSE. See the GNU General Public License for more details. You
+--  should have received a copy of the GNU General Public License
+--  distributed with this program; see file COPYING. If not, write to
+--  the Free Software Foundation, 51 Franklin Street, Suite 500, Boston,
+--  MA 02110-1335, USA.
+
+pragma License (GPL);
+
+with Ada.Command_Line;
+with Ada.Directories;
+with Ada.Text_IO;
+with Ada.Strings.Fixed;
+package body WisiToken.BNF is
+
+   procedure Add
+     (Set   : in out Generate_Set_Access;
+      Tuple : in     Generate_Tuple)
+   is
+      Prev : Generate_Set_Access := Set;
+      Last : constant Integer    := (if Prev = null then 1 else 
Prev.all'Length + 1);
+   begin
+      Set := new Generate_Set (1 .. Last);
+      for I in 1 .. Last - 1 loop
+         Set (I) := Prev (I);
+      end loop;
+      Set (Last) := Tuple;
+      Free (Prev);
+   end Add;
+
+   function To_Output_Language (Item : in String) return Output_Language
+   is begin
+      for I in Output_Language loop
+         if To_Lower (Output_Language_Image (I).all) = To_Lower (Item) then
+            return I;
+         end if;
+      end loop;
+      raise User_Error with "invalid output language name: '" & Item & "'";
+   end To_Output_Language;
+
+   function To_Lexer (Item : in String) return Lexer_Type
+   is begin
+      for I in Valid_Lexer loop
+         if Lexer_Image (I).all = To_Lower (Item) then
+            return I;
+         end if;
+      end loop;
+      raise User_Error with "invalid lexer name: '" & Item & "'";
+   end To_Lexer;
+
+   function Split_Lines (Item : in String) return String_Lists.List
+   is
+      CR : Character renames ASCII.CR;
+      LF : Character renames ASCII.LF;
+
+      Result    : WisiToken.BNF.String_Lists.List;
+      I         : Integer   := Item'First;
+      First     : Integer   := Item'First;
+      Last_Char : Character := ' ';
+   begin
+      loop
+         exit when I > Item'Last;
+         if Item (I) = LF then
+            Result.Append (Item (First .. I - (if Last_Char = CR then 2 else 
1)));
+            First := I + 1;
+
+         elsif I = Item'Last then
+            Result.Append (Item (First .. I));
+         end if;
+
+         Last_Char := Item (I);
+
+         I := I + 1;
+      end loop;
+      return Result;
+   end Split_Lines;
+
+   function Trim (Item : in String_Lists.List; Comment_Start : in String) 
return String_Lists.List
+   is
+      use Ada.Strings;
+      use Ada.Strings.Fixed;
+      Result : String_Lists.List;
+      Comment : Integer;
+
+      procedure Maybe_Append (Line : in String)
+      is begin
+         if Line'Length > 0 then
+            Result.Append (Line);
+         end if;
+      end Maybe_Append;
+
+   begin
+      for Line of Item loop
+         Comment := Index (Line, Comment_Start, Going => Backward);
+         if Comment /= 0 then
+            Maybe_Append (Trim (Line (Line'First .. Comment - 1), Both));
+         else
+            Maybe_Append (Trim (Line, Both));
+         end if;
+      end loop;
+      return Result;
+   end Trim;
+
+   procedure Put_Raw_Code
+     (Comment_Syntax : in String_2;
+      Code           : in String_Lists.List;
+      Comment_Only   : in Boolean := False)
+   is
+      use Ada.Text_IO;
+      Real_Comment_Only : Boolean := Comment_Only;
+   begin
+      for Line of Code loop
+         if Line'Length >= 2 and then
+           ((Line (Line'First) = Line (Line'First + 1)) and
+              Line (Line'First) /= ' ')
+         then
+            --  The line is a comment.
+            Real_Comment_Only := Real_Comment_Only or Line (Line'First .. 
Line'First + 1) /= Comment_Syntax;
+
+            Put_Line (Comment_Syntax & Line (Line'First + 2 .. Line'Last));
+
+         elsif Comment_Syntax = Elisp_Comment and (Line'Length > 0 and then 
Line (Line'First) /= '(') then
+            null;
+
+         elsif not Comment_Only then
+            Put_Line (Line);
+         end if;
+      end loop;
+   end Put_Raw_Code;
+
+   procedure Put_File_Header
+     (Comment_Syntax : in String_2;
+      Emacs_Mode     : in String         := "";
+      Use_Tuple      : in Boolean        := False;
+      Tuple          : in Generate_Tuple := (others => <>))
+   is
+      use Ada.Text_IO;
+   begin
+      Put_Line (Comment_Syntax & "  generated parser support file." & 
Emacs_Mode);
+      Put_Command_Line  (Comment_Syntax & "  ", Use_Tuple, Tuple);
+      Put_Line (Comment_Syntax);
+   end Put_File_Header;
+
+   function Is_Present (List : in WisiToken.BNF.String_Pair_Lists.List; Name : 
in String) return Boolean
+   is
+      use all type Ada.Strings.Unbounded.Unbounded_String;
+   begin
+      for Pair of List loop
+         if Pair.Name = Name then
+            return True;
+         end if;
+      end loop;
+      return False;
+   end Is_Present;
+
+   function Value (List : in WisiToken.BNF.String_Pair_Lists.List; Name : in 
String) return String
+   is
+      use all type Ada.Strings.Unbounded.Unbounded_String;
+   begin
+      for Pair of List loop
+         if Pair.Name = Name then
+            return -Pair.Value;
+         end if;
+      end loop;
+      raise Not_Found;
+   end Value;
+
+   function Count (Tokens : in Token_Lists.List) return Integer
+   is
+      Result : Integer := 0;
+   begin
+      for Kind of Tokens loop
+         Result := Result + Integer (Kind.Tokens.Length);
+      end loop;
+      return Result;
+   end Count;
+
+   procedure Add_Token
+     (Tokens : in out Token_Lists.List;
+      Kind   : in     String;
+      Name   : in     String;
+      Value  : in     String)
+   is
+      use type Ada.Strings.Unbounded.Unbounded_String;
+   begin
+      for Token_Kind of Tokens loop
+         if Token_Kind.Kind = Kind then
+            Token_Kind.Tokens.Append ((+Name, +Value));
+            return;
+         end if;
+      end loop;
+
+      --  Kind not found; add it
+      declare
+         Temp : String_Pair_Lists.List;
+      begin
+         Temp.Append ((+Name, +Value));
+         Tokens.Append ((+Kind, Temp));
+      end;
+   end Add_Token;
+
+   function Is_In (Tokens : in Token_Lists.List; Kind : in String) return 
Boolean
+   is begin
+      for Token of Tokens loop
+         if -Token.Kind = Kind then
+            return True;
+         end if;
+      end loop;
+      return False;
+   end Is_In;
+
+   function Is_In
+     (Tokens : in Token_Lists.List;
+      Kind   : in String;
+      Value  : in String)
+     return Boolean
+   is begin
+      for Token of Tokens loop
+         if -Token.Kind = Kind then
+            for Item of Token.Tokens loop
+               if -Item.Value = Value then
+                  return True;
+               end if;
+            end loop;
+         end if;
+      end loop;
+      return False;
+   end Is_In;
+
+   function Is_Present (Rules : in Rule_Lists.List; LHS : in String) return 
Boolean
+   is
+      use Rule_Lists;
+
+      Found : Boolean := False;
+
+      procedure Process (Position : in Cursor)
+      is begin
+         if -Rules (Position).Left_Hand_Side = LHS then
+            Found := True;
+         end if;
+      end Process;
+
+   begin
+      Rules.Iterate (Process'Access);
+      return Found;
+   end Is_Present;
+
+   function "+" (List : in String_Lists.List; Item : in String) return 
String_Lists.List
+   is
+      Result : String_Lists.List := List;
+   begin
+      Result.Append (Item);
+      return Result;
+   end "+";
+
+   function String_To_String_List (Item : in String) return String_Lists.List
+   is
+      Result : String_Lists.List;
+   begin
+      Result.Append (Item);
+      return Result;
+   end String_To_String_List;
+
+   function RHS_To_RHS_List (Item : in RHS_Type) return RHS_Lists.List
+   is
+      Result : RHS_Lists.List;
+   begin
+      Result.Append (Item);
+      return Result;
+   end RHS_To_RHS_List;
+
+   function "+" (List : in RHS_Lists.List; Item : in RHS_Type) return 
RHS_Lists.List
+   is
+      Result : RHS_Lists.List := List;
+   begin
+      Result.Append (Item);
+      return Result;
+   end "+";
+
+   procedure Put_Command_Line
+     (Comment_Prefix : in String;
+      Use_Tuple      : in Boolean        := False;
+      Tuple          : in Generate_Tuple := (others => <>))
+   is
+      use Ada.Command_Line;
+      use Ada.Text_IO;
+
+      Max_Line_Length : constant := 120;
+      Col : Integer := 0;
+
+      procedure Put (Item : in String; Leading_Space : in Boolean)
+      is begin
+         if Col > 0 and Col + Item'Length + 1 > Max_Line_Length then
+            New_Line;
+            Col := Comment_Prefix'Length;
+            Put (Comment_Prefix);
+         else
+            if Leading_Space then
+               Put (" ");
+               Col := Col + 1;
+            end if;
+         end if;
+
+         Col := Col + Item'Length;
+         Put (Item);
+      end Put;
+   begin
+      Put (Comment_Prefix & "command line:", False);
+      Put (Ada.Directories.Simple_Name (Command_Name), True);
+      if Use_Tuple then
+         Put (" --generate " & Generate_Algorithm'Image (Tuple.Gen_Alg) & " " &
+                Output_Language_Image (Tuple.Out_Lang).all &
+                (if Tuple.Lexer /= None then " " & Lexer_Image 
(Tuple.Lexer).all else "") &
+                (if Tuple.Interface_Kind /= None then " " & 
Interface_Type'Image (Tuple.Interface_Kind) else "") &
+                (if Tuple.Text_Rep then " text_rep" else "") &
+                " " & Argument (Argument_Count), --  .wy file
+              True);
+      else
+         for I in 1 .. Argument_Count loop
+            Put (Argument (I), True);
+         end loop;
+      end if;
+      New_Line;
+   end Put_Command_Line;
+
+end WisiToken.BNF;
diff --git a/wisitoken-bnf.ads b/wisitoken-bnf.ads
new file mode 100644
index 0000000..60fb96a
--- /dev/null
+++ b/wisitoken-bnf.ads
@@ -0,0 +1,310 @@
+--  Abstract :
+--
+--  Root package for generating a parser from a BNF source file; see [2]
+--
+--  The input file syntax is based on BNF syntax [1] with declarations
+--  and grammar actions.
+--
+--  The Elisp and Ada_Emacs output languages are for use with the
+--  Emacs wisi package.
+--
+--  Reference :
+--
+--  [1] https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form
+--  [2] http://www.nongnu.org/ada-mode/wisi/wisi-user_guide.html, (info 
"(wisi-user_guide)Top")
+--
+--  Copyright (C) 2012 - 2015, 2017, 2018 Stephen Leake.  All Rights Reserved.
+--
+--  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 MERCHANTABILITY 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.Containers.Doubly_Linked_Lists;
+with Ada.Containers.Indefinite_Doubly_Linked_Lists;
+with Ada.Strings.Unbounded;
+with Ada.Unchecked_Deallocation;
+with WisiToken;
+package WisiToken.BNF is
+
+   --  See also WisiToken exceptions
+
+   Not_Found : exception;
+   --  something not found; should be handled and converted to Syntax_ or 
Grammar_Error
+
+   type Generate_Algorithm is (None, LALR, LR1, Packrat_Gen, Packrat_Proc, 
External);
+   subtype Valid_Generate_Algorithm is Generate_Algorithm range LALR .. 
Generate_Algorithm'Last;
+   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"),
+      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.
+
+   type Generate_Algorithm_Set is array (Generate_Algorithm) of Boolean;
+   type Generate_Algorithm_Set_Access is access Generate_Algorithm_Set;
+
+   type Output_Language is (Ada_Lang, Ada_Emacs_Lang, Elisp_Lang);
+   subtype Ada_Output_Language is Output_Language range Ada_Lang .. 
Ada_Emacs_Lang;
+   --  _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 :=
+     (Ada_Lang       => new String'("Ada"),
+      Ada_Emacs_Lang => new String'("Ada_Emacs"),
+      Elisp_Lang     => new String'("elisp"));
+
+   function To_Output_Language (Item : in String) return Output_Language;
+   --  Raises User_Error for invalid Item
+
+   type Lexer_Type is (None, Elisp_Lexer, re2c_Lexer);
+   subtype Valid_Lexer is Lexer_Type range Elisp_Lexer .. Lexer_Type'Last;
+   --  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 :=
+     (None        => new String'("none"),
+      Elisp_Lexer => new String'("elisp"),
+      re2c_Lexer  => new String'("re2c"));
+
+   function To_Lexer (Item : in String) return Lexer_Type;
+   --  Raises User_Error for invalid Item
+
+   type Lexer_Set is array (Lexer_Type) of Boolean;
+
+   type Lexer_Generate_Algorithm_Set is array (Lexer_Type) of 
Generate_Algorithm_Set;
+   --  %if lexer change change the generated parse table
+
+   type Interface_Type is (None, Process, Module);
+   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;
+   end record;
+
+   type Generate_Set is array (Natural range <>) of Generate_Tuple;
+   type Generate_Set_Access is access Generate_Set;
+   procedure Free is new Ada.Unchecked_Deallocation (Generate_Set, 
Generate_Set_Access);
+
+   procedure Add
+     (Set   : in out Generate_Set_Access;
+      Tuple : in     Generate_Tuple);
+
+   package String_Lists is new Ada.Containers.Indefinite_Doubly_Linked_Lists 
(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;
+   end record;
+
+   type Raw_Code_Location is
+     (Copyright_License,
+      Actions_Spec_Context, Actions_Spec_Pre, Actions_Spec_Post,
+      Actions_Body_Context, Actions_Body_Pre, Actions_Body_Post);
+   --  So far we have not needed raw code other than license in the main
+   --  package.
+
+   type Raw_Code is array (Raw_Code_Location) of String_Lists.List;
+
+   subtype String_2 is String (1 .. 2);
+
+   Ada_Comment   : constant String_2 := "--";
+   C_Comment     : constant String_2 := "//";
+   Elisp_Comment : constant String_2 := ";;";
+
+   function Split_Lines (Item : in String) return String_Lists.List;
+
+   function Trim (Item : in String_Lists.List; Comment_Start : in String) 
return String_Lists.List;
+   --  From each element, delete trailing comments starting with
+   --  Comment_Start; delete leading and trailing spaces.
+
+   procedure Put_Raw_Code
+     (Comment_Syntax : in String_2;
+      Code           : in String_Lists.List;
+      Comment_Only   : in Boolean := False);
+   --  Output Code to Ada.Text_IO.Current_Output.
+   --
+   --  If first two characters of a line are the same and not ' ', it is
+   --  assumed to be a comment; ensure the output line has
+   --  Comment_Syntax.
+   --
+   --  If Comment_Only is True, or if the comment syntax used in Code
+   --  does not equal Comment_Syntax, only output comment lines.
+   --
+   --  If Comment_Syntax is Elisp_Comment, only output lines that are
+   --  valid elisp comments or forms (ie start with ';;' or '(').
+   --
+   --  Otherwise output all lines.
+
+   procedure Put_File_Header
+     (Comment_Syntax : in String_2;
+      Emacs_Mode     : in String         := "";
+      Use_Tuple      : in Boolean        := False;
+      Tuple          : in Generate_Tuple := (others => <>));
+   --  Output "parser support file <emacs_mode> /n command line: " comment to 
Ada.Text_IO.Current_Output.
+
+   type String_Pair_Type is record
+      Name  : aliased Ada.Strings.Unbounded.Unbounded_String;
+      Value : Ada.Strings.Unbounded.Unbounded_String;
+   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 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;
+   end record;
+
+   type Token_Kind_Type is record
+      Kind   : Ada.Strings.Unbounded.Unbounded_String;
+      Tokens : String_Pair_Lists.List;
+   end record;
+
+   package Token_Lists is new Ada.Containers.Doubly_Linked_Lists 
(Token_Kind_Type);
+
+   function Count (Tokens : in Token_Lists.List) return Integer;
+   --  Count of all leaves.
+
+   procedure Add_Token
+     (Tokens : in out Token_Lists.List;
+      Kind   : in     String;
+      Name   : in     String;
+      Value  : in     String);
+   --  Add Name, Value to Kind list in Tokens.
+
+   function Is_In (Tokens : in Token_Lists.List; Kind : in String) return 
Boolean;
+   function Is_In
+     (Tokens : in Token_Lists.List;
+      Kind   : in String;
+      Value  : in String)
+     return Boolean;
+
+   type Conflict is record
+      Source_Line : WisiToken.Line_Number_Type;
+      Action_A    : Ada.Strings.Unbounded.Unbounded_String;
+      LHS_A       : Ada.Strings.Unbounded.Unbounded_String;
+      Action_B    : Ada.Strings.Unbounded.Unbounded_String;
+      LHS_B       : Ada.Strings.Unbounded.Unbounded_String;
+      On          : Ada.Strings.Unbounded.Unbounded_String;
+   end record;
+
+   package Conflict_Lists is new Ada.Containers.Doubly_Linked_Lists (Conflict);
+
+   type RHS_Type is record
+      Tokens      : String_Lists.List;
+      Action      : Ada.Strings.Unbounded.Unbounded_String;
+      Check       : Ada.Strings.Unbounded.Unbounded_String;
+      Source_Line : WisiToken.Line_Number_Type := 
WisiToken.Invalid_Line_Number;
+   end record;
+   package RHS_Lists is new Ada.Containers.Doubly_Linked_Lists (RHS_Type, "=");
+
+   type Rule_Type is record
+      Left_Hand_Side   : aliased Ada.Strings.Unbounded.Unbounded_String;
+      Right_Hand_Sides : RHS_Lists.List;
+      Source_Line      : WisiToken.Line_Number_Type;
+   end record;
+
+   package Rule_Lists is new Ada.Containers.Doubly_Linked_Lists (Rule_Type);
+
+   function Is_Present (Rules : in Rule_Lists.List; LHS : in String) return 
Boolean;
+
+   type Tokens is record
+      Non_Grammar      : Token_Lists.List;
+      Keywords         : String_Pair_Lists.List;
+      Tokens           : Token_Lists.List;
+      Rules            : Rule_Lists.List;
+      --  Rules included here because they define the nonterminal tokens, as
+      --  well as the productions.
+
+      Regexps : String_Pair_Lists.List;
+      --  Regexps included here because they are used in defining the
+      --  Tokens.
+   end record;
+
+   type User_Names is record
+      --  Specified in grammar file declarations, used in other declarations
+      --  or actions. Faces, Indents only used if .wy action language is
+      --  elisp and output language is not elisp.
+
+      Faces   : String_Lists.List;      -- %elisp_face
+      Indents : String_Pair_Lists.List; -- %elisp_indent
+      Regexps : String_Pair_Lists.List; -- %regexp_name
+   end record;
+
+   function "+" (Item : in String) return 
Ada.Strings.Unbounded.Unbounded_String
+     renames Ada.Strings.Unbounded.To_Unbounded_String;
+
+   function "-" (Item : in Ada.Strings.Unbounded.Unbounded_String) return 
String
+     renames Ada.Strings.Unbounded.To_String;
+
+   function To_Lower (Item : in String) return String
+     renames Ada.Characters.Handling.To_Lower;
+
+   function To_Upper (Item : in String) return String
+     renames Ada.Characters.Handling.To_Upper;
+
+   function To_Upper (Item : in Character) return Character
+     renames Ada.Characters.Handling.To_Upper;
+
+   function "+" (List : in String_Lists.List; Item : in String) return 
String_Lists.List;
+
+   function String_To_String_List (Item : in String) return String_Lists.List;
+   function "+" (Item : in String) return String_Lists.List renames 
String_To_String_List;
+
+   function RHS_To_RHS_List (Item : in RHS_Type) return RHS_Lists.List;
+   function "+" (Item : in RHS_Type) return RHS_Lists.List renames 
RHS_To_RHS_List;
+
+   function "+" (List : in RHS_Lists.List; Item : in RHS_Type) return 
RHS_Lists.List;
+
+   function Image (Item : in Boolean) return String
+     is (if Item then "True" else "False");
+   --  Match casing in Standard.
+
+   procedure Put_Command_Line
+     (Comment_Prefix : in String;
+      Use_Tuple      : in Boolean        := False;
+      Tuple          : in Generate_Tuple := (others => <>));
+   --  Put command line to current output; indicate current tuple.
+
+end WisiToken.BNF;
diff --git a/wisitoken-gen_token_enum.adb b/wisitoken-gen_token_enum.adb
new file mode 100644
index 0000000..87e5f6d
--- /dev/null
+++ b/wisitoken-gen_token_enum.adb
@@ -0,0 +1,133 @@
+--  Abstract :
+--
+--  See spec
+--
+--  Copyright (C) 2017, 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 (GPL);
+
+with Ada.Characters.Handling;
+with WisiToken.Wisi_Ada;
+package body WisiToken.Gen_Token_Enum is
+
+   function Token_Enum_Image return Token_ID_Array_String
+   is
+      use Ada.Characters.Handling;
+      Result : Token_ID_Array_String (Token_ID'First .. +Last_Nonterminal);
+   begin
+      for I in Token_Enum_ID loop
+         if I <= Last_Terminal then
+            Result (+I) := new String'(Token_Enum_ID'Image (I));
+         else
+            Result (+I) := new String'(To_Lower (Token_Enum_ID'Image (I)));
+         end if;
+      end loop;
+      return Result;
+   end Token_Enum_Image;
+
+   function To_Syntax (Item : in Enum_Syntax) return 
WisiToken.Lexer.Regexp.Syntax
+   is
+      Result : WisiToken.Lexer.Regexp.Syntax (Token_ID'First .. 
+Last_Terminal);
+   begin
+      for I in Result'Range loop
+         Result (I) := Item (-I);
+      end loop;
+      return Result;
+   end To_Syntax;
+
+   function "&" (Left, Right : in Token_Enum_ID) return Token_ID_Arrays.Vector
+   is begin
+      return Result : Token_ID_Arrays.Vector do
+         Result.Append (+Left);
+         Result.Append (+Right);
+      end return;
+   end "&";
+
+   function "&"
+     (Left  : in Token_ID_Arrays.Vector;
+      Right : in Token_Enum_ID)
+     return Token_ID_Arrays.Vector
+   is begin
+      return Result : Token_ID_Arrays.Vector := Left do
+         Result.Append (+Right);
+      end return;
+   end "&";
+
+   function "+"
+     (Left  : in Token_Enum_ID;
+      Right : in WisiToken.Syntax_Trees.Semantic_Action)
+     return WisiToken.Productions.Right_Hand_Side
+   is begin
+      return WisiToken.Wisi_Ada."+" (+Left, Right);
+   end "+";
+
+   function "<="
+     (Left  : in Token_Enum_ID;
+      Right : in WisiToken.Productions.Right_Hand_Side)
+     return WisiToken.Productions.Instance
+   is begin
+      return WisiToken.Wisi_Ada."<=" (+Left, Productions.RHS_Arrays.To_Vector 
(Right, 1));
+   end "<=";
+
+   function To_Nonterminal_Array_Token_Set
+     (Item : in Nonterminal_Array_Token_Set)
+     return WisiToken.Token_Array_Token_Set
+   is
+      Result : Token_Array_Token_Set :=
+        (LR1_Descriptor.First_Nonterminal .. LR1_Descriptor.Last_Nonterminal =>
+           (LR1_Descriptor.First_Terminal .. LR1_Descriptor.Last_Nonterminal 
=> False));
+   begin
+      for I in Item'Range (1) loop
+         for J in Item'Range (2) loop
+            Result (+I, +J) := Item (I, J);
+         end loop;
+      end loop;
+      return Result;
+   end To_Nonterminal_Array_Token_Set;
+
+   function To_Nonterminal_Array_Terminal_Set
+     (Item : in Nonterminal_Array_Terminal_Set)
+     return WisiToken.Token_Array_Token_Set
+   is
+      Result : Token_Array_Token_Set :=
+        (LR1_Descriptor.First_Nonterminal .. LR1_Descriptor.Last_Nonterminal =>
+           (LR1_Descriptor.First_Terminal .. LR1_Descriptor.Last_Terminal => 
False));
+   begin
+      for I in Item'Range (1) loop
+         for J in Item'Range (2) loop
+            Result (+I, +J) := Item (I, J);
+         end loop;
+      end loop;
+      return Result;
+   end To_Nonterminal_Array_Terminal_Set;
+
+   function "+" (Item : in Token_Array) return WisiToken.Token_ID_Set
+   is
+      Result : Token_ID_Set := (LR1_Descriptor.First_Terminal .. 
LR1_Descriptor.Last_Terminal => False);
+   begin
+      for I in Item'Range loop
+         Result (+Item (I)) := True;
+      end loop;
+      return Result;
+   end "+";
+
+   function "+" (Item : in Token_Enum_ID) return WisiToken.Token_ID_Set
+   is begin
+      return +Token_Array'(1 => Item);
+   end "+";
+
+begin
+   LR1_Descriptor.Image := Token_Enum_Image;
+   LALR_Descriptor.Image := LR1_Descriptor.Image;
+end WisiToken.Gen_Token_Enum;
diff --git a/wisitoken-gen_token_enum.ads b/wisitoken-gen_token_enum.ads
new file mode 100644
index 0000000..b14be0d
--- /dev/null
+++ b/wisitoken-gen_token_enum.ads
@@ -0,0 +1,130 @@
+--  Abstract :
+--
+--  Support for an enumerated token type
+--
+--  Copyright (C) 2017, 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 (GPL);
+
+with WisiToken.Lexer.Regexp;
+with WisiToken.Productions;
+with WisiToken.Syntax_Trees;
+generic
+   type Token_Enum_ID is (<>);
+   First_Terminal    : Token_Enum_ID;
+   Last_Terminal     : Token_Enum_ID;
+   First_Nonterminal : Token_Enum_ID;
+   Last_Nonterminal  : Token_Enum_ID;
+   EOF_ID            : Token_Enum_ID;
+   Accept_ID         : Token_Enum_ID;
+   Case_Insensitive  : Boolean;
+package WisiToken.Gen_Token_Enum is
+
+   function "+" (Item : in Token_Enum_ID) return Token_ID
+     is (Token_ID'First + Token_Enum_ID'Pos (Item));
+
+   function "-" (Item : in Token_ID) return Token_Enum_ID
+     is (Token_Enum_ID'Val (Item - Token_ID'First));
+
+   function Token_Enum_Image return Token_ID_Array_String;
+
+   subtype Terminal_Enum_ID is Token_Enum_ID range First_Terminal .. 
Last_Terminal;
+   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,
+      EOF_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);
+
+   LALR_Descriptor : aliased WisiToken.Descriptor :=
+     (First_Terminal                => +First_Terminal,
+      Last_Terminal                 => +Last_Terminal,
+      First_Nonterminal             => +First_Nonterminal,
+      Last_Nonterminal              => +Last_Nonterminal,
+      EOF_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);
+
+   type Enum_Syntax is array (Token_Enum_ID range Token_Enum_ID'First .. 
Last_Terminal) of
+     WisiToken.Lexer.Regexp.Syntax_Item;
+
+   function To_Syntax (Item : in Enum_Syntax) return 
WisiToken.Lexer.Regexp.Syntax;
+
+   function "&" (Left, Right : in Token_Enum_ID) return Token_ID_Arrays.Vector;
+
+   function "&"
+     (Left  : in Token_ID_Arrays.Vector;
+      Right : in Token_Enum_ID)
+     return Token_ID_Arrays.Vector;
+
+   function "+" (Left : in Token_Enum_ID; Right : in 
Syntax_Trees.Semantic_Action) return Productions.Right_Hand_Side;
+
+   function "<="
+     (Left  : in Token_Enum_ID;
+      Right : in WisiToken.Productions.Right_Hand_Side)
+     return WisiToken.Productions.Instance;
+
+   ----------
+   --  For unit tests
+
+   subtype Terminal_ID is Token_Enum_ID range First_Terminal .. Last_Terminal;
+   subtype Nonterminal_ID is Token_Enum_ID range First_Nonterminal .. 
Last_Nonterminal;
+   subtype Grammar_ID is Token_Enum_ID range First_Terminal .. 
Last_Nonterminal;
+
+   type Nonterminal_Array_Token_Set is array (Nonterminal_ID, Grammar_ID) of 
Boolean;
+
+   function To_Nonterminal_Array_Token_Set
+     (Item : in Nonterminal_Array_Token_Set)
+     return WisiToken.Token_Array_Token_Set;
+
+   type Nonterminal_Array_Terminal_Set is array (Nonterminal_ID, Terminal_ID) 
of Boolean;
+
+   function To_Nonterminal_Array_Terminal_Set
+     (Item : in Nonterminal_Array_Terminal_Set)
+     return WisiToken.Token_Array_Token_Set;
+
+   type Nonterminal_ID_Set is array (Nonterminal_ID) of Boolean;
+
+   type Token_Array is array (Positive range <>) of Token_Enum_ID;
+
+   function "+" (Item : in Token_Array) return WisiToken.Token_ID_Set;
+   function "+" (Item : in Token_Enum_ID) return WisiToken.Token_ID_Set;
+
+end WisiToken.Gen_Token_Enum;
diff --git a/wisitoken-generate-lr-lalr_generate.adb 
b/wisitoken-generate-lr-lalr_generate.adb
new file mode 100644
index 0000000..d886077
--- /dev/null
+++ b/wisitoken-generate-lr-lalr_generate.adb
@@ -0,0 +1,593 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2002 - 2005, 2008 - 2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  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.EOF_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_First : constant Token_Array_Token_ID :=
+        WisiToken.Generate.LR.Minimal_Terminal_First (Grammar, Descriptor);
+
+      Ancestors : constant Token_Array_Token_Set := 
WisiToken.Generate.Ancestors (Grammar, Descriptor);
+
+      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_Terminal_First for 
McKenzie_Recover
+      for State in Table.States'Range loop
+         Table.States (State).Productions := LR1_Items.Productions (Kernels 
(State));
+         WisiToken.Generate.LR.Set_Minimal_Complete_Actions
+           (Table.States (State), Kernels (State), Minimal_Terminal_First, 
Ancestors, Descriptor, Grammar);
+      end loop;
+
+      if Put_Parse_Table then
+         WisiToken.Generate.LR.Put_Parse_Table
+           (Table, "LALR", Grammar, Kernels, Ancestors, 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;
diff --git a/wisitoken-generate-lr-lalr_generate.ads 
b/wisitoken-generate-lr-lalr_generate.ads
new file mode 100644
index 0000000..01c65ca
--- /dev/null
+++ b/wisitoken-generate-lr-lalr_generate.ads
@@ -0,0 +1,67 @@
+--  Abstract :
+--
+--  Generalized LALR parse table generator.
+--
+--  Copyright (C) 2002 - 2003, 2009 - 2010, 2013 - 2015, 2017, 2018 Stephe 
Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  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 WisiToken.Generate.LR1_Items;
+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)
+     return Parse_Table_Ptr
+   with Pre =>
+     Descriptor.Last_Lookahead = Descriptor.First_Nonterminal and
+     Descriptor.First_Nonterminal = Descriptor.Accept_ID;
+   --  Generate a generalized LALR parse table for Grammar. The
+   --  grammar start symbol is the LHS of the first production in
+   --  Grammar.
+   --
+   --  Unless Ignore_Unused_Tokens is True, raise Grammar_Error if
+   --  there are unused tokens.
+   --
+   --  Unless Ignore_Unknown_Conflicts is True, raise Grammar_Error if there
+   --  are unknown conflicts.
+
+   ----------
+   --  Visible for unit tests
+
+   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;
+   --  Return the Item_Set that is the goto for Symbol from Kernel.
+   --  If there is no such Item_Set, Result.Set is null.
+
+   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;
+
+end WisiToken.Generate.LR.LALR_Generate;
diff --git a/wisitoken-generate-lr-lr1_generate.adb 
b/wisitoken-generate-lr-lr1_generate.adb
new file mode 100644
index 0000000..14fa5db
--- /dev/null
+++ b/wisitoken-generate-lr-lr1_generate.adb
@@ -0,0 +1,315 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 2018 Stephe Leake
+--
+--  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 EOF in the
+              --  accept production. EOF should only appear in the
+              --  accept production.
+              Symbol /= Descriptor.EOF_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.EOF_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_First : constant Token_Array_Token_ID :=
+        WisiToken.Generate.LR.Minimal_Terminal_First (Grammar, Descriptor);
+
+      Ancestors : constant Token_Array_Token_Set := 
WisiToken.Generate.Ancestors (Grammar, Descriptor);
+
+      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_Terminal_First 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));
+         WisiToken.Generate.LR.Set_Minimal_Complete_Actions
+           (Table.States (State),
+            LR1_Items.Filter (Item_Sets (State), Grammar, Descriptor, 
LR1_Items.In_Kernel'Access),
+            Minimal_Terminal_First, Ancestors, Descriptor, Grammar);
+      end loop;
+
+      if Put_Parse_Table then
+         WisiToken.Generate.LR.Put_Parse_Table
+           (Table, "LR1", Grammar, Item_Sets, Ancestors, 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;
diff --git a/wisitoken-generate-lr-lr1_generate.ads 
b/wisitoken-generate-lr-lr1_generate.ads
new file mode 100644
index 0000000..7dea371
--- /dev/null
+++ b/wisitoken-generate-lr-lr1_generate.ads
@@ -0,0 +1,76 @@
+--  Abstract :
+--
+--  LR1 (Left-to-right scanning 1 look-ahead) parser table generator.
+--
+--  References:
+--
+--  [dragon] "Compilers Principles, Techniques, and Tools" by Aho,
+--  Sethi, and Ullman (aka: "The [Red] Dragon Book").
+--
+--  Copyright (C) 2017, 2018 Stephe Leake
+--
+--  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 WisiToken.Generate.LR1_Items;
+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)
+     return Parse_Table_Ptr
+   with Pre => Descriptor.First_Nonterminal = Descriptor.Accept_ID;
+   --  Generate a generalized LR1 parse table for Grammar. The
+   --  grammar start symbol is the LHS of the first production in
+   --  Grammar.
+   --
+   --  If Trace, output debug info to Standard_Error about generation
+   --  process. We don't use WisiToken.Trace here; we often want to
+   --  see a trace of the parser execution without the parser
+   --  generation.
+   --
+   --  Unless Ignore_Unused_Tokens is True, raise Grammar_Error if
+   --  there are unused tokens.
+   --
+   --  Unless Ignore_Unknown_Conflicts is True, raise Grammar_Error if there
+   --  are unknown conflicts.
+
+   ----------
+   --  visible for unit test
+
+   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;
+   --  'goto' from [dragon] algorithm 4.9
+
+   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;
+   --  [dragon] algorithm 4.9 pg 231; figure 4.38 pg 232; procedure "items"
+
+end WisiToken.Generate.LR.LR1_Generate;
diff --git a/wisitoken-generate-lr.adb b/wisitoken-generate-lr.adb
new file mode 100644
index 0000000..32689c0
--- /dev/null
+++ b/wisitoken-generate-lr.adb
@@ -0,0 +1,1141 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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 (GPL);
+
+with Ada.Strings.Fixed;
+with Ada.Text_IO;
+with System.Multiprocessors;
+with WisiToken.Generate;
+package body WisiToken.Generate.LR is
+
+   ----------
+   --  Body subprograms, alphabetical
+
+   function Count_Reduce (List : in Parse.LR.Minimal_Action_Lists.List) return 
Integer
+   is
+      Count : Integer := 0;
+   begin
+      for Item of List loop
+         if Item.Verb = Reduce then
+            Count := Count + 1;
+         end if;
+      end loop;
+      return Count;
+   end Count_Reduce;
+
+   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;
+
+   procedure Terminal_Sequence
+     (Grammar       : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor    : in     WisiToken.Descriptor;
+      All_Sequences : in out Token_Sequence_Arrays.Vector;
+      All_Set       : in out Token_ID_Set;
+      Recursing     : in out Token_ID_Set;
+      Nonterm       : in     Token_ID)
+   is
+      use Ada.Containers;
+      Prod : Productions.Instance renames Grammar (Nonterm);
+
+      Temp              : Token_Sequence_Arrays.Vector;
+      Min_Length        : Count_Type := Count_Type'Last;
+      Skipped_Recursive : Boolean    := False;
+   begin
+      --  We get here because All_Sequences (Nonterm) has not been comptued
+      --  yet. Attempt to compute All_Sequences (Nonterm); if successful, set
+      --  All_Set (Nonterm) True.
+
+      --  First fill Temp with terminals from each production for Nonterm.
+      for L in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
+
+         if Prod.RHSs (L).Tokens.Length = 0 then
+            All_Set (Nonterm) := True;
+
+            if Trace_Generate > Detail then
+               Ada.Text_IO.Put_Line (Image (Nonterm, Descriptor) & " => ()");
+            end if;
+
+            return;
+         end if;
+
+         if Prod.RHSs (L).Tokens (1) = Nonterm then
+            --  The first RHS token = LHS; a recursive list. This will never be
+            --  the shortest production, so just skip it.
+            null;
+
+         else
+            declare
+               Sequence : Token_ID_Arrays.Vector;
+            begin
+               for ID of Prod.RHSs (L).Tokens loop
+                  if ID in Descriptor.First_Terminal .. 
Descriptor.Last_Terminal then
+                     Sequence.Append (ID);
+
+                  else
+                     if not All_Set (ID) then
+                        if Recursing (ID) then
+                           --  This nonterm is mutually recursive with some 
other. This
+                           --  production will never be the shortest unless 
it's the only one,
+                           --  so skip it.
+                           if Trace_Generate > Detail then
+                              Ada.Text_IO.Put_Line (Image (ID, Descriptor) & " 
mutual recurse skipped");
+                           end if;
+                           Skipped_Recursive := True;
+                           goto Skip;
+                        else
+                           Recursing (ID) := True;
+                           if Trace_Generate > Detail then
+                              Ada.Text_IO.Put_Line (Image (ID, Descriptor) & " 
recurse");
+                           end if;
+                           Terminal_Sequence (Grammar, Descriptor, 
All_Sequences, All_Set, Recursing, ID);
+                           Recursing (ID) := False;
+
+                           if not All_Set (ID) then
+                              --  abandoned because of recursion
+                              Skipped_Recursive := True;
+                              goto Skip;
+                           end if;
+                        end if;
+                     end if;
+                     Sequence.Append (All_Sequences (ID));
+                  end if;
+               end loop;
+
+               if Trace_Generate > Detail then
+                  Ada.Text_IO.Put_Line (Image (Nonterm, Descriptor) & " -> " & 
Image (Sequence, Descriptor));
+               end if;
+               Temp.Append (Sequence);
+            end;
+         end if;
+
+         <<Skip>>
+         null;
+      end loop;
+
+      --  Now find the minimum length.
+      if Temp.Length = 0 and Skipped_Recursive then
+         --  better luck next time.
+         return;
+      end if;
+
+      for S of Temp loop
+         if S.Length <= Min_Length then
+            Min_Length := S.Length;
+
+            All_Sequences (Nonterm) := S;
+         end if;
+      end loop;
+
+      if Trace_Generate > Detail then
+         Ada.Text_IO.Put_Line (Image (Nonterm, Descriptor) & " ==> " & Image 
(All_Sequences (Nonterm), Descriptor));
+      end if;
+
+      All_Set (Nonterm) := True;
+   end Terminal_Sequence;
+
+   ----------
+   --  Public subprograms, declaration order
+
+   procedure Put
+     (Item       : in Conflict_Lists.List;
+      File       : in Ada.Text_IO.File_Type;
+      Descriptor : in WisiToken.Descriptor)
+   is begin
+      for Conflict of Item loop
+         Ada.Text_IO.Put_Line (File, Image (Conflict, Descriptor));
+      end loop;
+   end Put;
+
+   procedure Add_Action
+     (Symbol               : in     Token_ID;
+      Action               : in     Parse_Action_Rec;
+      Action_List          : in out Action_Node_Ptr;
+      Closure              : in     LR1_Items.Item_Set;
+      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in     Token_ID_Set;
+      First_Nonterm_Set    : in     Token_Array_Token_Set;
+      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
+         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
+               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
+            declare
+               --  Enforce canonical Shift/Reduce or Accept/Reduce
+               --  order, to simplify searching and code generation.
+               Action_A : constant Parse_Action_Rec :=
+                 (if Action.Verb in Shift | Accept_It then Action else 
Matching_Action.Action.Item);
+
+               Action_B : constant Parse_Action_Rec :=
+                 (if Action.Verb in Shift | Accept_It then 
Matching_Action.Action.Item else Action);
+
+               New_Conflict : constant Conflict :=
+                 (Action_A    => Action_A.Verb,
+                  Action_B    => Action_B.Verb,
+                  LHS_A       => Find
+                    (Closure, Action_A, Symbol, Grammar, Has_Empty_Production, 
First_Nonterm_Set, Descriptor),
+                  LHS_B       => Find
+                    (Closure, Action_B, Symbol, Grammar, Has_Empty_Production, 
First_Nonterm_Set, Descriptor),
+                  State_Index => Closure.State,
+                  On          => Symbol);
+            begin
+               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
+                     Ada.Text_IO.Put_Line (" - conflict added: " & Image 
(New_Conflict, Descriptor));
+                  end if;
+               else
+                  if Trace_Generate > Outline 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
+                  Matching_Action.Action.Next := new 
Parse_Action_Node'(Action, Matching_Action.Action.Next);
+               end if;
+            end;
+         end if;
+      else
+         WisiToken.Parse.LR.Add (Action_List, Symbol, Action);
+      end if;
+   end Add_Action;
+
+   procedure Add_Actions
+     (Closure              : in     LR1_Items.Item_Set;
+      Table                : in out Parse_Table;
+      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in     Token_ID_Set;
+      First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflicts            : in out Conflict_Lists.List;
+      Descriptor           : in     WisiToken.Descriptor)
+   is
+      use WisiToken.Token_ID_Arrays;
+
+      State : constant State_Index := Closure.State;
+   begin
+      if Trace_Generate > Outline then
+         Ada.Text_IO.Put_Line ("adding actions for state" & State_Index'Image 
(State));
+      end if;
+
+      for Item of Closure.Set loop
+         if Item.Dot = No_Element then
+            --  Pointer is at the end of the production; add a reduce action.
+
+            Add_Lookahead_Actions
+              (Item, Table.States (State).Action_List, Grammar, 
Has_Empty_Production, First_Nonterm_Set,
+               Conflicts, Closure, Descriptor);
+
+         elsif Element (Item.Dot) in Descriptor.First_Terminal .. 
Descriptor.Last_Terminal then
+            --  Dot is before a terminal token.
+            declare
+               use all type Ada.Containers.Count_Type;
+
+               Dot_ID : constant Token_ID := Element (Item.Dot);
+               --  ID of token after Item.Dot
+
+               Goto_State : constant Unknown_State_Index := 
LR1_Items.Goto_State (Closure, Dot_ID);
+            begin
+               if Dot_ID = Descriptor.EOF_ID then
+                  --  This is the start symbol production with dot before EOF.
+                  declare
+                     P_ID : constant Production_ID := Item.Prod;
+                     RHS  : Productions.Right_Hand_Side renames Grammar 
(P_ID.LHS).RHSs (P_ID.RHS);
+                  begin
+                     Add_Action
+                       (Dot_ID,
+                        (Accept_It, P_ID, RHS.Action, RHS.Check, 
RHS.Tokens.Length - 1),
+                        --  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);
+                  end;
+               else
+                  if Goto_State /= Unknown_State then
+                     Add_Action
+                       (Dot_ID,
+                        (Shift, Goto_State),
+                        Table.States (State).Action_List,
+                        Closure, Grammar, Has_Empty_Production, 
First_Nonterm_Set, Conflicts, Descriptor);
+                  end if;
+               end if;
+            end;
+         else
+            --  Dot is before a non-terminal token; no action.
+            if Trace_Generate > Outline then
+               Ada.Text_IO.Put_Line (Image (Element (Item.Dot), Descriptor) & 
" => no action");
+            end if;
+         end if;
+      end loop;
+
+      --  Place a default error action at the end of every state.
+      --  (it should always have at least one action already).
+      declare
+         --  The default action, when nothing else matches an input
+         Default_Action : constant Action_Node :=
+           --  The symbol here is actually irrelevant; it is the
+           --  position as the last on a state's action list that makes
+           --  it the default.
+           (Symbol => Invalid_Token_ID,
+            Action => new Parse_Action_Node'(Parse_Action_Rec'(Verb => 
WisiToken.Parse.LR.Error), null),
+            Next   => null);
+
+         Last_Action : Action_Node_Ptr := Table.States (State).Action_List;
+      begin
+         if Last_Action = null then
+            --  This happens if the first production in the grammar is
+            --  not the start symbol production.
+            --
+            --  It also happens when the start symbol production does
+            --  not have an explicit EOF, or when there is more than
+            --  one production that has the start symbol on the left
+            --  hand side.
+            --
+            --  It also happens when the grammar is bad, for example:
+            --
+            --  declarations <= declarations & declaration
+            --
+            --  without 'declarations <= declaration'.
+            --
+            --  We continue generating the grammar, in order to help the user
+            --  debug this issue.
+            WisiToken.Generate.Error := True;
+
+            Ada.Text_IO.Put_Line
+              (Ada.Text_IO.Current_Error, "Error: state" & State_Index'Image 
(State) &
+                 " has no actions; bad grammar, or " &
+                 "first production in grammar must be the only start symbol 
production, " &
+                 "and it must must have an explicit EOF.");
+         else
+            while Last_Action.Next /= null loop
+               Last_Action := Last_Action.Next;
+            end loop;
+            Last_Action.Next := new Action_Node'(Default_Action);
+         end if;
+      end;
+
+      for Item of Closure.Goto_List loop
+         if Item.Symbol in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal then
+            Add_Goto (Table.States (State), Item.Symbol, Item.State); -- note 
list is already sorted.
+         end if;
+      end loop;
+   end Add_Actions;
+
+   procedure Add_Lookahead_Actions
+     (Item                 : in     LR1_Items.Item;
+      Action_List          : in out Action_Node_Ptr;
+      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in     Token_ID_Set;
+      First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflicts            : in out Conflict_Lists.List;
+      Closure              : in     LR1_Items.Item_Set;
+      Descriptor           : in     WisiToken.Descriptor)
+   is
+      Prod   : Productions.Instance renames Grammar (Item.Prod.LHS);
+      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
+         Ada.Text_IO.Put_Line ("processing lookaheads");
+      end if;
+
+      --  We ignore propagate lookaheads here.
+      for Lookahead in Item.Lookaheads'Range loop
+         if Item.Lookaheads (Lookahead) then
+            if Lookahead = Descriptor.First_Nonterminal then
+               null;
+            else
+               Add_Action
+                 (Lookahead, Action, Action_List, Closure, Grammar,
+                  Has_Empty_Production, First_Nonterm_Set, Conflicts, 
Descriptor);
+            end if;
+         end if;
+      end loop;
+   end Add_Lookahead_Actions;
+
+   procedure Delete_Known
+     (Conflicts       : in out Conflict_Lists.List;
+      Known_Conflicts : in out Conflict_Lists.List)
+   is
+      --  Delete all elements in Conflicts that match an element in
+      --  Known_Conflicts. There can be more than one Conflict that
+      --  match one Known_Conflict.
+      use Conflict_Lists;
+      Known      : Cursor  := Known_Conflicts.First;
+      Next_Known : Cursor;
+   begin
+      loop
+         exit when Known = No_Element;
+         Next_Known := Next (Known);
+         declare
+            I      : Cursor  := Conflicts.First;
+            Next_I : Cursor;
+            Used   : Boolean := False;
+         begin
+            loop
+               exit when I = No_Element;
+               Next_I := Next (I);
+               if Match (Element (Known), Conflicts.Constant_Reference (I)) 
then
+                  Delete (Conflicts, I);
+                  Used := True;
+               end if;
+               I := Next_I;
+            end loop;
+
+            if Used then
+               Delete (Known_Conflicts, Known);
+            end if;
+         end;
+         Known := Next_Known;
+      end loop;
+   end Delete_Known;
+
+   function Find
+     (Closure              : in LR1_Items.Item_Set;
+      Action               : in Parse_Action_Rec;
+      Lookahead            : in Token_ID;
+      Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in Token_ID_Set;
+      First                : in Token_Array_Token_Set;
+      Descriptor           : in WisiToken.Descriptor)
+     return Token_ID
+   is
+      use WisiToken.Token_ID_Arrays;
+
+      ID_I : Cursor;
+   begin
+      case Action.Verb is
+      when Reduce | Accept_It =>
+         --  If the nonterm produced by the reduce is the LHS of the state
+         --  production, use it.
+         for Item of Closure.Set loop
+            if LR1_Items.In_Kernel (Grammar, Descriptor, Item) and
+              Action.Production.LHS = Item.Prod.LHS
+            then
+               return Item.Prod.LHS;
+            end if;
+         end loop;
+
+         --  The reduce nonterm is after Dot in a state production; find which
+         --  one, use that.
+         for Item of Closure.Set loop
+            if LR1_Items.In_Kernel (Grammar, Descriptor, Item) then
+               ID_I := Item.Dot;
+               loop
+                  if ID_I = No_Element then
+                     if Item.Lookaheads (Lookahead) then
+                        return Item.Prod.LHS;
+                     end if;
+                  else
+                     declare
+                        Dot_ID : Token_ID renames Element (ID_I);
+                     begin
+                        if Dot_ID = Lookahead or
+                          (Dot_ID in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal and then
+                             First (Dot_ID, Lookahead))
+                        then
+                           return Item.Prod.LHS;
+                        end if;
+                        exit when Dot_ID in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal and then
+                          not Has_Empty_Production (Dot_ID);
+                     end;
+                  end if;
+
+                  exit when ID_I = No_Element;
+                  Next (ID_I);
+               end loop;
+            end if;
+         end loop;
+
+      when Shift =>
+
+         for Item of Closure.Set loop
+            --  Lookahead (the token shifted) is starting a nonterm in a state
+            --  production; it is in First of that nonterm.
+            if LR1_Items.In_Kernel (Grammar, Descriptor, Item) then
+               ID_I := Item.Dot;
+               loop
+                  exit when ID_I = No_Element;
+                  declare
+                     Dot_ID : Token_ID renames Element (ID_I);
+                  begin
+                     if Dot_ID = Lookahead or
+                       (Dot_ID in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal and then
+                          First (Dot_ID, Lookahead))
+                     then
+                        return Item.Prod.LHS;
+                     end if;
+
+                     exit when Dot_ID in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal and then
+                       not Has_Empty_Production (Dot_ID);
+                  end;
+
+                  Next (ID_I);
+               end loop;
+            end if;
+         end loop;
+
+      when WisiToken.Parse.LR.Error =>
+         raise SAL.Programmer_Error;
+      end case;
+
+      Ada.Text_IO.Put_Line
+        ("item for " & Image (Action, Descriptor) & " on " & Image (Lookahead, 
Descriptor) & " not found in");
+      LR1_Items.Put (Grammar, Descriptor, Closure, Kernel_Only => True);
+      raise SAL.Programmer_Error;
+   end Find;
+
+   function Image (Item : in Conflict; Descriptor : in WisiToken.Descriptor) 
return String
+   is begin
+      return
+        ("%conflict " &
+           Conflict_Parse_Actions'Image (Item.Action_A) & "/" &
+           Conflict_Parse_Actions'Image (Item.Action_B) & " in state " &
+           Image (Item.LHS_A, Descriptor) & ", " &
+           Image (Item.LHS_B, Descriptor) &
+           " on token " & Image (Item.On, Descriptor) &
+           " (" & State_Index'Image (Item.State_Index) & ")"); -- state number 
last for easier delete
+   end Image;
+
+   function Is_Present (Item : in Conflict; Conflicts : in 
Conflict_Lists.List) return Boolean
+   is
+      use Conflict_Lists;
+      I : Cursor := Conflicts.First;
+   begin
+      loop
+         exit when I = No_Element;
+         if Match (Item, Conflicts.Constant_Reference (I)) then
+            return True;
+         end if;
+         I := Next (I);
+      end loop;
+      return False;
+   end Is_Present;
+
+   function Match (Known : in Conflict; Item : in 
Conflict_Lists.Constant_Reference_Type) return Boolean
+   is begin
+      --  Ignore State_Index. Actions are in canonical order; enforced
+      --  in Add_Action above. For reduce/reduce, LHS_A, LHS_B are not
+      --  in canonical order.
+      return
+        Known.Action_A = Item.Action_A and
+        Known.Action_B = Item.Action_B and
+        ((Known.LHS_A = Item.LHS_A and Known.LHS_B = Item.LHS_B) or
+           (Known.LHS_B = Item.LHS_A and Known.LHS_A = Item.LHS_B)) and
+        Known.On = Item.On;
+   end Match;
+
+   procedure Compute_Minimal_Terminal_Sequences
+     (Grammar    : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in     WisiToken.Descriptor;
+      Result     : in out Token_Sequence_Arrays.Vector)
+   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);
+
+      Last_Count : Integer := 0;
+      This_Count : Integer;
+   begin
+      Result.Set_First (Descriptor.First_Nonterminal);
+      Result.Set_Last (Descriptor.Last_Nonterminal);
+
+      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, 
Recursing, P.LHS);
+            end if;
+         end loop;
+         This_Count := Count (All_Set);
+         if This_Count = Last_Count then
+            raise Grammar_Error with "nonterminals have no minimum terminal 
sequence: " &
+              Image (All_Set, Descriptor, Inverted => True);
+         end if;
+         Last_Count := This_Count;
+      end loop;
+   end Compute_Minimal_Terminal_Sequences;
+
+   function Minimal_Terminal_First
+     (Grammar    : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in     WisiToken.Descriptor)
+     return Token_Array_Token_ID
+   is
+      use all type Ada.Containers.Count_Type;
+      Minimal_Terminal_Sequences : Token_Sequence_Arrays.Vector;
+   begin
+      Compute_Minimal_Terminal_Sequences (Grammar, Descriptor, 
Minimal_Terminal_Sequences);
+
+      return Result : Token_Array_Token_ID (Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal) do
+         for ID in Result'Range loop
+            if Minimal_Terminal_Sequences (ID).Length = 0 then
+               Result (ID) := Invalid_Token_ID;
+            else
+               Result (ID) := Minimal_Terminal_Sequences 
(ID)(Minimal_Terminal_Sequences (ID).First);
+            end if;
+         end loop;
+      end return;
+   end Minimal_Terminal_First;
+
+   procedure Set_Minimal_Complete_Actions
+     (State                  : in out Parse_State;
+      Kernel                 : in     LR1_Items.Item_Set;
+      Minimal_Terminal_First : in     Token_Array_Token_ID;
+      Ancestors              : in     Token_Array_Token_Set;
+      Descriptor             : in     WisiToken.Descriptor;
+      Grammar                : in     WisiToken.Productions.Prod_Arrays.Vector)
+   is
+      use all type Ada.Containers.Count_Type;
+      use LR1_Items.Item_Lists;
+      use Token_ID_Arrays;
+
+      subtype Terminals is Token_ID range Descriptor.First_Terminal .. 
Descriptor.Last_Terminal;
+
+      Del  : LR1_Items.Item_Lists.Cursor;
+
+      procedure Delete_Same_Ancestor (List : in out LR1_Items.Item_Lists.List; 
Cur : in LR1_Items.Item_Lists.Cursor)
+      is
+         Cur_LHS : constant Token_ID := Element (Cur).Prod.LHS;
+
+         J : LR1_Items.Item_Lists.Cursor := List.First;
+      begin
+         loop
+            exit when not Has_Element (J);
+            if J = Cur then
+               Next (J);
+            else
+               declare
+                  Item : LR1_Items.Item renames Constant_Ref (J);
+               begin
+                  if Cur_LHS = Item.Prod.LHS or else Ancestors (Cur_LHS, 
Item.Prod.LHS) then
+                     Del := J;
+                     Next (J);
+                     List.Delete (Del);
+                  else
+                     Next (J);
+                  end if;
+               end;
+            end if;
+         end loop;
+      end Delete_Same_Ancestor;
+
+      procedure Append_No_Dup (Item : in Minimal_Action)
+      is begin
+         if not State.Minimal_Complete_Actions.Contains (Item) then
+            State.Minimal_Complete_Actions.Insert (Item);
+         end if;
+      end Append_No_Dup;
+
+      function Find_Action (List : in Action_Node_Ptr; ID : in Token_ID) 
return Minimal_Action
+      is
+         Node : Action_Node_Ptr := List;
+      begin
+         loop
+            if Node.Symbol = ID then
+               case Node.Action.Item.Verb is
+               when Shift =>
+                  return (Shift, ID, Node.Action.Item.State);
+               when Reduce =>
+                  --  Item.Dot is a nonterm that starts with a nullable 
nonterm; reduce
+                  --  to that first.
+                  return (Reduce, Node.Action.Item.Production.LHS, 0);
+               when Accept_It | WisiToken.Parse.LR.Error =>
+                  raise SAL.Programmer_Error;
+               end case;
+            end if;
+            Node := Node.Next;
+            exit when Node = null;
+         end loop;
+         raise SAL.Programmer_Error;
+      end Find_Action;
+
+      Working_Set : LR1_Items.Item_Lists.List := Kernel.Set;
+      I           : LR1_Items.Item_Lists.Cursor;
+
+   begin
+      --  First find items to delete.
+      --
+      --  This algorithm will return an empty Minimal_Complete_Actions in
+      --  the top level accept state.
+
+      I := Working_Set.First;
+      loop
+         exit when not Has_Element (I);
+         declare
+            Item : LR1_Items.Item renames Constant_Ref (I);
+            Prod : WisiToken.Productions.Instance renames Grammar 
(Item.Prod.LHS);
+         begin
+            if not Has_Element (Item.Dot) then
+               --  Completing this item also completes items that share an 
ancestor.
+               Delete_Same_Ancestor (Working_Set, I);
+               Next (I);
+
+            elsif To_Index (Item.Dot) = 2 and then
+              Prod.RHSs (Item.Prod.RHS).Tokens (1) = Item.Prod.LHS
+            then
+               --  Item is left-recursive; it can't be minimal.
+               Del := I;
+               Next (I);
+               Working_Set.Delete (Del);
+            else
+               Next (I);
+            end if;
+         end;
+      end loop;
+
+      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.
+            Append_No_Dup
+              ((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.EOF_ID then
+
+                  if ID in Terminals then
+                     Append_No_Dup (Find_Action (State.Action_List, ID));
+
+                  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.
+                        Append_No_Dup ((Reduce, ID, Token_Count => 0));
+
+                     else
+                        Append_No_Dup (Find_Action (State.Action_List, 
Minimal_Terminal_First (ID)));
+                     end if;
+                  end if;
+               end if;
+            end;
+         end if;
+      end loop;
+   end Set_Minimal_Complete_Actions;
+
+   ----------
+   --  Parse table output
+
+   procedure Put_Text_Rep
+     (Table        : in Parse_Table;
+      File_Name    : in String;
+      Action_Names : in Names_Array_Array;
+      Check_Names  : in Names_Array_Array)
+   is
+      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.
+
+      Create (File, Out_File, File_Name);
+
+      --  First the discriminants
+      Put (File,
+           Trimmed_Image (Table.State_First) & State_Index'Image 
(Table.State_Last) &
+             Token_ID'Image (Table.First_Terminal) & Token_ID'Image 
(Table.Last_Terminal) &
+             Token_ID'Image (Table.First_Nonterminal) & Token_ID'Image 
(Table.Last_Nonterminal));
+      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
+            loop
+               exit when Node_I = null;
+               --  Action first, so we know if Symbol is present (not when 
Error)
+               declare
+                  Node_J     : Parse_Action_Node_Ptr := Node_I.Action;
+                  Put_Symbol : Boolean               := True;
+               begin
+                  loop
+                     Put (File, Parse_Action_Verbs'Image (Node_J.Item.Verb));
+
+                     case Node_J.Item.Verb is
+                     when Shift =>
+                        Put (File, State_Index'Image (Node_J.Item.State));
+
+                     when Reduce | Accept_It =>
+                        Put (File, Token_ID'Image (Node_J.Item.Production.LHS) 
&
+                               Integer'Image (Node_J.Item.Production.RHS));
+
+                        if Action_Names (Node_J.Item.Production.LHS) /= null 
and then
+                          Action_Names 
(Node_J.Item.Production.LHS)(Node_J.Item.Production.RHS) /= null
+                        then
+                           Put (File, " true");
+                        else
+                           Put (File, " false");
+                        end if;
+                        if Check_Names (Node_J.Item.Production.LHS) /= null 
and then
+                          Check_Names 
(Node_J.Item.Production.LHS)(Node_J.Item.Production.RHS) /= null
+                        then
+                           Put (File, " true");
+                        else
+                           Put (File, " false");
+                        end if;
+
+                        Put (File, Ada.Containers.Count_Type'Image 
(Node_J.Item.Token_Count));
+
+                     when Parse.LR.Error =>
+                        --  Error action terminates the action list
+                        Put_Symbol := False;
+                     end case;
+
+                     Node_J := Node_J.Next;
+                     exit when Node_J = null;
+                     Put (File, ' ');
+                  end loop;
+                  Put (File, ';');
+                  if Put_Symbol then
+                     Put (File, Token_ID'Image (Node_I.Symbol));
+                  end if;
+               end;
+               New_Line (File);
+
+               Node_I := Node_I.Next;
+            end loop;
+         end;
+
+         declare
+            Node_I : Goto_Node_Ptr := State.Goto_List;
+         begin
+            loop
+               exit when Node_I = null;
+               Put (File, Token_ID'Image (Symbol (Node_I)) & State_Index'Image 
(Parse.LR.State (Node_I)));
+               Node_I := Next (Node_I);
+            end loop;
+            Put (File, ';');
+            New_Line (File);
+         end;
+
+         for Action of State.Minimal_Complete_Actions loop
+            Put (File, ' ' & Minimal_Verbs'Image (Action.Verb));
+            case Action.Verb is
+            when Shift =>
+               Put (File, Token_ID'Image (Action.ID) & State_Index'Image 
(Action.State));
+            when Reduce =>
+               Put (File, Token_ID'Image (Action.Nonterm) & 
Ada.Containers.Count_Type'Image (Action.Token_Count));
+            end case;
+         end loop;
+         Put (File, ';');
+         New_Line (File);
+      end loop;
+      Close (File);
+   end Put_Text_Rep;
+
+   procedure Put (Item : in Parse_Action_Rec; Descriptor : in 
WisiToken.Descriptor)
+   is
+      use Ada.Containers;
+      use Ada.Text_IO;
+   begin
+      case Item.Verb is
+      when Shift =>
+         Put ("shift and goto state" & State_Index'Image (Item.State));
+
+      when Reduce =>
+         Put
+           ("reduce" & Count_Type'Image (Item.Token_Count) & " tokens to " &
+              Image (Item.Production.LHS, Descriptor));
+      when Accept_It =>
+         Put ("accept it");
+      when Parse.LR.Error =>
+         Put ("ERROR");
+      end case;
+   end Put;
+
+   procedure Put (Item : in McKenzie_Param_Type; Descriptor : in 
WisiToken.Descriptor)
+   is
+      use Ada.Text_IO;
+   begin
+      Put_Line ("(Insert =>");
+      for I in Item.Insert'Range loop
+         Put (" " & Padded_Image (I, Descriptor) & " =>" & Natural'Image 
(Item.Insert (I)));
+         if I = Item.Insert'Last then
+            Put_Line (")");
+         else
+            Put_Line (",");
+         end if;
+      end loop;
+      Put_Line ("(Delete =>");
+      for I in Item.Delete'Range loop
+         Put (" " & Padded_Image (I, Descriptor) & " =>" & Natural'Image 
(Item.Delete (I)));
+         if I = Item.Delete'Last then
+            Put_Line (")");
+         else
+            Put_Line (",");
+         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
+            Put_Line (")");
+         else
+            Put_Line (",");
+         end if;
+      end loop;
+      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));
+   end Put;
+
+   procedure Put (Descriptor : in WisiToken.Descriptor; Item : in 
Parse_Action_Rec)
+   is
+      use Ada.Containers;
+      use Ada.Text_IO;
+   begin
+      case Item.Verb is
+      when Shift =>
+         Put ("shift and goto state" & State_Index'Image (Item.State));
+      when Reduce =>
+         Put
+           ("reduce" & Count_Type'Image (Item.Token_Count) & " tokens to " &
+              Image (Item.Production.LHS, Descriptor));
+         Put (" " & Trimmed_Image (Item.Production));
+      when Accept_It =>
+         Put ("accept it");
+         Put (" " & Trimmed_Image (Item.Production));
+      when Parse.LR.Error =>
+         Put ("ERROR");
+      end case;
+   end Put;
+
+   procedure Put (Descriptor : in WisiToken.Descriptor; Action : in 
Parse_Action_Node_Ptr)
+   is
+      use Ada.Text_IO;
+      Ptr    : Parse_Action_Node_Ptr   := Action;
+      Column : constant Positive_Count := Col;
+   begin
+      loop
+         Put (Descriptor, Ptr.Item);
+         Ptr := Ptr.Next;
+         exit when Ptr = null;
+         Put_Line (",");
+         Set_Col (Column);
+      end loop;
+   end Put;
+
+   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;
+      Goto_Ptr   : Goto_Node_Ptr   := State.Goto_List;
+      Need_Comma : Boolean := False;
+   begin
+      while Action_Ptr /= null loop
+         Put ("   ");
+         if Action_Ptr.Next = null then
+            Put ("default" & (Descriptor.Image_Width - 7) * ' ' & " => ");
+
+         elsif Action_Ptr.Action.Item.Verb /= Parse.LR.Error then
+            Put (Image (Action_Ptr.Symbol, Descriptor) &
+                   (Descriptor.Image_Width - Image (Action_Ptr.Symbol, 
Descriptor)'Length) * ' '
+                   & " => ");
+         end if;
+         Put (Descriptor, Action_Ptr.Action);
+         New_Line;
+         Action_Ptr := Action_Ptr.Next;
+      end loop;
+
+      if Goto_Ptr /= null then
+         New_Line;
+      end if;
+
+      while Goto_Ptr /= null loop
+         Put_Line
+           ("   " & Image (Symbol (Goto_Ptr), Descriptor) &
+              (Descriptor.Image_Width - Image (Symbol (Goto_Ptr), 
Descriptor)'Length) * ' ' &
+              " goto state" & State_Index'Image (Parse.LR.State (Goto_Ptr)));
+         Goto_Ptr := Next (Goto_Ptr);
+      end loop;
+
+      if State.Minimal_Complete_Actions.Length > 0 then
+         New_Line;
+         Put ("   Minimal_Complete_Actions => (");
+         for Action of State.Minimal_Complete_Actions loop
+            if Need_Comma then
+               Put (", ");
+            else
+               Need_Comma := True;
+            end if;
+            case Action.Verb is
+            when Shift =>
+               Put (Image (Action.ID, Descriptor));
+            when Reduce =>
+               Put (Image (Action.Nonterm, Descriptor));
+            end case;
+         end loop;
+         Put_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;
+      Ancestors  : in Token_Array_Token_Set;
+      Conflicts  : in Conflict_Lists.List;
+      Descriptor : in WisiToken.Descriptor)
+   is
+      use all type Ada.Containers.Count_Type;
+      use Ada.Text_IO;
+      Minimal_Complete_Multiple_Reduce : State_Index_Arrays.Vector;
+   begin
+      Put_Line ("Tokens:");
+      WisiToken.Put_Tokens (Descriptor);
+
+      New_Line;
+      Put_Line ("Productions:");
+      WisiToken.Productions.Put (Grammar, Descriptor);
+
+      if Table.McKenzie_Param.Cost_Limit /= Default_McKenzie_Param.Cost_Limit 
or
+          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
+         New_Line;
+         Put_Line ("McKenzie:");
+         Put (Table.McKenzie_Param, Descriptor);
+      end if;
+
+      New_Line;
+      Put_Line ("Ancestors:");
+      for ID in Ancestors'Range (1) loop
+         if Any (Ancestors, ID) then
+            Put_Line (Image (ID, Descriptor) & " => " & Image (Slice 
(Ancestors, ID), Descriptor));
+         end if;
+      end loop;
+
+      New_Line;
+      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);
+         New_Line;
+         Put (Descriptor, Table.States (State_Index));
+
+         if Count_Reduce (Table.States (State_Index).Minimal_Complete_Actions) 
> 1 then
+            Minimal_Complete_Multiple_Reduce.Append (State_Index);
+         end if;
+
+         if State_Index /= Table.States'Last then
+            New_Line;
+         end if;
+      end loop;
+
+      if Minimal_Complete_Multiple_Reduce.Length + Conflicts.Length > 0 then
+         New_Line;
+      end if;
+
+      if Minimal_Complete_Multiple_Reduce.Length > 0 then
+         Indent_Wrap
+           ("States with multiple reduce in Minimal_Complete_Action: " & Image 
(Minimal_Complete_Multiple_Reduce));
+      end if;
+
+      if Conflicts.Length > 0 then
+         declare
+            use Ada.Strings.Unbounded;
+            Last_State : Unknown_State_Index := Unknown_State;
+            Line : Unbounded_String := +"States with conflicts:";
+         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;
+            end loop;
+            Indent_Wrap (-Line);
+         end;
+      end if;
+   end Put_Parse_Table;
+
+end WisiToken.Generate.LR;
diff --git a/wisitoken-generate-lr.ads b/wisitoken-generate-lr.ads
new file mode 100644
index 0000000..6000dab
--- /dev/null
+++ b/wisitoken-generate-lr.ads
@@ -0,0 +1,176 @@
+--  Abstract :
+--
+--  Common utilities for LR parser table generators.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Containers.Doubly_Linked_Lists;
+with WisiToken.Generate.LR1_Items;
+with WisiToken.Parse.LR;
+with WisiToken.Productions;
+package WisiToken.Generate.LR is
+   use WisiToken.Parse.LR;
+
+   subtype Conflict_Parse_Actions is Parse_Action_Verbs range Shift .. 
Accept_It;
+   type Conflict is record
+      --  A typical conflict is:
+      --
+      --  SHIFT/REDUCE in state: 11 on token IS
+      --
+      --  State numbers change with minor changes in the grammar, so we
+      --  attempt to identify the state by the LHS of the two productions
+      --  involved; this is _not_ guarranteed to be unique, but is good
+      --  enough for our purposes. We also store the state number for
+      --  generated conflicts (not for known conflicts from the grammar
+      --  definition file), for debugging.
+      Action_A    : Conflict_Parse_Actions;
+      LHS_A       : Token_ID;
+      Action_B    : Conflict_Parse_Actions;
+      LHS_B       : Token_ID;
+      State_Index : Unknown_State_Index;
+      On          : Token_ID;
+   end record;
+
+   package Conflict_Lists is new Ada.Containers.Doubly_Linked_Lists (Conflict);
+
+   procedure Put
+     (Item       : in Conflict_Lists.List;
+      File       : in Ada.Text_IO.File_Type;
+      Descriptor : in WisiToken.Descriptor);
+
+   procedure Add_Action
+     (Symbol               : in     Token_ID;
+      Action               : in     Parse_Action_Rec;
+      Action_List          : in out Action_Node_Ptr;
+      Closure              : in     LR1_Items.Item_Set;
+      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in     Token_ID_Set;
+      First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflicts            : in out Conflict_Lists.List;
+      Descriptor           : in     WisiToken.Descriptor);
+   --  Add (Symbol, Action) to Action_List; check for conflicts
+   --
+   --  Closure .. Conflicts are for conflict reporting
+
+   procedure Add_Actions
+     (Closure              : in     LR1_Items.Item_Set;
+      Table                : in out Parse_Table;
+      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in     Token_ID_Set;
+      First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflicts            : in out Conflict_Lists.List;
+      Descriptor           : in     WisiToken.Descriptor);
+   --  Add actions for Closure to Table. Has_Empty_Production, First,
+   --  Conflicts used for conflict reporting.
+
+   procedure Add_Lookahead_Actions
+     (Item                 : in     LR1_Items.Item;
+      Action_List          : in out Action_Node_Ptr;
+      Grammar              : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in     Token_ID_Set;
+      First_Nonterm_Set    : in     Token_Array_Token_Set;
+      Conflicts            : in out Conflict_Lists.List;
+      Closure              : in     LR1_Items.Item_Set;
+      Descriptor           : in     WisiToken.Descriptor);
+   --  Add actions for Item.Lookaheads to Action_List
+   --  Closure must be from the item set containing Item.
+   --  Has_Empty_Production .. Closure used for conflict reporting.
+
+   procedure Delete_Known
+     (Conflicts       : in out Conflict_Lists.List;
+      Known_Conflicts : in out Conflict_Lists.List);
+   --  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;
+      Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
+      Has_Empty_Production : in Token_ID_Set;
+      First                : in Token_Array_Token_Set;
+      Descriptor           : in WisiToken.Descriptor)
+     return Token_ID;
+   --  Return the LHS of a production in kernel of Closure, for an Action
+   --  conflict on Lookahead; for naming a Conflict object.
+
+   function Image (Item : in Conflict; Descriptor : in WisiToken.Descriptor) 
return String;
+
+   function Is_Present (Item : in Conflict; Conflicts : in 
Conflict_Lists.List) return Boolean;
+
+   function Match (Known : in Conflict; Item : in 
Conflict_Lists.Constant_Reference_Type) return Boolean;
+
+   procedure Compute_Minimal_Terminal_Sequences
+     (Grammar    : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in     WisiToken.Descriptor;
+      Result     : in out Token_Sequence_Arrays.Vector);
+   --  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.
+
+   function Minimal_Terminal_First
+     (Grammar    : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in     WisiToken.Descriptor)
+      return Token_Array_Token_ID;
+   --  For each nonterminal in Grammar, return the first of the minimal
+   --  sequence of terminals that will complete it; Invalid_Token_ID if
+   --  the minimal sequence is empty.
+
+   procedure Set_Minimal_Complete_Actions
+     (State                  : in out Parse_State;
+      Kernel                 : in     LR1_Items.Item_Set;
+      Minimal_Terminal_First : in     Token_Array_Token_ID;
+      Ancestors              : in     Token_Array_Token_Set;
+      Descriptor             : in     WisiToken.Descriptor;
+      Grammar                : in     
WisiToken.Productions.Prod_Arrays.Vector);
+   --  Set State.Minimal_Terminal_First to the set of terminals that will
+   --  most quickly complete the productions in Kernel (which must be for
+   --  State). Useful in error correction when we know the next actual
+   --  terminal is a block ending or statement start.
+
+   ----------
+   --  Parse table output
+
+   procedure Put_Text_Rep
+     (Table        : in Parse_Table;
+      File_Name    : in String;
+      Action_Names : in Names_Array_Array;
+      Check_Names  : in Names_Array_Array);
+   --  Write machine-readable text format of Table.States to a file
+   --  File_Name, to be read by the parser executable at startup, using
+   --  WisiToken.Parse.LR.Get_Text_Rep.
+
+   procedure Put (Item : in Parse_Action_Rec; Descriptor : in 
WisiToken.Descriptor);
+   procedure Put (Item : in McKenzie_Param_Type; Descriptor : in 
WisiToken.Descriptor);
+   procedure Put (Descriptor : in WisiToken.Descriptor; Item : in 
Parse_Action_Rec);
+   procedure Put (Descriptor : in WisiToken.Descriptor; Action : in 
Parse_Action_Node_Ptr);
+   procedure Put (Descriptor : in WisiToken.Descriptor; State : in 
Parse_State);
+   --  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;
+      Ancestors  : in Token_Array_Token_Set;
+      Conflicts  : in Conflict_Lists.List;
+      Descriptor : in WisiToken.Descriptor);
+
+end WisiToken.Generate.LR;
diff --git a/wisitoken-generate-lr1_items.adb b/wisitoken-generate-lr1_items.adb
new file mode 100644
index 0000000..a62a4af
--- /dev/null
+++ b/wisitoken-generate-lr1_items.adb
@@ -0,0 +1,580 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2002, 2003, 2008, 2009, 2012 - 2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with Ada.Text_IO;
+with Ada.Strings.Unbounded;
+package body WisiToken.Generate.LR1_Items is
+   use type Ada.Strings.Unbounded.Unbounded_String;
+
+   ----------
+   --  body subprograms
+
+   function Get_Dot_IDs (Set : in Item_Lists.List; Descriptor : in 
WisiToken.Descriptor) return Token_ID_Arrays.Vector
+   is
+      use all type Token_ID_Arrays.Cursor;
+      use Item_Lists;
+      IDs : Token_ID_Set (Descriptor.First_Terminal .. 
Descriptor.Last_Nonterminal) := (others => False);
+   begin
+      for Item of Set loop
+         if Item.Dot /= Token_ID_Arrays.No_Element then
+            if Element (Item.Dot) /= Descriptor.EOF_ID then
+               IDs (Element (Item.Dot)) := True;
+            end if;
+         end if;
+      end loop;
+      return To_Array (IDs);
+   end Get_Dot_IDs;
+
+   function Merge
+     (Prod         : in     Production_ID;
+      Dot          : in     Token_ID_Arrays.Cursor;
+      Lookaheads   : in     Lookahead;
+      Existing_Set : in out Item_Set)
+     return Boolean
+   is
+      --  Merge item into Existing_Set. Return True if Existing_Set
+      --  is modified.
+
+      use Item_Lists;
+
+      Found    : constant Item_Lists.Cursor := Find (Prod, Dot, Existing_Set);
+      Modified : Boolean                    := False;
+   begin
+      if Found = No_Element then
+         Existing_Set.Set.Insert ((Prod, Dot, new Token_ID_Set'(Lookaheads)));
+
+         Modified := True;
+      else
+         Include (Ref (Found), Lookaheads, Modified);
+      end if;
+
+      return Modified;
+   end Merge;
+
+   ----------
+   --  Public subprograms, declaration order
+
+   function To_Lookahead (Item : in Token_ID; Descriptor : in 
WisiToken.Descriptor) return Lookahead
+   is begin
+      return Result : Token_ID_Set := (Descriptor.First_Terminal .. 
Descriptor.Last_Lookahead => False) do
+         Result (Item) := True;
+      end return;
+   end To_Lookahead;
+
+   function Lookahead_Image (Item : in Lookahead; Descriptor : in 
WisiToken.Descriptor) return String
+   is
+      use Ada.Strings.Unbounded;
+      Result : Unbounded_String := Null_Unbounded_String;
+   begin
+      for I in Item'Range loop
+         if Item (I) then
+            if Length (Result) > 0 then
+               Result := Result & "/";
+            end if;
+            Result := Result & Image (I, Descriptor);
+         end if;
+      end loop;
+      return To_String (Result);
+   end Lookahead_Image;
+
+   function Item_Compare (Left, Right : in Item) return SAL.Compare_Result
+   is begin
+      if Left.Prod.LHS > Right.Prod.LHS then
+         return SAL.Greater;
+      elsif Left.Prod.LHS < Right.Prod.LHS then
+         return SAL.Less;
+
+      elsif Left.Prod.RHS > Right.Prod.RHS then
+         return SAL.Greater;
+      elsif Left.Prod.RHS < Right.Prod.RHS then
+         return SAL.Less;
+
+      else
+         declare
+            Left_Index : Integer renames Token_ID_Arrays.To_Index (Left.Dot);
+            Right_Index : Integer renames Token_ID_Arrays.To_Index (Right.Dot);
+         begin
+            if Left_Index > Right_Index then
+               return SAL.Greater;
+            elsif Left_Index < Right_Index then
+               return SAL.Less;
+            else
+               return SAL.Equal;
+            end if;
+         end;
+      end if;
+   end Item_Compare;
+
+   procedure Include
+     (Item  : in out LR1_Items.Item;
+      Value : in     Lookahead;
+      Added :    out Boolean)
+   is begin
+      Added := False;
+
+      for I in Item.Lookaheads'Range loop
+         if Value (I) then
+            Added := Added or not Item.Lookaheads (I);
+            Item.Lookaheads (I) := True;
+         end if;
+      end loop;
+   end Include;
+
+   procedure Include
+     (Item       : in out LR1_Items.Item;
+      Value      : in     Lookahead;
+      Descriptor : in     WisiToken.Descriptor)
+   is
+      Added : Boolean;
+      pragma Unreferenced (Added);
+   begin
+      Include (Item, Value, Added, Descriptor);
+   end Include;
+
+   procedure Include
+     (Item       : in out LR1_Items.Item;
+      Value      : in     Lookahead;
+      Added      :    out Boolean;
+      Descriptor : in     WisiToken.Descriptor)
+   is begin
+      Added := False;
+
+      for I in Item.Lookaheads'Range loop
+         if I = Descriptor.Last_Lookahead then
+            null;
+         else
+            if Value (I) then
+               Added := Added or not Item.Lookaheads (I);
+               Item.Lookaheads (I) := True;
+            end if;
+         end if;
+      end loop;
+   end Include;
+
+   function Filter
+     (Set        : in     Item_Set;
+      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in     WisiToken.Descriptor;
+      Include    : access function
+        (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+         Descriptor : in WisiToken.Descriptor;
+         Item       : in LR1_Items.Item)
+        return Boolean)
+     return Item_Set
+   is begin
+      return Result : Item_Set := (Set => <>, Goto_List => Set.Goto_List, 
Dot_IDs => Set.Dot_IDs, State => Set.State)
+      do
+         for Item of Set.Set loop
+            if Include (Grammar, Descriptor, Item) then
+               Result.Set.Insert (Item);
+            end if;
+         end loop;
+      end return;
+   end Filter;
+
+   function In_Kernel
+     (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor;
+      Item       : in LR1_Items.Item)
+     return Boolean
+   is
+      use Token_ID_Arrays;
+      Prod : WisiToken.Productions.Instance renames Grammar (Item.Prod.LHS);
+      RHS  : WisiToken.Productions.Right_Hand_Side renames Prod.RHSs 
(Item.Prod.RHS);
+   begin
+      return
+        No_Element /= RHS.Tokens.First and
+        (Item.Dot = No_Element or else
+           ((Prod.LHS = Descriptor.Accept_ID and
+               Item.Dot = RHS.Tokens.First)
+              -- Start symbol production with dot before first token.
+              or
+              Item.Dot /= RHS.Tokens.First));
+   end In_Kernel;
+
+   function Find
+     (Item : in LR1_Items.Item;
+      Set  : in Item_Set)
+     return Item_Lists.Cursor
+   is begin
+      return Find (Item.Prod, Item.Dot, Set);
+   end Find;
+
+   function Find
+     (Prod  : in Production_ID;
+      Dot   : in Token_ID_Arrays.Cursor;
+      Right : in Item_Set)
+     return Item_Lists.Cursor
+   is begin
+      return Right.Set.Find ((Prod, Dot, null));
+   end Find;
+
+   function Find
+     (Prod       : in Production_ID;
+      Dot        : in Token_ID_Arrays.Cursor;
+      Right      : in Item_Set;
+      Lookaheads : in Lookahead)
+     return Item_Lists.Cursor
+   is
+      use Item_Lists;
+      Result : constant Cursor := Right.Set.Find ((Prod, Dot, null));
+   begin
+      --  Item_Equal does not consider lookaheads
+      if Result = No_Element then
+         return Result;
+      elsif Constant_Ref (Result).Lookaheads.all = Lookaheads then
+         return Result;
+      else
+         return No_Element;
+      end if;
+   end Find;
+
+   function To_Item_Set_Tree_Key
+     (Item_Set           : in LR1_Items.Item_Set;
+      Include_Lookaheads : in Boolean)
+     return Item_Set_Tree_Key
+   is
+      use Interfaces;
+      use Item_Lists;
+      Cur : Item_Lists.Cursor := Item_Set.Set.First;
+   begin
+      return Result : Item_Set_Tree_Key do
+         Result.Append (Integer_16 (Item_Set.Set.Length));
+         --  Int_Arrays."<" compares length, but only after everything else; we
+         --  want it to compare first, since it is most likely to be different.
+
+         loop
+            exit when Cur = No_Element;
+            declare
+               Item_1 : Item renames Item_Set.Set (Cur);
+            begin
+               Result.Append (Integer_16 (Item_1.Prod.LHS));
+               Result.Append (Integer_16 (Item_1.Prod.RHS));
+               Result.Append (Integer_16 (Token_ID_Arrays.To_Index 
(Item_1.Dot)));
+               if Include_Lookaheads then
+                  for ID in Item_1.Lookaheads'Range loop
+                     if Item_1.Lookaheads (ID) then
+                        Result.Append (Integer_16 (ID));
+                     end if;
+                  end loop;
+               end if;
+            end;
+            Next (Cur);
+         end loop;
+      end return;
+   end To_Item_Set_Tree_Key;
+
+   function Find
+     (New_Item_Set     : in Item_Set;
+      Item_Set_Tree    : in Item_Set_Trees.Tree;
+      Match_Lookaheads : in Boolean)
+     return Unknown_State_Index
+   is
+      use all type Item_Set_Trees.Cursor;
+
+      Tree_It    : constant Item_Set_Trees.Iterator := Item_Set_Trees.Iterate 
(Item_Set_Tree);
+      Key        : constant Item_Set_Tree_Key       := To_Item_Set_Tree_Key
+        (New_Item_Set, Include_Lookaheads => Match_Lookaheads);
+      Found_Tree : constant Item_Set_Trees.Cursor   := Tree_It.Find (Key);
+   begin
+      if Found_Tree = Item_Set_Trees.No_Element then
+         return Unknown_State;
+      else
+         return Item_Set_Tree.Constant_Ref (Found_Tree).State;
+      end if;
+   end Find;
+
+   procedure Add
+     (New_Item_Set       : in out Item_Set;
+      Item_Set_Vector    : in out Item_Set_List;
+      Item_Set_Tree      : in out Item_Set_Trees.Tree;
+      Descriptor         : in     WisiToken.Descriptor;
+      Include_Lookaheads : in     Boolean)
+   is
+      use Item_Set_Trees;
+      Key : constant Item_Set_Tree_Key := To_Item_Set_Tree_Key (New_Item_Set, 
Include_Lookaheads);
+   begin
+      New_Item_Set.Dot_IDs := Get_Dot_IDs (New_Item_Set.Set, Descriptor);
+      Item_Set_Vector.Append (New_Item_Set);
+      Item_Set_Tree.Insert ((Key, New_Item_Set.State));
+   end Add;
+
+   function Is_In
+     (Item      : in Goto_Item;
+      Goto_List : in Goto_Item_Lists.List)
+     return Boolean
+   is begin
+      for List_Item of Goto_List loop
+         if List_Item = Item then
+            return True;
+         end if;
+      end loop;
+
+      return False;
+   end Is_In;
+
+   function Goto_State
+     (From   : in Item_Set;
+      Symbol : in Token_ID)
+     return Unknown_State_Index
+   is begin
+      for Item of From.Goto_List loop
+         if Item.Symbol = Symbol then
+            return Item.State;
+         end if;
+      end loop;
+
+      return Unknown_State;
+   end Goto_State;
+
+   function Closure
+     (Set                     : in Item_Set;
+      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 Item_Set
+   is
+      use all type Item_Lists.Cursor;
+      use Token_ID_Arrays;
+
+      --  [dragon] algorithm 4.9 pg 231; figure 4.38 pg 232; procedure 
"closure"
+      --
+      --  Taken literally, the algorithm modifies its input; we make a
+      --  copy instead.
+
+      I : Item_Set; --  The result.
+
+      Item_I     : Item_Lists.Cursor; -- iterator 'for each item in I'
+      Added_Item : Boolean := False;  -- 'until no more items can be added'
+
+      Beta : Token_ID_Arrays.Cursor; -- into RHS.Tokens
+   begin
+      I := Set;
+
+      Item_I := I.Set.First;
+      loop
+         declare
+            Item : LR1_Items.Item renames I.Set (Item_I);
+         begin
+            --  An item has the structure [A -> alpha Dot B Beta, a].
+            --
+            --  If B is a nonterminal, find its productions and place
+            --  them in the set with lookaheads from FIRST(Beta a).
+            if Item.Dot /= No_Element and then
+              Element (Item.Dot) in Descriptor.First_Nonterminal .. 
Descriptor.Last_Nonterminal
+            then
+               declare
+                  Prod : WisiToken.Productions.Instance renames Grammar 
(Element (Item.Dot));
+               begin
+
+                  For_Each_RHS :
+                  for B in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
+                     declare
+                        RHS : WisiToken.Productions.Right_Hand_Side renames 
Prod.RHSs (B);
+                        P_ID : constant Production_ID := (Prod.LHS, B);
+                     begin
+                        --  Compute FIRST (<tail of right hand side> a); loop
+                        --  until find a terminal, a nonterminal that
+                        --  cannot be empty, or end of production, adding
+                        --  items on the way.
+
+                        Beta := Next (Item.Dot); -- tokens after nonterminal, 
possibly null
+
+                        First_Tail :
+                        loop
+                           if Beta = No_Element then
+                              --  Use FIRST (a); a = Item.Lookaheads.
+                              --  Lookaheads are all terminals, so
+                              --  FIRST (a) = a.
+                              Added_Item := Added_Item or
+                                Merge (P_ID, RHS.Tokens.First, 
Item.Lookaheads.all, I);
+                              exit First_Tail;
+
+                           elsif Element (Beta) in Descriptor.First_Terminal 
.. Descriptor.Last_Terminal then
+                              --  FIRST (Beta) = Beta
+                              Added_Item := Added_Item or Merge
+                                (P_ID, RHS.Tokens.First, To_Lookahead (Element 
(Beta), Descriptor), I);
+                              exit First_Tail;
+
+                           else
+                              --  Beta is a nonterminal; use FIRST (Beta)
+                              for Terminal of First_Terminal_Sequence (Element 
(Beta)) loop
+                                 Added_Item := Added_Item or
+                                   Merge (P_ID, RHS.Tokens.First, To_Lookahead 
(Terminal, Descriptor), I);
+                              end loop;
+
+                              if Has_Empty_Production (Element (Beta)) then
+                                 --  Process the next token in the tail, or "a"
+                                 Beta := Next (Beta);
+                              else
+                                 exit First_Tail;
+                              end if;
+                           end if;
+                        end loop First_Tail;
+                     end;
+                  end loop For_Each_RHS;
+               end;
+            end if; -- Dot is at non-terminal
+         end;
+
+         if Item_Lists.Next (Item_I) = Item_Lists.No_Element then
+            exit when not Added_Item;
+
+            Item_I := I.Set.First;
+            Added_Item := False;
+
+            if Trace_Generate > Extra then
+               Ada.Text_IO.Put_Line ("I:");
+               Put (Grammar, Descriptor, I);
+               Ada.Text_IO.New_Line;
+            end if;
+         else
+            Item_I := Item_Lists.Next (Item_I);
+         end if;
+      end loop;
+
+      return I;
+   end Closure;
+
+   function Productions (Set : in Item_Set) return Production_ID_Arrays.Vector
+   is begin
+      return Result : Production_ID_Arrays.Vector do
+         for Item of Set.Set loop
+            Result.Append (Item.Prod);
+         end loop;
+      end return;
+   end Productions;
+
+   function Image
+     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor      : in WisiToken.Descriptor;
+      Item            : in LR1_Items.Item;
+      Show_Lookaheads : in Boolean)
+     return String
+   is
+      use Token_ID_Arrays;
+
+      I : Cursor;
+
+      Prod   : WisiToken.Productions.Instance renames Grammar (Item.Prod.LHS);
+      RHS    : WisiToken.Productions.Right_Hand_Side renames Prod.RHSs 
(Item.Prod.RHS);
+      Result : Ada.Strings.Unbounded.Unbounded_String :=
+        +Padded_Image (Item.Prod, Width => Prod_ID_Image_Width) & ":" & Image 
(Prod.LHS, Descriptor) & " <=";
+   begin
+      I := RHS.Tokens.First;
+
+      while I /= No_Element loop
+         if I = Item.Dot then
+            Result := Result & " ^ ";
+         else
+            Result := Result & " ";
+         end if;
+         Result := Result & Image (Element (I), Descriptor);
+         Next (I);
+      end loop;
+
+      if Item.Dot = No_Element then
+         Result := Result & " ^";
+      end if;
+
+      if Show_Lookaheads then
+         Result := Result & ", " & Lookahead_Image (Item.Lookaheads.all, 
Descriptor);
+      end if;
+
+      return Ada.Strings.Unbounded.To_String (Result);
+   end Image;
+
+   procedure Put
+     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor      : in WisiToken.Descriptor;
+      Item            : in LR1_Items.Item;
+      Show_Lookaheads : in Boolean := True)
+   is begin
+      Ada.Text_IO.Put (Image (Grammar, Descriptor, Item, Show_Lookaheads => 
Show_Lookaheads));
+   end Put;
+
+   procedure Put
+     (Descriptor : in WisiToken.Descriptor;
+      List       : in Goto_Item_Lists.List)
+   is
+      use Ada.Text_IO;
+   begin
+      for Item of List loop
+         Put_Line
+           ("      on " & Image (Item.Symbol, Descriptor) &
+              " => State" & Unknown_State_Index'Image (Item.State));
+      end loop;
+   end Put;
+
+   procedure Put
+     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor      : in WisiToken.Descriptor;
+      Item            : in Item_Set;
+      Show_Lookaheads : in Boolean := True;
+      Kernel_Only     : in Boolean := False;
+      Show_Goto_List  : in Boolean := False)
+   is
+      use Ada.Text_IO;
+   begin
+      if Item.State /= Unknown_State then
+         Put_Line ("State" & Unknown_State_Index'Image (Item.State) & ":");
+      end if;
+
+      for It of Item.Set loop
+         if not Kernel_Only or else
+           In_Kernel (Grammar, Descriptor, It)
+         then
+            Put_Line
+              ("  " & Image (Grammar, Descriptor, It, Show_Lookaheads => 
Show_Lookaheads));
+         end if;
+      end loop;
+
+      if Show_Goto_List then
+         Put (Descriptor, Item.Goto_List);
+      end if;
+   end Put;
+
+   procedure Put
+     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor      : in WisiToken.Descriptor;
+      Item            : in Item_Set_List;
+      Show_Lookaheads : in Boolean := True)
+   is
+      use Ada.Text_IO;
+   begin
+      for Set of Item loop
+         Put (Grammar, Descriptor, Set, Show_Lookaheads);
+         Put_Line ("   Goto:");
+         Put (Descriptor, Set.Goto_List);
+      end loop;
+   end Put;
+
+end WisiToken.Generate.LR1_Items;
diff --git a/wisitoken-generate-lr1_items.ads b/wisitoken-generate-lr1_items.ads
new file mode 100644
index 0000000..ff90e4a
--- /dev/null
+++ b/wisitoken-generate-lr1_items.ads
@@ -0,0 +1,332 @@
+--  Abstract :
+--
+--  Types and operatorion for LR(1) items.
+--
+--  Copyright (C) 2003, 2008, 2013-2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with Interfaces;
+with SAL.Gen_Definite_Doubly_Linked_Lists_Sorted;
+with SAL.Gen_Unbounded_Definite_Red_Black_Trees;
+with SAL.Gen_Unbounded_Definite_Vectors.Gen_Comparable;
+with WisiToken.Productions;
+package WisiToken.Generate.LR1_Items is
+
+   use all type Interfaces.Integer_16;
+
+   subtype Lookahead is Token_ID_Set;
+   --  Picking a type for Lookahead is not straight-forward. The
+   --  operations required are (called numbers are for LR1 generate
+   --  ada_lite):
+   --
+   --  to_lookahead (token_id)
+   --     Requires allocating memory dynamically:
+   --        an unconstrained array range (first_terminal .. last_terminal) 
for (1),
+   --        a smaller unconstrained array for (2), that grows as items are 
added
+   --        individual list elements for (3).
+   --
+   --     lr1_items.to_lookahead        called 4_821_256 times in (2)
+   --     sorted_token_id_lists.to_list called 4_821_256 times in (3)
+   --
+   --  for tok_id of lookaheads loop
+   --     sorted_token_id_lists__iterate called 5_687 times in (3)
+   --
+   --  if lookaheads.contains (tok_id) then
+   --     token_id_arrays__contains called 22_177_109 in (2)
+   --
+   --  new_item := (... , lookaheads => old_item.lookaheads)
+   --  new_item := (... , lookaheads => null_lookaheads)
+   --  new_item := (... , lookaheads => propagate_lookahead)
+   --     token_id_arrays.adjust called 8_437_967 times in (2)
+   --     sorted_token_id_lists.adjust  8_435_797 times in (3)
+   --
+   --  include: add tok_id to lookaheads
+   --
+   --      keep sorted in token_id order, so rest of algorithm is
+   --      stable/faster
+   --
+   --      lr1_items.include called 6_818_725 times in (2)
+   --
+   --  lookaheads /= lookaheads
+   --     if using a container, container must override "="
+   --
+   --  We've tried:
+   --
+   --  (1) Token_ID_Set (unconstrained array of boolean, allocated directly) - 
fastest
+   --
+   --     Allocates more memory than (2), but everything else is fast,
+   --     and it's not enough memory to matter.
+   --
+   --     Loop over lookaheads is awkward:
+   --     for tok_id in lookaheads'range loop
+   --        if lookaheads (tok_id) then
+   --           ...
+   --     But apparently it's fast enough.
+   --
+   --  (2) Instantiation of SAL.Gen_Unbounded_Definite_Vectors 
(token_id_arrays) - slower than (1).
+   --
+   --      Productions RHS is also token_id_arrays, so gprof numbers are
+   --      hard to sort out. Could be improved with a custom container, that
+   --      does sort and insert internally. Insert is inherently slow.
+   --
+   --  (3) Instantiation of SAL.Gen_Definite_Doubly_Linked_Lists_Sorted - 
slower than (2)
+
+   type Item is record
+      Prod       : Production_ID;
+      Dot        : Token_ID_Arrays.Cursor; -- token after item Dot
+      Lookaheads : access Lookahead := null;
+      --  Programmer must remember to copy Item.Lookaheads.all, not
+      --  Item.Lookaheads. Wrapping this in Ada.Finalization.Controlled
+      --  would just slow it down.
+      --
+      --  We don't free Lookaheads; we assume the user is running
+      --  wisi-generate, and not keeping LR1_Items around.
+   end record;
+
+   function To_Lookahead (Item : in Token_ID; Descriptor : in 
WisiToken.Descriptor) return Lookahead;
+
+   function Contains (Item : in Lookahead; ID : in Token_ID) return Boolean
+     is (Item (ID));
+
+   function Lookahead_Image (Item : in Lookahead; Descriptor : in 
WisiToken.Descriptor) return String;
+   --  Returns the format used in parse table output.
+
+   function Item_Compare (Left, Right : in Item) return SAL.Compare_Result;
+   --  Sort Item_Lists in ascending order of Prod.Nonterm, Prod.RHS, Dot;
+   --  ignores Lookaheads.
+
+   package Item_Lists is new SAL.Gen_Definite_Doubly_Linked_Lists_Sorted 
(Item, Item_Compare);
+
+   procedure Include
+     (Item  : in out LR1_Items.Item;
+      Value : in     Lookahead;
+      Added :    out Boolean);
+   --  Add Value to Item.Lookahead, if not already present.
+   --
+   --  Added is True if Value was not already present.
+   --
+   --  Does not exclude Propagate_ID.
+
+   procedure Include
+     (Item       : in out LR1_Items.Item;
+      Value      : in     Lookahead;
+      Descriptor : in     WisiToken.Descriptor);
+   --  Add Value to Item.Lookahead. Does not check if already present.
+   --  Excludes Propagate_ID.
+
+   procedure Include
+     (Item       : in out LR1_Items.Item;
+      Value      : in     Lookahead;
+      Added      :    out Boolean;
+      Descriptor : in     WisiToken.Descriptor);
+   --  Add Value to Item.Lookahead.
+
+   type Goto_Item is record
+      Symbol : Token_ID;
+      --  If Symbol is a terminal, this is a shift and goto state action.
+      --  If Symbol is a non-terminal, this is a post-reduce goto state action.
+      State  : State_Index;
+   end record;
+
+   function Goto_Item_Compare (Left, Right : in Goto_Item) return 
SAL.Compare_Result is
+     (if Left.Symbol > Right.Symbol then SAL.Greater
+      elsif Left.Symbol < Right.Symbol then SAL.Less
+      else SAL.Equal);
+   --  Sort Goto_Item_Lists in ascending order of Symbol.
+
+   package Goto_Item_Lists is new SAL.Gen_Definite_Doubly_Linked_Lists_Sorted
+     (Goto_Item, Goto_Item_Compare);
+
+   type Item_Set is record
+      Set       : Item_Lists.List;
+      Goto_List : Goto_Item_Lists.List;
+      Dot_IDs   : Token_ID_Arrays.Vector;
+      State     : Unknown_State_Index := Unknown_State;
+   end record;
+
+   function Filter
+     (Set        : in     Item_Set;
+      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in     WisiToken.Descriptor;
+      Include    : access function
+        (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+         Descriptor : in WisiToken.Descriptor;
+         Item       : in LR1_Items.Item)
+        return Boolean)
+     return Item_Set;
+   --  Return a deep copy of Set, including only items for which Include 
returns True.
+
+   function In_Kernel
+     (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor;
+      Item       : in LR1_Items.Item)
+     return Boolean;
+   --  For use with Filter; [dragon] sec 4.7 pg 240
+
+   function Find
+     (Item : in LR1_Items.Item;
+      Set  : in Item_Set)
+     return Item_Lists.Cursor;
+   --  Return an item from Set that matches Item.Prod, Item.Dot.
+   --
+   --  Return No_Element if not found.
+
+   function Find
+     (Prod  : in Production_ID;
+      Dot   : in Token_ID_Arrays.Cursor;
+      Right : in Item_Set)
+     return Item_Lists.Cursor;
+   --  Return an item from Right that matches Prod, Dot.
+   --
+   --  Return No_Element if not found.
+
+   function Find
+     (Prod       : in Production_ID;
+      Dot        : in Token_ID_Arrays.Cursor;
+      Right      : in Item_Set;
+      Lookaheads : in Lookahead)
+     return Item_Lists.Cursor;
+   --  Return an item from Right that matches Prod, Dot, and
+   --  Lookaheads.
+   --
+   --  Return No_Element if not found.
+   --
+   --  Not combined with non-Lookaheads version for speed; this is called
+   --  a lot.
+
+   package Item_Set_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(State_Index, Item_Set);
+   subtype Item_Set_List is Item_Set_Arrays.Vector;
+
+   package State_Index_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Positive, State_Index);
+
+   package Int_Arrays is new SAL.Gen_Unbounded_Definite_Vectors (Positive, 
Interfaces.Integer_16);
+   function Compare_Integer_16 (Left, Right : in Interfaces.Integer_16) return 
SAL.Compare_Result is
+     (if Left > Right then SAL.Greater
+      elsif Left < Right then SAL.Less
+      else SAL.Equal);
+
+   package Int_Arrays_Comparable is new Int_Arrays.Gen_Comparable 
(Compare_Integer_16);
+
+   subtype Item_Set_Tree_Key is Int_Arrays_Comparable.Vector;
+   --  We want a key that is fast to compare, and has enough info to
+   --  significantly speed the search for an item set. So we convert all
+   --  relevant data in an item into a string of integers. We need 16 bit
+   --  because Ada token_ids max is 332. LR1 keys include lookaheads,
+   --  LALR keys do not.
+
+   type Item_Set_Tree_Node is record
+      Key   : Item_Set_Tree_Key;
+      State : Unknown_State_Index;
+   end record;
+
+   function To_Item_Set_Tree_Key
+     (Item_Set           : in LR1_Items.Item_Set;
+      Include_Lookaheads : in Boolean)
+     return Item_Set_Tree_Key;
+
+   function To_Item_Set_Tree_Key (Node : in Item_Set_Tree_Node) return 
Item_Set_Tree_Key is
+     (Node.Key);
+
+   package Item_Set_Trees is new SAL.Gen_Unbounded_Definite_Red_Black_Trees
+     (Element_Type => Item_Set_Tree_Node,
+      Key_Type     => Item_Set_Tree_Key,
+      Key          => To_Item_Set_Tree_Key,
+      Key_Compare  => Int_Arrays_Comparable.Compare);
+   --  Item_Set_Arrays.Vector holds state item sets indexed by state, for
+   --  iterating in state order. Item_Set_Trees.Tree holds lists of state
+   --  indices sorted by LR1 item info, for fast Find in LR1_Item_Sets
+   --  and LALR_Kernels.
+
+   function Find
+     (New_Item_Set     : in Item_Set;
+      Item_Set_Tree    : in Item_Set_Trees.Tree;
+      Match_Lookaheads : in Boolean)
+     return Unknown_State_Index;
+   --  Return the State of an element in Item_Set_Tree matching
+   --  New_Item_Set, Unknown_State if not found.
+   --
+   --  Match_Lookaheads is True in LR1_Generate.
+
+   procedure Add
+     (New_Item_Set       : in out Item_Set;
+      Item_Set_Vector    : in out Item_Set_List;
+      Item_Set_Tree      : in out Item_Set_Trees.Tree;
+      Descriptor         : in     WisiToken.Descriptor;
+      Include_Lookaheads : in     Boolean);
+   --  Set New_Item_Set.Dot_IDs, add New_Item_Set to Item_Set_Vector, 
Item_Set_Tree
+
+   function Is_In
+     (Item      : in Goto_Item;
+      Goto_List : in Goto_Item_Lists.List)
+     return Boolean;
+   --  Return True if a goto on Symbol to State is found in Goto_List
+
+   function Goto_State
+     (From   : in Item_Set;
+      Symbol : in Token_ID)
+     return Unknown_State_Index;
+   --  Return state from From.Goto_List where the goto symbol is
+   --  Symbol; Unknown_State if not found.
+
+   function Closure
+     (Set                     : in Item_Set;
+      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 Item_Set;
+   --  Return the closure of Set over Grammar. First must be the
+   --  result of First above. Makes a deep copy of Goto_List.
+   --  Implements 'closure' from [dragon] algorithm 4.9 pg 232, but
+   --  allows merging lookaheads into one item..
+
+   function Productions (Set : in Item_Set) return Production_ID_Arrays.Vector;
+
+   procedure Put
+     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor      : in WisiToken.Descriptor;
+      Item            : in LR1_Items.Item;
+      Show_Lookaheads : in Boolean := True);
+
+   procedure Put
+     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor      : in WisiToken.Descriptor;
+      Item            : in Item_Set;
+      Show_Lookaheads : in Boolean := True;
+      Kernel_Only     : in Boolean := False;
+      Show_Goto_List  : in Boolean := False);
+
+   procedure Put
+     (Descriptor : in WisiToken.Descriptor;
+      List       : in Goto_Item_Lists.List);
+   procedure Put
+     (Grammar         : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor      : in WisiToken.Descriptor;
+      Item            : in Item_Set_List;
+      Show_Lookaheads : in Boolean := True);
+   --  Put Item to Ada.Text_IO.Standard_Output. Does not end with New_Line.
+
+end WisiToken.Generate.LR1_Items;
diff --git a/wisitoken-generate-packrat.adb b/wisitoken-generate-packrat.adb
new file mode 100644
index 0000000..068b6d0
--- /dev/null
+++ b/wisitoken-generate-packrat.adb
@@ -0,0 +1,247 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+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
new file mode 100644
index 0000000..152dd99
--- /dev/null
+++ b/wisitoken-generate-packrat.ads
@@ -0,0 +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 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);
+
+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
new file mode 100644
index 0000000..aba997b
--- /dev/null
+++ b/wisitoken-generate.adb
@@ -0,0 +1,495 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+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;
+
+   function Ancestors
+     (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return Token_Array_Token_Set
+   is
+      use all type Ada.Containers.Count_Type;
+
+      Done : Boolean := False;
+   begin
+      return All_Ancestors : Token_Array_Token_Set
+          (Descriptor.First_Terminal .. Grammar.Last_Index,
+           Grammar.First_Index .. Grammar.Last_Index) :=
+             (others => (others => False))
+      do
+         loop
+            exit when Done;
+            Done := True;
+            for Prod of Grammar loop
+               for R of Prod.RHSs loop
+                  if R.Tokens.Length = 1 then
+                     declare
+                        ID : constant Token_ID := R.Tokens (1);
+                     begin
+                        if not All_Ancestors (ID, Prod.LHS) then
+                           Done := False;
+                        end if;
+                        All_Ancestors (ID, Prod.LHS) := True;
+                        for J in All_Ancestors'Range (2) loop
+                           if All_Ancestors (Prod.LHS, J) then
+                              if not All_Ancestors (ID, J) then
+                                 Done := False;
+                                 All_Ancestors (ID, J) := True;
+                              end if;
+                           end if;
+                        end loop;
+                     end;
+                  end if;
+               end loop;
+            end loop;
+         end loop;
+      end return;
+   end Ancestors;
+
+   function Descendants
+     (Ancestors : in Token_Array_Token_Set)
+     return Token_Sequence_Arrays.Vector
+   is
+      subtype Nonterminals is Token_ID range Ancestors'First (2) .. 
Ancestors'Last (2);
+   begin
+      return Result : Token_Sequence_Arrays.Vector do
+         Result.Set_First_Last (Ancestors'First (2), Ancestors'Last (2));
+         for I in Ancestors'Range (1) loop
+            for J in Ancestors'Range (2) loop
+               if I in Nonterminals and Ancestors (I, J) then
+                  Result (J).Append (I);
+               end if;
+            end loop;
+         end loop;
+      end return;
+   end Descendants;
+
+   ----------
+   --  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
new file mode 100644
index 0000000..19648d2
--- /dev/null
+++ b/wisitoken-generate.ads
@@ -0,0 +1,136 @@
+--  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 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);
+
+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.
+
+   function Ancestors
+     (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return Token_Array_Token_Set;
+   --  For each terminal and nonterm, record the nonterms it reduces to
+   --  via one token reductions, recursively. In other words, if there is
+   --  a production J <= I, then Ancestors (I, J) is True.
+
+   function Descendants
+     (Ancestors : in Token_Array_Token_Set)
+     return Token_Sequence_Arrays.Vector;
+   --  Inverse of Ancestors, excluding terminals. If there is a
+   --  production J <= I and I is a nonterminal, then I is present in
+   --  Descendants (J).
+
+   ----------
+   --  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
new file mode 100644
index 0000000..c5bc6af
--- /dev/null
+++ b/wisitoken-lexer-re2c.adb
@@ -0,0 +1,244 @@
+--  Abstract:
+--
+--  see spec.
+--
+--  Copyright (C) 2017, 2018 Stephe Leake
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with Ada.Directories;
+with Ada.Strings.Unbounded;
+with GNATCOLL.Mmap;
+package body WisiToken.Lexer.re2c is
+
+   overriding procedure Finalize (Object : in out Instance)
+   is
+      use all type System.Address;
+   begin
+      if Object.Lexer /= System.Null_Address then
+         Free_Lexer (Object.Lexer);
+         Object.Lexer := System.Null_Address;
+      end if;
+
+      Finalize (Object.Source);
+   end Finalize;
+
+   type Instance_Access is access Instance; --  silence compiler warning
+
+   function New_Lexer
+     (Trace  : not null access WisiToken.Trace'Class)
+     return Handle
+   is begin
+      return Handle (Instance_Access'(new Instance (Trace)));
+   end New_Lexer;
+
+   overriding procedure Reset_With_String (Lexer : in out Instance; Input : in 
String)
+   is begin
+      Finalize (Lexer);
+
+      --  We assume Input is in UTF-8 encoding
+      Lexer.Source :=
+        (Label       => String_Label,
+         File_Name   => +"",
+         Buffer      => new String'(Input),
+         User_Buffer => False);
+
+      Lexer.Lexer := New_Lexer
+        (Buffer    => Lexer.Source.Buffer.all'Address,
+         Length    => Interfaces.C.size_t (Input'Length),
+         Verbosity => Interfaces.C.int (if Trace_Parse > 3 then Trace_Parse - 
3 else 0));
+
+      Reset (Lexer);
+   end Reset_With_String;
+
+   overriding procedure Reset_With_String_Access
+     (Lexer     : in out Instance;
+      Input     : in     Ada.Strings.Unbounded.String_Access;
+      File_Name : in     Ada.Strings.Unbounded.Unbounded_String)
+   is begin
+      Finalize (Lexer);
+
+      --  We assume Input is in UTF-8 encoding
+      Lexer.Source :=
+        (Label       => String_Label,
+         File_Name   =>
+           +(if Ada.Strings.Unbounded.Length (File_Name) = 0 then ""
+             else Ada.Directories.Simple_Name (-File_Name)),
+         Buffer      => Input,
+         User_Buffer => True);
+
+      Lexer.Lexer := New_Lexer
+        (Buffer    => Lexer.Source.Buffer.all'Address,
+         Length    => Interfaces.C.size_t (Input'Length),
+         Verbosity => Interfaces.C.int (if Trace_Parse > 3 then Trace_Parse - 
3 else 0));
+
+      Reset (Lexer);
+   end Reset_With_String_Access;
+
+   overriding procedure Reset_With_File (Lexer : in out Instance; File_Name : 
in String)
+   is
+      use GNATCOLL.Mmap;
+   begin
+      Finalize (Lexer);
+
+      --  We assume the file is in UTF-8 encoding
+      Lexer.Source :=
+        (File_Label, +Ada.Directories.Simple_Name (File_Name), Open_Read 
(File_Name), Invalid_Mapped_Region, 1);
+
+      Lexer.Source.Region      := Read (Lexer.Source.File);
+      Lexer.Source.Buffer_Last := Last (Lexer.Source.Region);
+
+      if Integer (Length (Lexer.Source.File)) /= Lexer.Source.Buffer_Last then
+         raise SAL.Programmer_Error with "not all of file is mapped; file 
length" &
+           File_Size'Image (Length (Lexer.Source.File)) & " mapped:" & 
Integer'Image (Lexer.Source.Buffer_Last);
+      end if;
+
+      Lexer.Lexer := New_Lexer
+        (Buffer    => Data (Lexer.Source.Region).all'Address,
+         Length    => Interfaces.C.size_t (Last (Lexer.Source.Region)),
+         Verbosity => Interfaces.C.int (if Trace_Parse > 3 then Trace_Parse - 
3 else 0));
+
+      Reset (Lexer);
+   end Reset_With_File;
+
+   overriding procedure Reset (Lexer : in out Instance)
+   is begin
+      Reset_Lexer (Lexer.Lexer);
+      Lexer.Line            := 1;
+      Lexer.Char_Line_Start := 1;
+      Lexer.ID :=
+        --  First token is assumed to be first on a line.
+        (if Lexer.Trace.Descriptor.New_Line_ID = Invalid_Token_ID
+         then Invalid_Token_ID
+         else Lexer.Trace.Descriptor.New_Line_ID);
+   end Reset;
+
+   overriding function Find_Next
+     (Lexer : in out Instance;
+      Token :    out Base_Token)
+     return Boolean
+   is
+      use Interfaces.C;
+
+      procedure Build_Token
+      is begin
+         Token :=
+           (ID => Lexer.ID,
+
+            Byte_Region =>
+              (Buffer_Pos (Lexer.Byte_Position),
+               Base_Buffer_Pos (Lexer.Byte_Position + Lexer.Byte_Length - 1)),
+
+            Line => Lexer.Line,
+
+            Column =>
+              (if Lexer.ID = Lexer.Trace.Descriptor.New_Line_ID or
+                 Lexer.ID = Lexer.Trace.Descriptor.EOF_ID
+               then 0
+               else Ada.Text_IO.Count (Lexer.Char_Position - 
Lexer.Char_Line_Start)),
+
+            Char_Region =>
+              (Buffer_Pos (Lexer.Char_Position),
+               Base_Buffer_Pos (Lexer.Char_Position + Lexer.Char_Length - 1)));
+      end Build_Token;
+
+   begin
+      Lexer.Prev_ID := Lexer.ID;
+      loop
+         declare
+            Status : constant int := Next_Token
+              (Lexer.Lexer, Lexer.ID,
+               Byte_Position => Interfaces.C.size_t (Lexer.Byte_Position),
+               Byte_Length   => Interfaces.C.size_t (Lexer.Byte_Length),
+               Char_Position => Interfaces.C.size_t (Lexer.Char_Position),
+               Char_Length   => Interfaces.C.size_t (Lexer.Char_Length),
+               Line_Start    => Interfaces.C.int (Lexer.Line));
+         begin
+            case Status is
+            when 0 =>
+               if Lexer.ID = Lexer.Trace.Descriptor.New_Line_ID then
+                  Lexer.Char_Line_Start := Lexer.Char_Position + 1;
+               end if;
+
+               Build_Token;
+               return False;
+
+            when 1 =>
+               --  Unrecognized character from lexer. Handle missing quotes by
+               --  inserting a virtual quote at the existing quote, and 
telling the
+               --  lexer to skip the char.
+               declare
+                  Buffer : constant GNATCOLL.Mmap.Str_Access := 
WisiToken.Lexer.Buffer (Lexer.Source);
+               begin
+                  if Buffer (Lexer.Byte_Position) = ''' then
+                     --  Lexer has read to next new-line (or eof), then 
backtracked to next
+                     --  char after '.
+                     Lexer.Errors.Append
+                       ((Buffer_Pos (Lexer.Char_Position), 
Invalid_Token_Index, (1 => ''', others => ASCII.NUL)));
+
+                     Lexer.ID := Lexer.Trace.Descriptor.String_1_ID;
+                     Build_Token;
+                     return True;
+
+                  elsif Buffer (Lexer.Byte_Position) = '"' then
+                     --  Lexer has read to next new-line (or eof), then 
backtracked to next
+                     --  char after ".
+                     Lexer.Errors.Append
+                       ((Buffer_Pos (Lexer.Char_Position), 
Invalid_Token_Index, (1 => '"', others => ASCII.NUL)));
+
+                     Lexer.ID := Lexer.Trace.Descriptor.String_2_ID;
+                     Build_Token;
+                     return True;
+
+                  else
+                     --  Just skip the character; call Next_Token again.
+                     Lexer.Errors.Append
+                       ((Buffer_Pos (Lexer.Char_Position), 
Invalid_Token_Index, (others => ASCII.NUL)));
+                  end if;
+               end;
+
+            when others =>
+               raise Fatal_Error with " lexer returned unrecognized status 
code" & int'Image (Status);
+            end case;
+         end;
+      end loop;
+   end Find_Next;
+
+   overriding function First (Lexer : in Instance) return Boolean
+   is begin
+      return Lexer.Trace.Descriptor.New_Line_ID /= Invalid_Token_ID and then
+           Lexer.Prev_ID = Lexer.Trace.Descriptor.New_Line_ID;
+   end First;
+
+   overriding function Buffer_Text (Lexer : in Instance; Byte_Bounds : in 
Buffer_Region) return String
+   is begin
+      return String (Buffer (Lexer.Source) (Integer (Byte_Bounds.First) .. 
Integer (Byte_Bounds.Last)));
+   end Buffer_Text;
+
+   overriding function File_Name (Lexer : in Instance) return String
+   is begin
+      return File_Name (Lexer.Source);
+   end File_Name;
+
+end WisiToken.Lexer.re2c;
diff --git a/wisitoken-lexer-re2c.ads b/wisitoken-lexer-re2c.ads
new file mode 100644
index 0000000..9bb33f5
--- /dev/null
+++ b/wisitoken-lexer-re2c.ads
@@ -0,0 +1,129 @@
+--  Abstract:
+--
+--  WisiToken wrapper around the re2c lexer
+--
+--  References:
+--
+--  [1] http://re2c.org/
+--
+--  Copyright (C) 2017, 2018 Stephe Leake
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. If not, write to the Free Software Foundation,
+--  59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+pragma License (GPL); -- GNATCOLL.Mmap
+
+pragma Warnings (Off, "license of withed unit ""GNATCOLL.Mmap"" may be 
inconsistent");
+
+pragma Warnings (On);
+with Interfaces.C;
+with System;
+generic
+   --  These subprograms are provided by generated source code.
+
+   with function New_Lexer
+     (Buffer    : in System.Address;
+      Length    : in Interfaces.C.size_t;
+      Verbosity : in Interfaces.C.int)
+     return System.Address;
+   --  Create the re2c lexer object, passing it the full text to process.
+   --  Length is buffer length in 8 bit bytes.
+
+   with procedure Free_Lexer (Lexer : in out System.Address);
+   --  Destruct the re2c lexer object
+
+   with procedure Reset_Lexer (Lexer : in System.Address);
+   --  Restart lexing, with previous input buffer.
+
+   with function Next_Token
+     (Lexer         : in     System.Address;
+      ID            :    out Token_ID;
+      Byte_Position :    out Interfaces.C.size_t;
+      Byte_Length   :    out Interfaces.C.size_t;
+      Char_Position :    out Interfaces.C.size_t;
+      Char_Length   :    out Interfaces.C.size_t;
+      Line_Start    :    out Interfaces.C.int)
+     return Interfaces.C.int;
+   --  *_Position and *_Length give the position and length in bytes and
+   --  characters of the token from the start of the buffer, 0 indexed.
+   --
+   --  Line_Start gives the line number in the source file that the first
+   --  character of the token is in, 1 indexed.
+   --
+   --  Result values:
+   --
+   --  0 - no error
+   --  1 - there is an unrecognized character at Position.
+
+package WisiToken.Lexer.re2c is
+
+   Invalid_Input : exception;
+
+   type Instance is new WisiToken.Lexer.Instance with private;
+
+   overriding procedure Finalize (Object : in out Instance);
+
+   function New_Lexer
+     (Trace  : not null access WisiToken.Trace'Class)
+     return WisiToken.Lexer.Handle;
+   --  If the tokens do not include a reporting New_Line token, set
+   --  New_Line_ID to Invalid_Token_ID.
+
+   overriding procedure Reset_With_String (Lexer : in out Instance; Input : in 
String);
+   --  Copies Input to internal buffer.
+
+   overriding procedure Reset_With_String_Access
+     (Lexer     : in out Instance;
+      Input     : in     Ada.Strings.Unbounded.String_Access;
+      File_Name : in     Ada.Strings.Unbounded.Unbounded_String);
+
+   overriding procedure Reset_With_File (Lexer : in out Instance; File_Name : 
in String);
+   --  Uses memory mapped file; no copies.
+
+   overriding procedure Discard_Rest_Of_Input (Lexer : in out Instance) is 
null;
+
+   overriding procedure Reset (Lexer : in out Instance);
+
+   overriding function Buffer_Text (Lexer : in Instance; Byte_Bounds : in 
Buffer_Region) return String;
+
+   overriding function First (Lexer : in Instance) return Boolean;
+
+   overriding
+   function Find_Next
+     (Lexer : in out Instance;
+      Token :    out Base_Token)
+     return Boolean;
+
+   overriding function File_Name (Lexer : in Instance) return String;
+
+private
+
+   type Instance is new WisiToken.Lexer.Instance with
+   record
+      Lexer         : System.Address := System.Null_Address;
+      Source        : WisiToken.Lexer.Source;
+      ID            : Token_ID; --  Last token read by find_next
+      Byte_Position : Natural; -- We don't use Buffer_Pos here, because 
Source.Buffer is indexed by Integer
+      Byte_Length   : Natural;
+      Char_Position : Natural;
+      Char_Length   : Natural;
+      --  Position and length in bytes and characters of last token from
+      --  start of Managed.Buffer, 1 indexed.
+
+      Line            : Line_Number_Type; -- after last (or current) New_Line 
token
+      Char_Line_Start : Natural;          -- Character position after last 
New_Line token
+      Prev_ID         : Token_ID;         -- previous token_id
+   end record;
+
+end WisiToken.Lexer.re2c;
diff --git a/wisitoken-lexer-regexp.adb b/wisitoken-lexer-regexp.adb
new file mode 100644
index 0000000..d355080
--- /dev/null
+++ b/wisitoken-lexer-regexp.adb
@@ -0,0 +1,240 @@
+--  Abstract:
+--
+--  See spec
+--
+--  Copyright (C) 2015, 2017, 2018 Stephe Leake
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with Ada.Characters.Latin_1;
+with SAL;
+package body WisiToken.Lexer.Regexp is
+
+   function Find_Best_Match (Lexer : in out Instance) return Boolean
+   is
+      --  Find the longest matching character sequence in the buffer that
+      --  matches a token.
+      --
+      --  Return True if a token is matched, False if not.
+
+      use WisiToken.Regexp;
+
+      Current_Char         : Integer := Lexer.Buffer_Head;
+      Current_State        : Match_State;
+      Current_Match_Length : Integer := 0;
+      Best_Match_ID        : Token_ID;
+      Best_Match_Length    : Natural := 0;
+      Still_Matching       : Boolean := False;
+   begin
+      --  We only support Reset_With_String.
+
+      if Current_Char > Lexer.Source.Buffer'Last then
+         Lexer.ID := Lexer.Trace.Descriptor.EOF_ID;
+         Lexer.Lexeme_Head := Lexer.Buffer_Head;
+         Lexer.Lexeme_Tail := Lexer.Buffer_Head - 1;
+         return True;
+      end if;
+
+      for I in Lexer.Syntax'Range loop
+         Clear (Lexer.Syntax (I).Regexp);
+      end loop;
+
+      loop
+         Still_Matching := False;
+
+         for I in Lexer.Syntax'Range loop
+            if State (Lexer.Syntax (I).Regexp) /= WisiToken.Regexp.Error then
+               Current_State := Match
+                 (Lexer.Syntax (I).Regexp,
+                  Lexer.Source.Buffer (Lexer.Buffer_Head .. 
Lexer.Source.Buffer'Last),
+                  Current_Char);
+
+               case Current_State is
+               when Matching =>
+                  Still_Matching := True;
+
+               when Final =>
+                  Still_Matching := True;
+
+                  Current_Match_Length := Current_Char - Lexer.Buffer_Head + 1;
+
+                  if Best_Match_Length < Current_Match_Length then
+                     Best_Match_ID  := I;
+                     Best_Match_Length := Current_Match_Length;
+                  end if;
+
+               when WisiToken.Regexp.Error =>
+                  null;
+               end case;
+            end if;
+         end loop;
+
+         exit when (not Still_Matching) or else (Current_Char = 
Lexer.Source.Buffer'Last);
+
+         if Best_Match_Length = Lexer.Source.Buffer'Length then
+            raise SAL.Programmer_Error with
+              "token larger than buffer size of" & Integer'Image 
(Lexer.Source.Buffer'Length);
+         end if;
+
+         Current_Char := Current_Char + 1;
+      end loop;
+
+      if Best_Match_Length > 0 then
+         Lexer.Lexeme_Head := Lexer.Buffer_Head;
+         Lexer.Lexeme_Tail := Lexer.Buffer_Head + Best_Match_Length - 1;
+         Lexer.ID          := Best_Match_ID;
+
+         if Lexer.Lexeme_Head = Lexer.Source.Buffer'Last and
+           Lexer.Source.Buffer (Lexer.Lexeme_Head) = Ada.Characters.Latin_1.EOT
+         then
+            --  matched EOF; repeat that next time
+            null;
+         else
+            Lexer.Buffer_Head := Lexer.Lexeme_Tail + 1;
+         end if;
+         return True;
+
+      elsif Current_Char = Lexer.Source.Buffer'Last then
+         Lexer.ID := Lexer.Trace.Descriptor.EOF_ID;
+         Lexer.Buffer_Head := Lexer.Buffer_Head + 1;
+         return True;
+
+      else
+         return False;
+      end if;
+
+   end Find_Best_Match;
+
+   ----------
+   --  Public subprograms
+
+   function Get
+     (Regexp         : in String;
+      Case_Sensitive : in Boolean := True;
+      Report         : in Boolean := True)
+      return Syntax_Item
+   is begin
+      return (WisiToken.Regexp.Compile (Regexp, Case_Sensitive), Report);
+   end Get;
+
+   type Instance_Access is access Instance; --  silence compiler warning
+
+   function New_Lexer
+     (Trace  : not null access WisiToken.Trace'Class;
+      Syntax : in              WisiToken.Lexer.Regexp.Syntax)
+     return WisiToken.Lexer.Handle
+   is
+      New_Lexer : constant Instance_Access := new Instance (Trace, 
Syntax'Last);
+   begin
+      New_Lexer.Syntax := Syntax;
+
+      return Handle (New_Lexer);
+   end New_Lexer;
+
+   overriding procedure Finalize (Object : in out Instance)
+   is begin
+      Finalize (Object.Source);
+   end Finalize;
+
+   overriding procedure Reset_With_String (Lexer : in out Instance; Input : in 
String)
+   is begin
+      Finalize (Lexer);
+
+      Lexer.Source :=
+        (Label       => String_Label,
+         File_Name   => +"",
+         Buffer      => new String'(Input),
+         User_Buffer => False);
+
+      Reset (Lexer);
+   end Reset_With_String;
+
+   overriding procedure Reset_With_String_Access
+     (Lexer     : in out Instance;
+      Input     : in     Ada.Strings.Unbounded.String_Access;
+      File_Name : in     Ada.Strings.Unbounded.Unbounded_String)
+   is begin
+      Finalize (Lexer);
+
+      Lexer.Source :=
+        (Label       => String_Label,
+         File_Name   => File_Name,
+         Buffer      => Input,
+         User_Buffer => True);
+
+      Reset (Lexer);
+   end Reset_With_String_Access;
+
+   overriding procedure Reset_With_File (Lexer : in out Instance; File_Name : 
in String)
+   is
+      pragma Unreferenced (File_Name);
+   begin
+      Finalize (Lexer);
+
+      raise SAL.Not_Implemented;
+   end Reset_With_File;
+
+   overriding procedure Reset
+     (Lexer : in out Instance)
+   is begin
+      Lexer.Lexeme_Head := Lexer.Source.Buffer'First;
+      Lexer.Lexeme_Tail := Lexer.Source.Buffer'First - 1;
+      Lexer.ID          := Invalid_Token_ID;
+      Lexer.Buffer_Head := Lexer.Source.Buffer'First;
+   end Reset;
+
+   overriding function Find_Next
+     (Lexer : in out Instance;
+      Token :    out Base_Token)
+     return Boolean
+   is begin
+      loop
+         if not Find_Best_Match (Lexer) then
+            if Lexer.Buffer_Head > Lexer.Source.Buffer'Last then
+               raise Syntax_Error with "Unrecognized EOF";
+            else
+               raise Syntax_Error with "Unrecognized character '" & 
Lexer.Source.Buffer (Lexer.Buffer_Head) & "'";
+            end if;
+         end if;
+
+         exit when Lexer.Syntax (Lexer.ID).Report;
+
+      end loop;
+
+      Token :=
+        (ID          => Lexer.ID,
+         Byte_Region => (Buffer_Pos (Lexer.Lexeme_Head), Buffer_Pos 
(Lexer.Lexeme_Tail)),
+         Line        => Invalid_Line_Number,
+         Column      => Ada.Text_IO.Count (Lexer.Lexeme_Head),
+         Char_Region => (Buffer_Pos (Lexer.Lexeme_Head), Buffer_Pos 
(Lexer.Lexeme_Tail)));
+
+      return False;
+   end Find_Next;
+
+   overriding function Buffer_Text (Lexer : in Instance; Byte_Region : in 
Buffer_Region) return String
+   is begin
+      return Lexer.Source.Buffer (Integer (Byte_Region.First) .. Integer 
(Byte_Region.Last));
+   end Buffer_Text;
+
+end WisiToken.Lexer.Regexp;
diff --git a/wisitoken-lexer-regexp.ads b/wisitoken-lexer-regexp.ads
new file mode 100644
index 0000000..33bda9b
--- /dev/null
+++ b/wisitoken-lexer-regexp.ads
@@ -0,0 +1,102 @@
+--  Abstract:
+--
+--  WisiToken lexer using compiled regular expressions interpreted at runtime.
+--
+--  This is slower, but easier to use, than the Aflex lexer; it is
+--  used in most of the WisiToken unit tests. Since it uses regexp, it
+--  is easy to convert to an Aflex lexer.
+--
+--  Copyright (C) 2015, 2017, 2018 Stephe Leake
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with Ada.Unchecked_Deallocation;
+with WisiToken.Regexp;
+package WisiToken.Lexer.Regexp is
+
+   type Syntax_Item is record
+      Regexp : WisiToken.Regexp.Regexp;
+      Report : Boolean;
+   end record;
+
+   function Get
+     (Regexp         : in String;
+      Case_Sensitive : in Boolean := True;
+      Report         : in Boolean := True)
+     return Syntax_Item;
+   --  Compiles Regexp with Case_Sensitive.
+
+   type Syntax is array (Token_ID range <>) of Syntax_Item;
+
+   type Instance
+     (Trace         : not null access WisiToken.Trace'Class;
+      Last_Terminal : Token_ID)
+     is new WisiToken.Lexer.Instance with private;
+
+   function New_Lexer
+     (Trace  : not null access WisiToken.Trace'Class;
+      Syntax : in              WisiToken.Lexer.Regexp.Syntax)
+     return WisiToken.Lexer.Handle;
+
+   overriding procedure Finalize (Object : in out Instance);
+   overriding procedure Reset_With_String (Lexer : in out Instance; Input : in 
String);
+   overriding procedure Reset_With_String_Access
+     (Lexer     : in out Instance;
+      Input     : in     Ada.Strings.Unbounded.String_Access;
+      File_Name : in     Ada.Strings.Unbounded.Unbounded_String);
+   overriding procedure Reset_With_File (Lexer : in out Instance; File_Name : 
in String);
+   overriding procedure Reset (Lexer : in out Instance);
+
+   overriding procedure Discard_Rest_Of_Input (Lexer : in out Instance) is 
null;
+
+   overriding function Find_Next
+     (Lexer : in out Instance;
+      Token :    out Base_Token)
+     return Boolean;
+
+   overriding function Buffer_Text (Lexer : in Instance; Byte_Region : in 
Buffer_Region) return String;
+
+   overriding function First (Lexer : in Instance) return Boolean is (False);
+
+   overriding function File_Name (Lexer : in Instance) return String is ("");
+
+private
+
+   type String_Access is access String;
+   procedure Free is new Ada.Unchecked_Deallocation (String, String_Access);
+
+   type Instance
+     (Trace         : not null access WisiToken.Trace'Class;
+      Last_Terminal : Token_ID)
+     is new WisiToken.Lexer.Instance (Trace => Trace) with
+   record
+      ID          : Token_ID; --  last token read by find_next
+      Syntax      : WisiToken.Lexer.Regexp.Syntax (Token_ID'First .. 
Last_Terminal);
+      Source      : Lexer.Source;
+      Buffer_Head : Integer;
+      Lexeme_Head : Integer;
+      Lexeme_Tail : Integer;
+   end record;
+
+end WisiToken.Lexer.Regexp;
diff --git a/wisitoken-lexer.adb b/wisitoken-lexer.adb
new file mode 100644
index 0000000..6d3d546
--- /dev/null
+++ b/wisitoken-lexer.adb
@@ -0,0 +1,56 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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);
+
+with GNAT.Strings;
+package body WisiToken.Lexer is
+
+   procedure Finalize (Object : in out Source)
+   is begin
+      case Object.Label is
+      when String_Label =>
+         if not Object.User_Buffer then
+            Ada.Strings.Unbounded.Free (Object.Buffer);
+         end if;
+
+      when File_Label =>
+         GNATCOLL.Mmap.Free (Object.Region);
+         GNATCOLL.Mmap.Close (Object.File);
+      end case;
+   end Finalize;
+
+   function Buffer (Source : in Lexer.Source) return GNATCOLL.Mmap.Str_Access
+   is
+      use GNATCOLL.Mmap;
+   begin
+      case Source.Label is
+      when String_Label =>
+         return Short.To_Str_Access (GNAT.Strings.String_Access 
(Source.Buffer));
+
+      when File_Label =>
+         return Data (Source.Region);
+      end case;
+
+   end Buffer;
+
+   function File_Name (Source : in Lexer.Source) return String
+   is begin
+      return -Source.File_Name;
+   end File_Name;
+
+end WisiToken.Lexer;
diff --git a/wisitoken-lexer.ads b/wisitoken-lexer.ads
new file mode 100644
index 0000000..b854004
--- /dev/null
+++ b/wisitoken-lexer.ads
@@ -0,0 +1,161 @@
+--  Abstract :
+--
+--  An abstract lexer interface.
+--
+--  Copyright (C) 2014 - 2015, 2017, 2018 Stephe Leake
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+pragma Warnings (Off, "license of withed unit ""GNATCOLL.Mmap"" may be 
inconsistent");
+
+with Ada.Containers.Doubly_Linked_Lists;
+with Ada.Finalization;
+with Ada.Strings.Unbounded;
+with GNATCOLL.Mmap;
+package WisiToken.Lexer is
+
+   type Error is record
+      Char_Pos : Buffer_Pos := Invalid_Buffer_Pos;
+      --  Character at that position is not recognized as part of a token.
+
+      Recover_Token : Base_Token_Index := Invalid_Token_Index;
+      --  If the error was corrected by inserting a missing quote, the token
+      --  (in shared parser Terminals) that was returned.
+
+      Recover_Char : String (1 .. 4) := (others => ASCII.NUL);
+      --  If the error was corrected, the character (in UTF-8 encoding) that
+      --  was inserted; unused trailing bytes set to ASCII.NUL. Otherwise,
+      --  all ASCII.Nul.
+   end record;
+
+   package Error_Lists is new Ada.Containers.Doubly_Linked_Lists (Error);
+
+   type Instance (Trace  : not null access WisiToken.Trace'Class)
+   is abstract new Ada.Finalization.Limited_Controlled with record
+      Errors : Error_Lists.List;
+   end record;
+
+   subtype Class is Instance'Class;
+
+   type Handle is access all Class;
+
+   procedure Reset_With_String (Lexer : in out Instance; Input : in String) is 
abstract;
+   --  Reset Lexer to start a new parse, reading from Input.
+
+   procedure Reset_With_String_Access
+     (Lexer     : in out Instance;
+      Input     : in     Ada.Strings.Unbounded.String_Access;
+      File_Name : in     Ada.Strings.Unbounded.Unbounded_String)
+     is abstract;
+   --  Reset Lexer to start a new parse, reading from Input.
+   --  File_Name is used for error messages.
+
+   procedure Reset_With_File (Lexer : in out Instance; File_Name : in String) 
is abstract;
+   --  Reset Lexer to start a new parse, reading from File_Name.
+   --
+   --  Raises Ada.IO_Exceptions.Name_Error if File_Name cannot be opened.
+
+   procedure Reset (Lexer : in out Instance) is abstract;
+   --  Reset Lexer, read from previous source.
+
+   procedure Discard_Rest_Of_Input (Lexer : in out Instance) is abstract;
+   --  If reading input from a stream, abort reading (or force it to
+   --  complete); Find_Next will not be called before another Reset.
+
+   function Buffer_Text (Lexer : in Instance; Byte_Region : in Buffer_Region) 
return String is abstract;
+   --  Return text from internal buffer, given region in byte position.
+
+   function First (Lexer : in Instance) return Boolean is abstract;
+   --  True if most recent token is first on a line.
+
+   function Find_Next
+     (Lexer : in out Instance;
+      Token :    out Base_Token)
+     return Boolean is abstract;
+   --  Set Token to the next token from the input stream.
+   --
+   --  If there is a recovered error, adds an entry to Lexer.Errors (with
+   --  Recover_Token invalid). Unrecognized characters are skipped;
+   --  missing quotes are inserted at the found quote. There can be more
+   --  than one error entry for one call to Find_Next, if several
+   --  unrecognized characters are skipped. If the recovery inserted a
+   --  missing quote, it is the last entry in Errors, the returned token
+   --  is an empty string literal, and Find_Next returns True.
+   --
+   --  If there is a non-recoverable error, raises Fatal_Error with an
+   --  appropriate message.
+   --
+   --  Otherwise returns False.
+   --
+   --  Token.Char_Region, Token.Byte_Region are the character and byte
+   --  position of the start and end of token, in the internal buffer,
+   --  1-indexed. Char_Region and Byte_Region differ when text is UTF-8
+   --  or other multi-byte encoding, and when line endings are two byte.
+   --
+   --  Token.Line is the line number in which recent token starts.
+   --  If the underlying text feeder does not support the notion of
+   --  'line', returns Invalid_Line_Number.
+   --
+   --  Token.Col is the column number of the start of the token, 1
+   --  indexed. If the underlying text feeder does not support the notion
+   --  of 'line', returns byte position in internal buffer.
+
+   function File_Name (Lexer : in Instance) return String is abstract;
+   --  Return input file name; empty string if there is no file.
+
+private
+
+   type Source_Labels is (String_Label, File_Label);
+
+   type Source (Label : Source_Labels := Source_Labels'First) is record
+      File_Name : Ada.Strings.Unbounded.Unbounded_String;
+      --  Not saved in Mapped_File, may be empty for String_Label
+
+      case Label is
+      when String_Label =>
+         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.
+
+      when File_Label =>
+
+         --  The input is memory mapped from the following, which must be 
closed:
+         File        : GNATCOLL.Mmap.Mapped_File;
+         Region      : GNATCOLL.Mmap.Mapped_Region;
+         Buffer_Last : Positive;
+      end case;
+   end record;
+
+   procedure Finalize (Object : in out Source);
+
+   function Buffer (Source : in Lexer.Source) return GNATCOLL.Mmap.Str_Access;
+   --  The bounds on the result are not present; 'First, 'Last are not
+   --  reliable. If Source_Label is String_label, actual bounds are
+   --  Source.Buffer'First, 'Last. Otherwise, actual bounds are 1 ..
+   --  Source.Buffer_Last. Indexing is reliable.
+
+   function File_Name (Source : in Lexer.Source) return String;
+
+end WisiToken.Lexer;
diff --git a/wisitoken-parse-lr-mckenzie_recover-base.adb 
b/wisitoken-parse-lr-mckenzie_recover-base.adb
new file mode 100644
index 0000000..52ddeac
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover-base.adb
@@ -0,0 +1,433 @@
+--  Abstract :
+--
+--  Base utilities for McKenzie_Recover
+--
+--  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);
+
+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;
diff --git a/wisitoken-parse-lr-mckenzie_recover-base.ads 
b/wisitoken-parse-lr-mckenzie_recover-base.ads
new file mode 100644
index 0000000..1d292d6
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover-base.ads
@@ -0,0 +1,180 @@
+--  Abstract :
+--
+--  Base utilities for McKenzie_Recover
+--
+--  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);
+
+with Ada.Exceptions;
+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 accesible 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
new file mode 100644
index 0000000..15910f7
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover-explore.adb
@@ -0,0 +1,1301 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with WisiToken.Parse.LR.McKenzie_Recover.Parse;
+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)
+   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
+      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.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;
+
+   function Do_Reduce_1
+     (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)
+     return Non_Success_Status
+   is
+      use all type SAL.Base_Peek_Type;
+      --  Perform Action on Config, setting Config.Check_Status. If that is
+      --  not Ok, call Language_Fixes (which may enqueue configs),
+      --  return Abandon. Otherwise return Continue.
+      use all type Semantic_Checks.Check_Status_Label;
+      use all type WisiToken.Parse.LR.Parser.Language_Fixes_Access;
+
+      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 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;
+
+         --  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));
+      return Continue;
+   end Do_Reduce_1;
+
+   procedure Do_Reduce_2
+     (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)
+   is
+      --  Perform reduce actions until shift Inserted_Token; 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 : Parse_Action_Node_Ptr;
+   begin
+      Next_Action := Action_For (Table, Config.Stack (1).State, Inserted_ID);
+
+      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);
+
+            when Reduce =>
+               case Do_Reduce_1 (Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Action) is
+               when Abandon =>
+                  null;
+               when Continue =>
+                  Do_Reduce_2 (Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Inserted_ID, Cost_Delta);
+               end case;
+
+            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);
+
+      when Reduce =>
+         case Do_Reduce_1 (Super, Shared, Parser_Index, Local_Config_Heap, 
Config, Next_Action.Item) is
+         when Abandon =>
+            null;
+         when Continue =>
+            Do_Reduce_2 (Super, Shared, Parser_Index, Local_Config_Heap, 
Config, Inserted_ID, Cost_Delta);
+         end case;
+
+      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;
+
+   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.EOF_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;
+      Cached_Status : Non_Success_Status;
+      --  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; we know that failed.
+                 Config.Ops (Config.Ops.Last_Index) /= (Push_Back, 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);
+                  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);
+
+                        Cached_Status := Do_Reduce_1
+                          (Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Action);
+                        Cached_Config := New_Config;
+                        Cached_Action := Action;
+
+                        if Cached_Status = Continue then
+                           Do_Reduce_2
+                             (Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, ID, Cost_Delta => 0);
+                        end if;
+                     end;
+
+                  else
+                     if Cached_Status = Continue then
+                        declare
+                           New_Config : Configuration := Cached_Config;
+                        begin
+                           Do_Reduce_2
+                             (Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, ID, Cost_Delta => 0);
+                        end;
+                     end if;
+                  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;
+
+   procedure 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)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      Table      : Parse_Table renames Shared.Table.all;
+      Descriptor : WisiToken.Descriptor renames Super.Trace.Descriptor.all;
+
+      Cost_Delta : constant Integer := -1;
+
+      type Work_Type is record
+         Config : Configuration;
+         Complete_Actions : Minimal_Action_Lists.List;
+      end record;
+
+      package Work_Queues is new SAL.Gen_Unbounded_Definite_Queues (Work_Type);
+
+      Work : Work_Queues.Queue;
+
+      function Reduce_Only (Item : in Minimal_Action_Lists.List) return 
Minimal_Action_Lists.List
+      is begin
+         return Result : Minimal_Action_Lists.List do
+            for Action of Item loop
+               if Action.Verb = Reduce then
+                  Result.Insert (Action);
+               end if;
+            end loop;
+         end return;
+      end Reduce_Only;
+
+      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;
+
+   begin
+      Work.Put ((Orig_Config, Table.States 
(Orig_Config.Stack.Peek.State).Minimal_Complete_Actions));
+      loop
+         exit when Work.Length = 0;
+         declare
+            Item : constant Work_Type := Work.Get;
+         begin
+            for Action of Item.Complete_Actions loop
+               case Action.Verb is
+               when Reduce =>
+                  --  Do a reduce, look at resulting state. Keep reducing 
until we can't
+                  --  anymore (ignoring possible shifts along the way; we are 
looking
+                  --  for the _minimal_ terminals to insert).
+                  declare
+                     use all type Ada.Containers.Count_Type;
+                     New_Config    : Configuration     := Item.Config;
+                     Reduce_Action : Reduce_Action_Rec := To_Reduce_Action 
(Action);
+
+                     Temp_Actions : Minimal_Action_Lists.List;
+                  begin
+                     loop
+                        case Do_Reduce_1 (Super, Shared, Parser_Index, 
Local_Config_Heap, New_Config, Reduce_Action) is
+                        when Abandon =>
+                           goto Abandon_Reduce;
+
+                        when Continue =>
+                           if Trace_McKenzie > Extra then
+                              Put_Line
+                                (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions reduce to" &
+                                   State_Index'Image 
(New_Config.Stack.Peek.State) & ", " &
+                                   Image (Reduce_Action.Production.LHS, 
Descriptor));
+                           end if;
+
+                           Temp_Actions := Reduce_Only
+                             (Table.States 
(New_Config.Stack.Peek.State).Minimal_Complete_Actions);
+
+                           exit when Temp_Actions.Length = 0;
+
+                           Reduce_Action := To_Reduce_Action 
(Temp_Actions.Pop);
+
+                           if Temp_Actions.Length > 0 then
+                              if Trace_McKenzie > Extra then
+                                 Put_Line
+                                   (Super.Trace.all, Super.Label 
(Parser_Index),
+                                    "Minimal_Complete_Actions add work item");
+                              end if;
+                              Work.Put ((New_Config, Temp_Actions));
+                           end if;
+                        end case;
+                     end loop;
+
+                     Insert_Minimal_Complete_Actions (Super, Shared, 
Parser_Index, New_Config, Local_Config_Heap);
+
+                     <<Abandon_Reduce>>
+                  end;
+
+               when Shift =>
+                  if Trace_McKenzie > Extra then
+                     Put_Line
+                       (Super.Trace.all, Super.Label (Parser_Index), 
"Minimal_Complete_Actions shift " &
+                          Image (Action.ID, Descriptor));
+                  end if;
+                  declare
+                     New_Config : Configuration := Item.Config;
+                  begin
+                     New_Config.Check_Status := (Label => 
WisiToken.Semantic_Checks.Ok);
+
+                     Do_Shift
+                       (Super, Shared, Parser_Index, Local_Config_Heap, 
New_Config, Action.State, Action.ID,
+                        Cost_Delta);
+                  end;
+               end case;
+            end loop;
+         end;
+      end loop;
+   end Insert_Minimal_Complete_Actions;
+
+   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)
+   is begin
+      if Use_Minimal_Complete_Actions then
+         Insert_Minimal_Complete_Actions (Super, Shared, Parser_Index, Config, 
Local_Config_Heap);
+      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;
+      use all type Lexer.Error_Lists.Cursor;
+
+      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
+         use WisiToken.Lexer;
+         use WisiToken.Lexer.Error_Lists;
+      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;
+
+         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.EOF_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;
+
+            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;
+
+      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;
+
+      function Allow_Insert_Terminal (Config : in Configuration) return Boolean
+      is
+         use all type Ada.Containers.Count_Type;
+         use all type 
WisiToken.Parse.LR.Parser.Language_Use_Minimal_Complete_Actions_Access;
+      begin
+         if Shared.Language_Use_Minimal_Complete_Actions = null then
+            return None_Since_FF (Config.Ops, Delete);
+         end if;
+
+         Use_Minimal_Complete_Actions := 
Shared.Language_Use_Minimal_Complete_Actions
+           (Current_Token_ID_Peek
+              (Shared.Terminals.all, Config.Current_Shared_Token, 
Config.Insert_Delete, Config.Current_Insert_Delete),
+            Config);
+
+         if Use_Minimal_Complete_Actions then
+            if Table.States 
(Config.Stack.Peek.State).Minimal_Complete_Actions.Length = 0 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 
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 None_Since_FF (Config.Ops, Delete) and
+        None_Since_FF (Config.Ops, Insert) and
+        Config.Stack.Depth > 1 -- can't delete the first state
+      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);
+      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;
diff --git a/wisitoken-parse-lr-mckenzie_recover-explore.ads 
b/wisitoken-parse-lr-mckenzie_recover-explore.ads
new file mode 100644
index 0000000..12e4871
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover-explore.ads
@@ -0,0 +1,28 @@
+--  Abstract :
+--
+--  Code to explore parse table, enqueuing new configs to check.
+--
+--  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);
+
+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
new file mode 100644
index 0000000..0d78fcd
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover-parse.adb
@@ -0,0 +1,302 @@
+--  Abstract :
+--
+--  See spec
+--
+--  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);
+
+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)
+         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);
+
+      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);
+
+         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;
diff --git a/wisitoken-parse-lr-mckenzie_recover-parse.ads 
b/wisitoken-parse-lr-mckenzie_recover-parse.ads
new file mode 100644
index 0000000..eb1eeee
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover-parse.ads
@@ -0,0 +1,77 @@
+--  Abstract :
+--
+--  Config parsing subprograms.
+--
+--  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);
+
+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;
diff --git a/wisitoken-parse-lr-mckenzie_recover.adb 
b/wisitoken-parse-lr-mckenzie_recover.adb
new file mode 100644
index 0000000..26d75df
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover.adb
@@ -0,0 +1,1062 @@
+--  Abstract :
+--
+--  See spec
+--
+--  Copyright (C) 2017 - 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);
+
+with Ada.Characters.Handling;
+with Ada.Exceptions;
+with Ada.Task_Identification;
+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
+
+   task type Worker_Task
+     (Super  : not null access Base.Supervisor;
+      Shared : not null access Base.Shared)
+   is
+      entry Start;
+      --  Start getting parser/configs to check from Config_Store.
+
+      entry Done;
+      --  Available when task is ready to terminate; after this rendezvous,
+      --  task discriminants may be freed.
+
+   end Worker_Task;
+
+   task body Worker_Task
+   is
+      use all type Base.Config_Status;
+      Status : Base.Config_Status;
+   begin
+      accept Start;
+
+      loop
+         Explore.Process_One (Super, Shared, Status);
+
+         exit when Status = All_Done;
+      end loop;
+
+      accept Done;
+   exception
+   when E : others =>
+      Super.Fatal (E);
+   end Worker_Task;
+
+   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;
+      use all type System.Multiprocessors.CPU_Range;
+      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 System.Multiprocessors.CPU_Range'Max (1, 
System.Multiprocessors.Number_Of_CPUs - 1)
+         --  Keep one CPU free for this main task, and the user.
+         else Shared_Parser.Table.McKenzie_Param.Task_Count);
+
+      Worker_Tasks : array (1 .. Task_Count) of Worker_Task (Super'Access, 
Shared'Access);
+
+      procedure Cleanup
+      is begin
+         for I in Worker_Tasks'Range loop
+            if Worker_Tasks (I)'Callable then
+               abort Worker_Tasks (I);
+            end if;
+         end loop;
+      end Cleanup;
+
+   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'Range loop
+         Worker_Tasks (I).Start;
+      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
+         if ID /= Null_Id then
+            Raise_Exception (ID, -Message);
+         end if;
+      end;
+
+      --  Ensure all tasks terminate before proceeding; otherwise local
+      --  variables disappear while task is still trying to access them.
+      for I in Worker_Tasks'Range loop
+         if Worker_Tasks (I)'Callable then
+            Worker_Tasks (I).Done;
+         end if;
+      end loop;
+
+      --  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;
+               begin
+                  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.
+                  --
+                  --  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;
+                              end;
+                           end loop;
+                           pragma Assert (Parser_State.Shared_Token = 
Op.Token_Index);
+                        end if;
+
+                     when Insert =>
+                        if Stack_Matches_Ops and Op.Token_Index = 
Parser_State.Shared_Token then
+                           --  This is the first Insert. Even if a later 
Push_Back supercedes it,
+                           --  we record Stack_Matches_Ops false here.
+                           Stack_Matches_Ops := False;
+
+                           if Op.Token_Index <= Min_Push_Back_Token_Index then
+                              Parser_State.Current_Token := 
Parser_State.Tree.Add_Terminal (Op.ID);
+                              Current_Token_Virtual      := True;
+                           else
+                              Sorted_Insert_Delete.Insert (Op);
+                           end if;
+                        else
+                           Sorted_Insert_Delete.Insert (Op);
+                        end if;
+
+                     when Delete =>
+                        if Stack_Matches_Ops and Op.Token_Index = 
Parser_State.Shared_Token then
+                           --  We can apply multiple deletes.
+                           Parser_State.Shared_Token := Op.Token_Index + 1;
+                           Shared_Token_Changed      := True;
+                        else
+                           Sorted_Insert_Delete.Insert (Op);
+                        end if;
+                     end case;
+                  end loop;
+
+                  --  We may not have processed the current Insert or Delete 
above, if
+                  --  they are after a fast_forward.
+                  for Op of Sorted_Insert_Delete loop
+                     if Op.Token_Index = Parser_State.Shared_Token and not 
Current_Token_Virtual then
+                        case Insert_Delete_Op_Label'(Op.Op) is
+                        when Insert =>
+                           Parser_State.Current_Token := 
Parser_State.Tree.Add_Terminal (Op.ID);
+                           Current_Token_Virtual      := True;
+
+                        when Delete =>
+                           Parser_State.Shared_Token := Op.Token_Index + 1;
+                           Shared_Token_Changed      := True;
+                        end case;
+                     else
+                        Parser_State.Recover_Insert_Delete.Put (Op);
+                     end if;
+                  end loop;
+
+                  --  If not Shared_Token_Changed, Shared_Token is the error 
token,
+                  --  which is the next token to read. If 
Shared_Token_Changed, we
+                  --  have set Shared_Token consistent with that; it is the 
next token to
+                  --  read.
+
+                  if (not Current_Token_Virtual) and Shared_Token_Changed then
+                     Parser_State.Current_Token := 
Parser_State.Tree.Add_Terminal
+                       (Parser_State.Shared_Token, Shared_Parser.Terminals);
+                  end if;
+
+                  --  Parser_State.Verb is the action that produced the 
current stack
+                  --  top. Parser_State.Inc_Shared_Token determines how to get 
the next
+                  --  token from Shared_Parser.Terminals.
+                  --
+                  --  If the stack top or Current_Token is virtual, then after 
all
+                  --  virtuals are inserted, the main parser would normally 
increment
+                  --  Parser_State.Shared_Token to get the next token. 
However, we have
+                  --  set Shared_Token to the next token, so we don't want it 
to
+                  --  increment. We could set Shared_Token to 1 less, but this 
way the
+                  --  debug messages all show the expected Shared_Terminal.
+
+                  if Parser_State.Stack (1).Token = 
Syntax_Trees.Invalid_Node_Index then
+                     --  a virtual token from a previous recover
+                     Parser_State.Set_Verb (Shift_Recover);
+                     Parser_State.Inc_Shared_Token := False;
+                  else
+                     case Tree.Label (Parser_State.Stack (1).Token) is
+                     when Syntax_Trees.Shared_Terminal =>
+                        Parser_State.Set_Verb (Shift_Recover);
+                        Parser_State.Inc_Shared_Token := not 
Current_Token_Virtual;
+
+                     when Syntax_Trees.Virtual_Terminal =>
+                        Parser_State.Set_Verb (Shift_Recover);
+                        Parser_State.Inc_Shared_Token := False;
+
+                     when Syntax_Trees.Nonterm =>
+                        Parser_State.Set_Verb (Reduce);
+                        Parser_State.Inc_Shared_Token := not 
Current_Token_Virtual;
+                     end case;
+                  end if;
+
+                  if Trace_McKenzie > Extra then
+                     Put_Line (Trace, Parser_State.Label, "after Ops 
applied:", Task_ID => False);
+                     Put_Line
+                       (Trace, Parser_State.Label, "stack " & 
Parser_Lists.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);
+                     Put_Line
+                       (Trace, Parser_State.Label, "recover_insert_delete " & 
Image
+                          (Parser_State.Recover_Insert_Delete, Descriptor), 
Task_ID => False);
+                     Put_Line
+                       (Trace, Parser_State.Label, "inc_shared_token " & 
Boolean'Image (Parser_State.Inc_Shared_Token) &
+                          " parser verb " & All_Parse_Action_Verbs'Image 
(Parser_State.Verb),
+                        Task_ID => False);
+
+                  elsif Trace_McKenzie > Outline then
+                     Put_Line
+                       (Trace, Parser_State.Label, "inc_shared_token " & 
Boolean'Image (Parser_State.Inc_Shared_Token) &
+                          " parser verb " & All_Parse_Action_Verbs'Image 
(Parser_State.Verb),
+                        Task_ID => False);
+                  end if;
+               end;
+            exception
+            when Bad_Config =>
+               if Parsers.Count = 1 then
+                  --  Oops. just give up
+                  return Fail_Programmer_Error;
+               end if;
+               Parsers.Terminate_Parser (Current_Parser, "bad config in 
recover", Trace);
+            end;
+         end if;
+         Current_Parser.Next;
+      end loop;
+
+      if Shared_Parser.Post_Recover /= null then
+         Shared_Parser.Post_Recover.all;
+      end if;
+
+      return Super.Recover_Result;
+
+   exception
+   when others =>
+      Cleanup;
+      return Fail_Programmer_Error;
+   end Recover;
+
+   ----------
+   --  Spec private subprograms; for language-specific
+   --  child packages.
+
+   procedure Check (ID : Token_ID; Expected_ID : in Token_ID)
+   is begin
+      pragma Assert (ID = Expected_ID, Token_ID'Image (ID) & " /=" & 
Token_ID'Image (Expected_ID));
+   end Check;
+
+   function Current_Token
+     (Terminals                 : in     Base_Token_Arrays.Vector;
+      Terminals_Current         : in out WisiToken.Base_Token_Index;
+      Restore_Terminals_Current :    out WisiToken.Base_Token_Index;
+      Insert_Delete             : in out Sorted_Insert_Delete_Arrays.Vector;
+      Current_Insert_Delete     : in out SAL.Base_Peek_Type)
+     return Base_Token
+   is
+      use all type SAL.Base_Peek_Type;
+
+      procedure Inc_I_D
+      is begin
+         Current_Insert_Delete := Current_Insert_Delete + 1;
+         if Current_Insert_Delete > Insert_Delete.Last_Index then
+            Current_Insert_Delete := No_Insert_Delete;
+            Insert_Delete.Clear;
+         end if;
+      end Inc_I_D;
+
+   begin
+      if Terminals_Current = Base_Token_Index'First then
+         --  Happens with really bad syntax; see test_mckenzie_recover.adb 
Error_4.
+         raise Bad_Config;
+      end if;
+
+      loop
+         if Current_Insert_Delete = No_Insert_Delete then
+            Restore_Terminals_Current := Terminals_Current;
+            return Terminals (Terminals_Current);
+
+         elsif Insert_Delete (Current_Insert_Delete).Token_Index = 
Terminals_Current then
+            declare
+               Op : Insert_Delete_Op renames Insert_Delete 
(Current_Insert_Delete);
+            begin
+               case Insert_Delete_Op_Label (Op.Op) is
+               when Insert =>
+                  --  Decrement Terminals_Current so Next_Token knows it 
should always
+                  --  increment it. Save the initial value, to restore in case 
of error.
+                  Restore_Terminals_Current := Terminals_Current;
+                  Terminals_Current         := Terminals_Current - 1;
+                  return (ID => Op.ID, others => <>);
+
+               when Delete =>
+                  Terminals_Current         := Terminals_Current + 1;
+                  Restore_Terminals_Current := Terminals_Current;
+                  Inc_I_D;
+               end case;
+            end;
+         else
+            return Terminals (Terminals_Current);
+         end if;
+      end loop;
+   end Current_Token;
+
+   function Current_Token_ID_Peek
+     (Terminals             : in Base_Token_Arrays.Vector;
+      Terminals_Current     : in Base_Token_Index;
+      Insert_Delete         : in Sorted_Insert_Delete_Arrays.Vector;
+      Current_Insert_Delete : in SAL.Base_Peek_Type)
+     return Token_ID
+   is
+      use all type SAL.Base_Peek_Type;
+   begin
+      if Terminals_Current = Base_Token_Index'First then
+         --  Happens with really bad syntax; see test_mckenzie_recover.adb 
Error_4.
+         raise Bad_Config;
+      end if;
+
+      if Current_Insert_Delete = No_Insert_Delete then
+         return Terminals (Terminals_Current).ID;
+
+      elsif Insert_Delete (Current_Insert_Delete).Token_Index = 
Terminals_Current then
+         declare
+            Op : Insert_Delete_Op renames Insert_Delete 
(Current_Insert_Delete);
+         begin
+            case Insert_Delete_Op_Label (Op.Op) is
+            when Insert =>
+               return Op.ID;
+
+            when Delete =>
+               --  This should have been handled in Check
+               raise SAL.Programmer_Error;
+            end case;
+         end;
+      else
+         return Terminals (Terminals_Current).ID;
+      end if;
+   end Current_Token_ID_Peek;
+
+   procedure Delete (Config : in out Configuration; ID : in Token_ID)
+   is
+      Op : constant Config_Op := (Delete, ID, Config.Current_Shared_Token);
+   begin
+      Config.Ops.Append (Op);
+      Config.Insert_Delete.Insert (Op);
+      Config.Current_Insert_Delete := 1;
+   exception
+   when SAL.Container_Full =>
+      raise Bad_Config;
+   end Delete;
+
+   procedure Find_ID
+     (Config         : in     Configuration;
+      ID             : in     Token_ID;
+      Matching_Index : in out SAL.Peek_Type)
+   is
+      use all type SAL.Peek_Type;
+   begin
+      loop
+         exit when Matching_Index = Config.Stack.Depth; -- Depth has 
Invalid_Token_ID
+         declare
+            Stack_ID : Token_ID renames Config.Stack (Matching_Index).Token.ID;
+         begin
+            exit when Stack_ID = ID;
+         end;
+         Matching_Index := Matching_Index + 1;
+      end loop;
+   end Find_ID;
+
+   procedure Find_ID
+     (Config         : in     Configuration;
+      IDs            : in     Token_ID_Set;
+      Matching_Index : in out SAL.Peek_Type)
+   is
+      use all type SAL.Peek_Type;
+   begin
+      loop
+         exit when Matching_Index = Config.Stack.Depth; -- Depth has 
Invalid_Token_ID
+         declare
+            ID : Token_ID renames Config.Stack (Matching_Index).Token.ID;
+         begin
+            exit when ID in IDs'First .. IDs'Last and then IDs (ID);
+         end;
+         Matching_Index := Matching_Index + 1;
+      end loop;
+   end Find_ID;
+
+   procedure Find_Descendant_ID
+     (Tree           : in     Syntax_Trees.Tree;
+      Config         : in     Configuration;
+      ID             : in     Token_ID;
+      ID_Set         : in     Token_ID_Set;
+      Matching_Index : in out SAL.Peek_Type)
+   is
+      use Syntax_Trees;
+      use all type SAL.Peek_Type;
+   begin
+      loop
+         exit when Matching_Index = Config.Stack.Depth; -- Depth has 
Invalid_Token_ID
+         exit when ID_Set (Config.Stack (Matching_Index).Token.ID) and
+           (Config.Stack (Matching_Index).Tree_Index /= Invalid_Node_Index and 
then
+              Tree.Find_Descendant (Config.Stack (Matching_Index).Tree_Index, 
ID) /= Invalid_Node_Index);
+
+         Matching_Index := Matching_Index + 1;
+      end loop;
+   end Find_Descendant_ID;
+
+   procedure Find_Matching_Name
+     (Config              : in     Configuration;
+      Lexer               : access constant WisiToken.Lexer.Instance'Class;
+      Name                : in     String;
+      Matching_Name_Index : in out SAL.Peek_Type;
+      Case_Insensitive    : in     Boolean)
+   is
+      use Ada.Characters.Handling;
+      use all type SAL.Peek_Type;
+      Match_Name : constant String := (if Case_Insensitive then To_Lower 
(Name) else Name);
+   begin
+      loop
+         exit when Matching_Name_Index = Config.Stack.Depth; -- Depth has 
Invalid_Token_ID
+         declare
+            Token       : Recover_Token renames Config.Stack 
(Matching_Name_Index).Token;
+            Name_Region : constant Buffer_Region :=
+              (if Token.Name = Null_Buffer_Region
+               then Token.Byte_Region
+               else Token.Name);
+         begin
+            exit when Name_Region /= Null_Buffer_Region and then
+              Match_Name =
+              (if Case_Insensitive
+               then To_Lower (Lexer.Buffer_Text (Name_Region))
+               else Lexer.Buffer_Text (Name_Region));
+
+            Matching_Name_Index := Matching_Name_Index + 1;
+         end;
+      end loop;
+   end Find_Matching_Name;
+
+   procedure Find_Matching_Name
+     (Config              : in     Configuration;
+      Lexer               : access constant WisiToken.Lexer.Instance'Class;
+      Name                : in     String;
+      Matching_Name_Index : in out SAL.Peek_Type;
+      Other_ID            : in     Token_ID;
+      Other_Count         :    out Integer;
+      Case_Insensitive    : in     Boolean)
+   is
+      use Ada.Characters.Handling;
+      use all type SAL.Peek_Type;
+      Match_Name : constant String := (if Case_Insensitive then To_Lower 
(Name) else Name);
+   begin
+      Other_Count := 0;
+
+      loop
+         exit when Matching_Name_Index = Config.Stack.Depth; -- Depth has 
Invalid_Token_ID
+         declare
+            Token       : Recover_Token renames Config.Stack 
(Matching_Name_Index).Token;
+            Name_Region : constant Buffer_Region :=
+              (if Token.Name = Null_Buffer_Region
+               then Token.Byte_Region
+               else Token.Name);
+         begin
+            exit when Name_Region /= Null_Buffer_Region and then
+              Match_Name =
+              (if Case_Insensitive
+               then To_Lower (Lexer.Buffer_Text (Name_Region))
+               else Lexer.Buffer_Text (Name_Region));
+
+            if Other_ID = Token.ID then
+               Other_Count := Other_Count + 1;
+            end if;
+
+            Matching_Name_Index := Matching_Name_Index + 1;
+         end;
+      end loop;
+   end Find_Matching_Name;
+
+   procedure Insert (Config : in out Configuration; ID : in Token_ID)
+   is
+      Op : constant Config_Op := (Insert, ID, Config.Current_Shared_Token);
+   begin
+      Config.Ops.Append (Op);
+      Config.Insert_Delete.Insert (Op);
+      Config.Current_Insert_Delete := 1;
+   exception
+   when SAL.Container_Full =>
+      raise Bad_Config;
+   end Insert;
+
+   procedure Insert (Config : in out Configuration; IDs : in Token_ID_Array)
+   is begin
+      for ID of IDs loop
+         Insert (Config, ID);
+      end loop;
+   end Insert;
+
+   function Next_Token
+     (Terminals                 : in     Base_Token_Arrays.Vector;
+      Terminals_Current         : in out Base_Token_Index;
+      Restore_Terminals_Current : in out WisiToken.Base_Token_Index;
+      Insert_Delete             : in out Sorted_Insert_Delete_Arrays.Vector;
+      Current_Insert_Delete     : in out SAL.Base_Peek_Type)
+     return Base_Token
+   is
+      use all type SAL.Base_Peek_Type;
+   begin
+      loop
+         if Insert_Delete.Last_Index > 0 and then Current_Insert_Delete = 
Insert_Delete.Last_Index then
+            Current_Insert_Delete     := No_Insert_Delete;
+            Insert_Delete.Clear;
+            Terminals_Current         := Terminals_Current + 1;
+            Restore_Terminals_Current := Terminals_Current;
+            return Terminals (Terminals_Current);
+
+         elsif Current_Insert_Delete = No_Insert_Delete then
+            Terminals_Current         := Terminals_Current + 1;
+            Restore_Terminals_Current := Terminals_Current;
+            return Terminals (Terminals_Current);
+
+         elsif Insert_Delete (Current_Insert_Delete + 1).Token_Index = 
Terminals_Current + 1 then
+            Current_Insert_Delete := Current_Insert_Delete + 1;
+            declare
+               Op : constant Insert_Delete_Op := Insert_Delete 
(Current_Insert_Delete);
+            begin
+               case Insert_Delete_Op_Label'(Op.Op) is
+               when Insert =>
+                  return (ID => Op.ID, others => <>);
+
+               when Delete =>
+                  Terminals_Current         := Terminals_Current + 1;
+                  Restore_Terminals_Current := Terminals_Current;
+               end case;
+            end;
+
+         else
+            Terminals_Current         := Terminals_Current + 1;
+            Restore_Terminals_Current := Terminals_Current;
+            return Terminals (Terminals_Current);
+         end if;
+      end loop;
+   end Next_Token;
+
+   procedure Push_Back (Config : in out Configuration)
+   is
+      Item        : constant Recover_Stack_Item := Config.Stack.Pop;
+      Token_Index : constant Base_Token_Index   := 
Item.Token.Min_Terminal_Index;
+
+      function Compare (Left : in Base_Token_Index; Right : in Config_Op) 
return Boolean
+        is (case Right.Op is
+            when Fast_Forward    => False,
+            when Undo_Reduce     => False,
+            when Push_Back       => False,
+            when Insert | Delete => Left < Right.Token_Index);
+      --  If Left = Right.Token_Index, we assume the Right ops go _after_
+      --  the Left, so the Left do not need to be repeated.
+   begin
+      if Token_Index /= Invalid_Token_Index then
+         Config.Current_Shared_Token := Token_Index;
+         for I in Config.Ops.First_Index .. Config.Ops.Last_Index loop
+            if Compare (Token_Index, Config.Ops (I)) then
+               Config.Insert_Delete.Insert (Config.Ops (I));
+            end if;
+         end loop;
+      end if;
+
+      Config.Ops.Append ((Push_Back, Item.Token.ID, 
Config.Current_Shared_Token));
+   exception
+   when SAL.Container_Full =>
+      raise Bad_Config;
+   end Push_Back;
+
+   procedure Push_Back_Check (Config : in out Configuration; Expected_ID : in 
Token_ID)
+   is begin
+      Check (Config.Stack (1).Token.ID, Expected_ID);
+      Push_Back (Config);
+   end Push_Back_Check;
+
+   procedure Push_Back_Check (Config : in out Configuration; Expected : in 
Token_ID_Array)
+   is begin
+      for ID of Expected loop
+         Push_Back_Check (Config, ID);
+      end loop;
+   end Push_Back_Check;
+
+   procedure Put
+     (Message      : in     String;
+      Trace        : in out WisiToken.Trace'Class;
+      Parser_Label : in     Natural;
+      Terminals    : in     Base_Token_Arrays.Vector;
+      Config       : in     Configuration;
+      Task_ID      : in     Boolean := True)
+   is
+      --  For debugging output
+
+      --  Build a string, call trace.put_line once, so output from multiple
+      --  tasks is not interleaved (mostly).
+      use all type Ada.Strings.Unbounded.Unbounded_String;
+      use all type SAL.Base_Peek_Type;
+      use all type WisiToken.Semantic_Checks.Check_Status_Label;
+
+      Descriptor : WisiToken.Descriptor renames Trace.Descriptor.all;
+
+      Result : Ada.Strings.Unbounded.Unbounded_String :=
+        (if Task_ID then +Ada.Task_Identification.Image 
(Ada.Task_Identification.Current_Task) else +"") &
+        Integer'Image (Parser_Label) & ": " &
+        (if Message'Length > 0 then Message & ":" else "");
+   begin
+      Result := Result & Natural'Image (Config.Cost) & ", ";
+      if Config.Check_Status.Label /= Ok then
+         Result := Result & Semantic_Checks.Check_Status_Label'Image 
(Config.Check_Status.Label) & " ";
+      elsif Config.Error_Token.ID /= Invalid_Token_ID then
+         Result := Result & "Error " & Image (Config.Error_Token, Descriptor) 
& " ";
+      end if;
+      Result := Result & Image (Config.Stack, Descriptor, Depth => 1);
+
+      if Config.Current_Insert_Delete = No_Insert_Delete then
+         Result := Result & "|" & Image (Config.Current_Shared_Token, 
Terminals, Descriptor) & "|";
+      else
+         Result := Result & "/" & Trimmed_Image (Config.Current_Insert_Delete) 
& ":" &
+           Image (Config.Insert_Delete (Config.Current_Insert_Delete), 
Descriptor) & "/";
+      end if;
+
+      Result := Result & Image (Config.Ops, Descriptor);
+      Trace.Put_Line (-Result);
+   end Put;
+
+   procedure Put_Line
+     (Trace        : in out WisiToken.Trace'Class;
+      Parser_Label : in     Natural;
+      Message      : in     String;
+      Task_ID      : in     Boolean := True)
+   is begin
+      Trace.Put_Line
+        ((if Task_ID then Ada.Task_Identification.Image 
(Ada.Task_Identification.Current_Task) else "") &
+           Integer'Image (Parser_Label) & ": " & Message);
+   end Put_Line;
+
+   function Undo_Reduce
+     (Stack : in out Recover_Stacks.Stack;
+      Tree  : in     Syntax_Trees.Tree)
+     return Ada.Containers.Count_Type
+   is
+      Nonterm_Item : constant Recover_Stack_Item                  := Stack.Pop;
+      Children     : constant Syntax_Trees.Valid_Node_Index_Array := 
Tree.Children (Nonterm_Item.Tree_Index);
+   begin
+      for C of Children loop
+         Stack.Push ((Tree.State (C), C, Tree.Recover_Token (C)));
+      end loop;
+      return Children'Length;
+   end Undo_Reduce;
+
+   procedure Undo_Reduce_Check
+     (Config   : in out Configuration;
+      Tree     : in     Syntax_Trees.Tree;
+      Expected : in     Token_ID)
+   is begin
+      Check (Config.Stack (1).Token.ID, Expected);
+      Config.Ops.Append ((Undo_Reduce, Expected, Undo_Reduce (Config.Stack, 
Tree)));
+   exception
+   when SAL.Container_Full =>
+      raise Bad_Config;
+   end Undo_Reduce_Check;
+
+   procedure Undo_Reduce_Check
+     (Config   : in out Configuration;
+      Tree     : in     Syntax_Trees.Tree;
+      Expected : in     Token_ID_Array)
+   is begin
+      for ID of Expected loop
+         Undo_Reduce_Check (Config, Tree, ID);
+      end loop;
+   end Undo_Reduce_Check;
+
+end WisiToken.Parse.LR.McKenzie_Recover;
diff --git a/wisitoken-parse-lr-mckenzie_recover.ads 
b/wisitoken-parse-lr-mckenzie_recover.ads
new file mode 100644
index 0000000..533ed87
--- /dev/null
+++ b/wisitoken-parse-lr-mckenzie_recover.ads
@@ -0,0 +1,220 @@
+--  Abstract :
+--
+--  Implement [McKenzie] error recovery, extended to parallel parsers.
+--
+--  References:
+--
+--  [McKenzie] McKenzie, Bruce J., Yeatman, Corey, and De Vere,
+--  Lorraine. Error repair in shift reduce parsers. ACM Trans. Prog.
+--  Lang. Syst., 17(4):672-689, July 1995.  Described in [Grune 2008] ref 321.
+--
+--  [Grune 2008] Parsing Techniques, A Practical Guide, Second
+--  Edition. Dick Grune, Ceriel J.H. Jacobs.
+--
+--  Copyright (C) 2017, 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);
+
+with WisiToken.Parse.LR.Parser;
+with WisiToken.Lexer;
+package WisiToken.Parse.LR.McKenzie_Recover is
+
+   Bad_Config : exception;
+   --  Raised when a config is determined to violate some programming
+   --  convention; abandon it.
+
+   type Recover_Status is
+     (Fail_Check_Delta, Fail_Enqueue_Limit, Fail_Cost, Fail_No_Configs_Left, 
Fail_Programmer_Error,
+      Success);
+
+   function Recover (Shared_Parser : in out WisiToken.Parse.LR.Parser.Parser) 
return Recover_Status;
+   --  Attempt to modify Parser.Parsers state and Parser.Lookahead to
+   --  allow recovering from an error state.
+
+   Force_Full_Explore        : Boolean := False;
+   --  Sometimes recover throws an exception in a race condition case
+   --  that is hard to reproduce. Setting this True ignores all Success,
+   --  so all configs are checked.
+
+   Force_High_Cost_Solutions : Boolean := False;
+   --  Similarly, setting this true keeps all solutions that are found,
+   --  and forces at least three.
+
+private
+
+   ----------
+   --  Visible for language-specific child packages. Alphabetical.
+
+   procedure Check (ID : Token_ID; Expected_ID : in Token_ID)
+   with Inline => True;
+   --  Check that ID = Expected_ID; raise Assertion_Error if not.
+   --  Implemented using 'pragma Assert'.
+
+   function Current_Token
+     (Terminals                 : in     Base_Token_Arrays.Vector;
+      Terminals_Current         : in out Base_Token_Index;
+      Restore_Terminals_Current :    out WisiToken.Base_Token_Index;
+      Insert_Delete             : in out Sorted_Insert_Delete_Arrays.Vector;
+      Current_Insert_Delete     : in out SAL.Base_Peek_Type)
+     return Base_Token;
+   --  Return the current token, from either Terminals or Insert_Delete;
+   --  set up for Next_Token.
+   --
+   --  See Next_Token for more info.
+
+   function Current_Token_ID_Peek
+     (Terminals             : in Base_Token_Arrays.Vector;
+      Terminals_Current     : in Base_Token_Index;
+      Insert_Delete         : in Sorted_Insert_Delete_Arrays.Vector;
+      Current_Insert_Delete : in SAL.Base_Peek_Type)
+     return Token_ID;
+   --  Return the current token, from either Terminals or Insert_Delete,
+   --  without setting up for Next_Token.
+
+   procedure Delete (Config : in out Configuration; ID : in Token_ID);
+   --  Append a Delete op to Config.Ops, and insert it in
+   --  Config.Insert_Deleted in token_index order.
+
+   procedure Find_ID
+     (Config         : in     Configuration;
+      ID             : in     Token_ID;
+      Matching_Index : in out SAL.Peek_Type);
+   --  Search Config.Stack for a token with ID, starting at
+   --  Matching_Index. If found, Matching_Index points to it.
+   --  If not found, Matching_Index = Config.Stack.Depth.
+
+   procedure Find_ID
+     (Config         : in     Configuration;
+      IDs            : in     Token_ID_Set;
+      Matching_Index : in out SAL.Peek_Type);
+   --  Search Config.Stack for a token with ID in IDs, starting at
+   --  Matching_Index. If found, Matching_Index points to it.
+   --  If not found, Matching_Index = Config.Stack.Depth.
+
+   procedure Find_Descendant_ID
+     (Tree           : in     Syntax_Trees.Tree;
+      Config         : in     Configuration;
+      ID             : in     Token_ID;
+      ID_Set         : in     Token_ID_Set;
+      Matching_Index : in out SAL.Peek_Type);
+   --  Search Config.Stack for a token with id in ID_Set, with a
+   --  descendant with id = ID, starting at Matching_Index. If found,
+   --  Matching_Index points to it. If not found, Matching_Index =
+   --  Config.Stack.Depth.
+
+   procedure Find_Matching_Name
+     (Config              : in     Configuration;
+      Lexer               : access constant WisiToken.Lexer.Instance'Class;
+      Name                : in     String;
+      Matching_Name_Index : in out SAL.Peek_Type;
+      Case_Insensitive    : in     Boolean);
+   --  Search Config.Stack for a token matching Name, starting at
+   --  Matching_Name_Index. If found, Matching_Name_Index points to it.
+   --  If not found, Matching_Name_Index = Config.Stack.Depth.
+
+   procedure Find_Matching_Name
+     (Config              : in     Configuration;
+      Lexer               : access constant WisiToken.Lexer.Instance'Class;
+      Name                : in     String;
+      Matching_Name_Index : in out SAL.Peek_Type;
+      Other_ID            : in     Token_ID;
+      Other_Count         :    out Integer;
+      Case_Insensitive    : in     Boolean);
+   --  Search Config.Stack for a token matching Name, starting at
+   --  Matching_Name_Index. If found, Matching_Name_Index points to it.
+   --  If not found, Matching_Name_Index = Config.Stack.Depth.
+   --
+   --  Also count tokens with ID = Other_ID.
+
+   procedure Insert (Config : in out Configuration; ID : in Token_ID);
+   --  Append an Insert op to Config.Ops, and insert it in
+   --  Config.Insert_Deleted in token_index order.
+
+   procedure Insert (Config : in out Configuration; IDs : in Token_ID_Array);
+   --  Call Insert for each item in IDs.
+
+   function Next_Token
+     (Terminals                 : in     Base_Token_Arrays.Vector;
+      Terminals_Current         : in out Base_Token_Index;
+      Restore_Terminals_Current : in out WisiToken.Base_Token_Index;
+      Insert_Delete             : in out Sorted_Insert_Delete_Arrays.Vector;
+      Current_Insert_Delete     : in out SAL.Base_Peek_Type)
+     return Base_Token;
+   --  Return the next token, from either Terminals or Insert_Delete;
+   --  update Terminals_Current or Current_Insert_Delete.
+   --
+   --  If result is Insert_Delete.Last_Index, Current_Insert_Delete =
+   --  Last_Index; Insert_Delete is cleared and Current_Insert_Delete
+   --  reset on next call.
+   --
+   --  When done parsing, reset actual Terminals_Current to
+   --  Restore_Terminals_Current.
+   --
+   --  Insert_Delete contains only Insert and Delete ops, in token_index
+   --  order. Those ops are applied when Terminals_Current =
+   --  op.token_index.
+
+   procedure Push_Back (Config : in out Configuration);
+   --  Pop the top Config.Stack item, set Config.Current_Shared_Token to
+   --  the first terminal in that item. If the item is empty,
+   --  Config.Current_Shared_Token is unchanged.
+   --
+   --  If any earlier Insert or Delete items in Config.Ops are for a
+   --  token_index after that first terminal, they are added to
+   --  Config.Insert_Delete in token_index order.
+
+   procedure Push_Back_Check (Config : in out Configuration; Expected_ID : in 
Token_ID);
+   --  In effect, call Check and Push_Back.
+
+   procedure Push_Back_Check (Config : in out Configuration; Expected : in 
Token_ID_Array);
+   --  Call Push_Back_Check for each item in Expected.
+
+   procedure Put
+     (Message      : in     String;
+      Trace        : in out WisiToken.Trace'Class;
+      Parser_Label : in     Natural;
+      Terminals    : in     Base_Token_Arrays.Vector;
+      Config       : in     Configuration;
+      Task_ID      : in     Boolean := True);
+   --  Put Message and an image of Config to Trace.
+
+   procedure Put_Line
+     (Trace        : in out WisiToken.Trace'Class;
+      Parser_Label : in     Natural;
+      Message      : in     String;
+      Task_ID      : in     Boolean := True);
+   --  Put message to Trace, with parser and task info.
+
+   function Undo_Reduce
+     (Stack : in out Recover_Stacks.Stack;
+      Tree  : in     Syntax_Trees.Tree)
+     return Ada.Containers.Count_Type
+   with Pre => Tree.Is_Nonterm (Stack (1).Tree_Index);
+   --  Undo the reduction that produced the top stack item, return the
+   --  token count for that reduction.
+
+   procedure Undo_Reduce_Check
+     (Config   : in out Configuration;
+      Tree     : in     Syntax_Trees.Tree;
+      Expected : in     Token_ID)
+   with Inline => True;
+   --  Call Check, Undo_Reduce.
+
+   procedure Undo_Reduce_Check
+     (Config   : in out Configuration;
+      Tree     : in     Syntax_Trees.Tree;
+      Expected : in     Token_ID_Array);
+   --  Call Undo_Reduce_Check for each item in Expected.
+
+end WisiToken.Parse.LR.McKenzie_Recover;
diff --git a/wisitoken-parse-lr-parser.adb b/wisitoken-parse-lr-parser.adb
new file mode 100644
index 0000000..5d3eedb
--- /dev/null
+++ b/wisitoken-parse-lr-parser.adb
@@ -0,0 +1,1105 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2002 - 2005, 2008 - 2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with Ada.Calendar.Formatting;
+with Ada.Exceptions;
+with WisiToken.Parse.LR.McKenzie_Recover;
+package body WisiToken.Parse.LR.Parser is
+
+   function Reduce_Stack_1
+     (Current_Parser : in     Parser_Lists.Cursor;
+      Action         : in     Reduce_Action_Rec;
+      Nonterm        :    out WisiToken.Syntax_Trees.Valid_Node_Index;
+      Lexer          : in     WisiToken.Lexer.Handle;
+      Trace          : in out WisiToken.Trace'Class)
+     return WisiToken.Semantic_Checks.Check_Status_Label
+   is
+      --  We treat semantic check errors as parse errors here, to allow
+      --  error recovery to take better advantage of them. One recovery
+      --  strategy is to fix things so the semantic check passes.
+
+      use all type SAL.Base_Peek_Type;
+      use all type Semantic_Checks.Check_Status_Label;
+      use all type Semantic_Checks.Semantic_Check;
+
+      Parser_State  : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref.Element.all;
+      Children_Tree : Syntax_Trees.Valid_Node_Index_Array (1 .. 
SAL.Base_Peek_Type (Action.Token_Count));
+   begin
+      for I in reverse Children_Tree'Range loop
+         Children_Tree (I) := Parser_State.Stack.Pop.Token;
+      end loop;
+
+      Nonterm := Parser_State.Tree.Add_Nonterm
+        (Action.Production, Children_Tree, Action.Action,
+         Default_Virtual => Parser_State.Tree.Is_Virtual 
(Parser_State.Current_Token));
+      --  Computes Nonterm.Byte_Region, Virtual
+
+      if Trace_Parse > Detail then
+         Trace.Put_Line (Parser_State.Tree.Image (Nonterm, 
Trace.Descriptor.all, Include_Children => True));
+      end if;
+
+      if Action.Check = null then
+         return Ok;
+
+      else
+         declare
+            Nonterm_Token  : Recover_Token := Parser_State.Tree.Recover_Token 
(Nonterm);
+            Children_Token : constant Recover_Token_Array := 
Parser_State.Tree.Recover_Token_Array (Children_Tree);
+            Status : constant Semantic_Checks.Check_Status := Action.Check
+              (Lexer, Nonterm_Token, Children_Token);
+         begin
+            Parser_State.Tree.Set_Name_Region (Nonterm, Nonterm_Token.Name);
+
+            if Trace_Parse > Detail then
+               Trace.Put_Line ("semantic check " & Semantic_Checks.Image 
(Status, Trace.Descriptor.all));
+            end if;
+
+            case Status.Label is
+            when Ok =>
+               return Ok;
+
+            when Semantic_Checks.Error =>
+               if Parser_State.Resume_Active then
+                  --  Ignore this error; that's how McKenzie_Recover decided 
to fix it
+                  return Ok;
+
+               else
+                  Parser_State.Errors.Append
+                    ((Label          => Check,
+                      First_Terminal => Trace.Descriptor.First_Terminal,
+                      Last_Terminal  => Trace.Descriptor.Last_Terminal,
+                      Check_Status   => Status,
+                      Recover        => (others => <>)));
+                  return Status.Label;
+               end if;
+            end case;
+         end;
+      end if;
+   end Reduce_Stack_1;
+
+   procedure Do_Action
+     (Action         : in Parse_Action_Rec;
+      Current_Parser : in Parser_Lists.Cursor;
+      Shared_Parser  : in LR.Parser.Parser)
+   is
+      use all type Semantic_Checks.Check_Status_Label;
+
+      Parser_State : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref;
+      Trace        : WisiToken.Trace'Class renames Shared_Parser.Trace.all;
+      Nonterm      : WisiToken.Syntax_Trees.Valid_Node_Index;
+      Status       : Semantic_Checks.Check_Status_Label;
+   begin
+      if Trace_Parse > Detail then
+         Trace.Put
+           (Integer'Image (Current_Parser.Label) & ": " &
+              Trimmed_Image (Parser_State.Stack.Peek.State) & ": " &
+              Parser_State.Tree.Image (Parser_State.Current_Token, 
Trace.Descriptor.all) & " : ");
+         Put (Trace, Action);
+         Trace.New_Line;
+      end if;
+
+      case Action.Verb is
+      when Shift =>
+         Current_Parser.Set_Verb (Shift);
+         Parser_State.Stack.Push ((Action.State, Parser_State.Current_Token));
+         Parser_State.Tree.Set_State (Parser_State.Current_Token, 
Action.State);
+
+      when Reduce =>
+         Status := Reduce_Stack_1 (Current_Parser, Action, Nonterm, 
Shared_Parser.Lexer, Trace);
+
+         --  Even when Reduce_Stack_1 returns Error, it did reduce the stack, 
so
+         --  push Nonterm.
+         Parser_State.Stack.Push
+           ((State    => Goto_For
+               (Table => Shared_Parser.Table.all,
+                State => Parser_State.Stack (1).State,
+                ID    => Action.Production.LHS),
+             Token    => Nonterm));
+
+         Parser_State.Tree.Set_State (Nonterm, Parser_State.Stack (1).State);
+
+         case Status is
+         when Ok =>
+            Current_Parser.Set_Verb (Reduce);
+
+            if Trace_Parse > Detail then
+               Trace.Put_Line (" ... goto state " & Trimmed_Image 
(Parser_State.Stack.Peek.State));
+            end if;
+
+         when Semantic_Checks.Error =>
+            Current_Parser.Set_Verb (Error);
+            Parser_State.Zombie_Token_Count := 1;
+         end case;
+
+      when Accept_It =>
+         case Reduce_Stack_1
+           (Current_Parser,
+            (Reduce, Action.Production, Action.Action, Action.Check, 
Action.Token_Count),
+            Nonterm, Shared_Parser.Lexer, Trace)
+         is
+         when Ok =>
+            Current_Parser.Set_Verb (Action.Verb);
+
+            Parser_State.Tree.Set_Root (Nonterm);
+
+         when Semantic_Checks.Error =>
+            Current_Parser.Set_Verb (Error);
+            Parser_State.Zombie_Token_Count := 1;
+         end case;
+
+      when Error =>
+         Current_Parser.Set_Verb (Action.Verb);
+
+         Parser_State.Zombie_Token_Count := 1;
+
+         declare
+            Expecting : constant Token_ID_Set := LR.Expecting
+              (Shared_Parser.Table.all, 
Current_Parser.State_Ref.Stack.Peek.State);
+         begin
+            Parser_State.Errors.Append
+              ((Label          => LR.Action,
+                First_Terminal => Trace.Descriptor.First_Terminal,
+                Last_Terminal  => Trace.Descriptor.Last_Terminal,
+                Error_Token    => Parser_State.Current_Token,
+                Expecting      => Expecting,
+                Recover        => (others => <>)));
+
+            if Trace_Parse > Outline then
+               Put
+                 (Trace,
+                  Integer'Image (Current_Parser.Label) & ": expecting: " &
+                    Image (Expecting, Trace.Descriptor.all));
+               Trace.New_Line;
+            end if;
+         end;
+      end case;
+   end Do_Action;
+
+   procedure Do_Deletes
+     (Shared_Parser : in out LR.Parser.Parser;
+      Parser_State  : in out Parser_Lists.Parser_State)
+   is
+      use all type SAL.Base_Peek_Type;
+   begin
+      if Trace_Parse > Extra then
+         Shared_Parser.Trace.Put_Line
+           (Integer'Image (Parser_State.Label) & ": recover_insert_delete: " &
+              Image (Parser_State.Recover_Insert_Delete, 
Shared_Parser.Trace.Descriptor.all));
+      end if;
+
+      loop
+         if Parser_State.Recover_Insert_Delete.Length > 0 and then
+           Parser_State.Recover_Insert_Delete.Peek.Op = Delete and then
+           Parser_State.Recover_Insert_Delete.Peek.Token_Index =
+           (if Parser_State.Inc_Shared_Token
+            then Parser_State.Shared_Token + 1
+            else Parser_State.Shared_Token)
+         then
+            Parser_State.Shared_Token := Parser_State.Shared_Token + 1;
+            Parser_State.Recover_Insert_Delete.Drop;
+         else
+            exit;
+         end if;
+      end loop;
+   end Do_Deletes;
+
+   --  Verb: the type of parser cycle to execute;
+   --
+   --  Accept : all Parsers.Verb return Accept - done parsing.
+   --
+   --  Shift : some Parsers.Verb return Shift, all with the same current
+   --  token in Shared_Parser.Terminals.
+   --
+   --  Shift_Recover : some Parsers.Verb return Shift, with current
+   --  tokens virtual (inserted by error recovery).
+   --
+   --  Pause : Resume is active, and this parser has reached Resume_Goal,
+   --  so it is waiting for the others to catch up.
+   --
+   --  Reduce : some Parsers.Verb return Reduce.
+   --
+   --  Error : all Parsers.Verb return Error.
+   --
+   --  Zombie_Count: count of parsers in Error state
+   procedure Parse_Verb
+     (Shared_Parser : in out LR.Parser.Parser;
+      Verb          :    out All_Parse_Action_Verbs;
+      Zombie_Count  :    out SAL.Base_Peek_Type)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      Shift_Count         : SAL.Base_Peek_Type := 0;
+      Shift_Recover_Count : SAL.Base_Peek_Type := 0;
+      Shift_Virtual_Count : SAL.Base_Peek_Type := 0;
+      Accept_Count        : SAL.Base_Peek_Type := 0;
+      Error_Count         : SAL.Base_Peek_Type := 0;
+      Resume_Active       : Boolean            := False;
+   begin
+      Zombie_Count := 0;
+
+      for Parser_State of Shared_Parser.Parsers loop
+         case Parser_State.Verb is
+         when Pause | Shift_Recover | Shift =>
+            Do_Deletes (Shared_Parser, Parser_State);
+
+            if Parser_State.Recover_Insert_Delete.Length > 0 and then
+              Parser_State.Recover_Insert_Delete.Peek.Op = Insert and then
+              Parser_State.Recover_Insert_Delete.Peek.Token_Index =
+              (if Parser_State.Inc_Shared_Token
+               then Parser_State.Shared_Token + 1
+               else Parser_State.Shared_Token)
+            then
+               --  Shifting a virtual token.
+               Shift_Virtual_Count := Shift_Virtual_Count + 1;
+               Shift_Recover_Count := Shift_Recover_Count + 1;
+               Parser_State.Set_Verb (Shift_Recover);
+
+            else
+               Shift_Count := Shift_Count + 1;
+               Parser_State.Set_Verb (Shift);
+            end if;
+
+            if Parser_State.Resume_Active then
+               if Parser_State.Resume_Token_Goal <= Parser_State.Shared_Token 
then
+                  Parser_State.Resume_Active := False;
+                  if Trace_Parse > Detail then
+                     Shared_Parser.Trace.Put_Line (Integer'Image 
(Parser_State.Label) & ": resume_active: False");
+                  end if;
+               else
+                  Resume_Active := True;
+               end if;
+            end if;
+
+         when Reduce =>
+            Verb := Reduce;
+            return;
+
+         when Accept_It =>
+            Accept_Count := Accept_Count + 1;
+
+         when Error =>
+            if Shared_Parser.Enable_McKenzie_Recover then
+               --  This parser is waiting for others to error; they can 
continue
+               --  parsing.
+               Zombie_Count := Zombie_Count + 1;
+            else
+               Error_Count := Error_Count + 1;
+            end if;
+         end case;
+      end loop;
+
+      if Accept_Count > 0 and Shared_Parser.Parsers.Count = Accept_Count + 
Zombie_Count then
+         Verb := Accept_It;
+
+      elsif Shared_Parser.Parsers.Count = Error_Count + Zombie_Count then
+         Verb := Error;
+
+      elsif Shift_Recover_Count > 0 then
+         Verb := Shift_Recover;
+
+      elsif Shift_Count > 0 then
+         Verb := Shift;
+
+      else
+         raise SAL.Programmer_Error;
+      end if;
+
+      if Resume_Active then
+         for Parser_State of Shared_Parser.Parsers loop
+            if Parser_State.Verb in Shift | Shift_Recover and not 
Parser_State.Resume_Active then
+               Parser_State.Set_Verb (Pause);
+            end if;
+         end loop;
+      end if;
+   end Parse_Verb;
+
+   ----------
+   --  Public subprograms, declaration order
+
+   overriding procedure Finalize (Object : in out LR.Parser.Parser)
+   is begin
+      Free_Table (Object.Table);
+   end Finalize;
+
+   procedure New_Parser
+     (Parser                                :    out          LR.Parser.Parser;
+      Trace                                 : not null access 
WisiToken.Trace'Class;
+      Lexer                                 : in              
WisiToken.Lexer.Handle;
+      Table                                 : in              Parse_Table_Ptr;
+      Language_Fixes                        : in              
Language_Fixes_Access;
+      Language_Use_Minimal_Complete_Actions : in              
Language_Use_Minimal_Complete_Actions_Access;
+      Language_String_ID_Set                : in              
Language_String_ID_Set_Access;
+      User_Data                             : in              
WisiToken.Syntax_Trees.User_Data_Access;
+      Max_Parallel                          : in              
SAL.Base_Peek_Type := Default_Max_Parallel;
+      Terminate_Same_State                  : in              Boolean          
  := True)
+   is
+      use all type Syntax_Trees.User_Data_Access;
+   begin
+      Parser.Lexer                               := Lexer;
+      Parser.Trace                               := Trace;
+      Parser.Table                               := Table;
+      Parser.Language_Fixes                      := Language_Fixes;
+      Parser.Language_Use_Minimal_Complete_Actions := 
Language_Use_Minimal_Complete_Actions;
+      Parser.Language_String_ID_Set              := Language_String_ID_Set;
+      Parser.User_Data                           := User_Data;
+
+      --  We can't use Table.McKenzie_Param /= Default_McKenzie_Param here,
+      --  because the discriminants are different. We also can't use just
+      --  Table.McKenzie_Param.Cost_Limit /=
+      --  Default_McKenzie_Param.Cost_Limit, because some grammars don't set
+      --  a Cost_Limit, just some other limit.
+      Parser.Enable_McKenzie_Recover :=
+        Table.McKenzie_Param.Cost_Limit /= Default_McKenzie_Param.Cost_Limit or
+          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;
+
+      Parser.Max_Parallel         := Max_Parallel;
+      Parser.Terminate_Same_State := Terminate_Same_State;
+
+      if User_Data /= null then
+         User_Data.Set_Lexer_Terminals (Lexer, 
Parser.Terminals'Unchecked_Access);
+      end if;
+   end New_Parser;
+
+   overriding procedure Parse (Shared_Parser : aliased in out LR.Parser.Parser)
+   is
+      use all type Ada.Strings.Unbounded.Unbounded_String;
+      use all type Syntax_Trees.User_Data_Access;
+      use all type Ada.Containers.Count_Type;
+      use all type SAL.Base_Peek_Type;
+
+      Trace : WisiToken.Trace'Class renames Shared_Parser.Trace.all;
+
+      Current_Verb   : All_Parse_Action_Verbs;
+      Current_Parser : Parser_Lists.Cursor;
+      Action         : Parse_Action_Node_Ptr;
+      Zombie_Count   : SAL.Base_Peek_Type;
+
+      procedure Check_Error (Check_Parser : in out Parser_Lists.Cursor)
+      is begin
+         if Check_Parser.Verb = Error then
+            --  This parser errored on last input. This is how grammar 
conflicts
+            --  are resolved when the input text is valid, in which case we 
should
+            --  just terminate this parser. However, this may be due to invalid
+            --  input text, so we keep the parser alive but suspended for a few
+            --  tokens, to see if the other parsers also error, in which case 
they
+            --  all participate in error recovery.
+
+            --  We do not create zombie parsers during resume.
+            if not Check_Parser.State_Ref.Resume_Active then
+               --  Parser is now a zombie
+               if Trace_Parse > Detail then
+                  Trace.Put_Line (Integer'Image (Check_Parser.Label) & ": 
zombie");
+               end if;
+               Check_Parser.Next;
+
+            else
+               if Shared_Parser.Parsers.Count = 1 then
+                  if Trace_Parse > Outline then
+                     Trace.Put_Line (Integer'Image (Check_Parser.Label) & ": 
error during resume");
+                  end if;
+                  Shared_Parser.Parsers.First_State_Ref.Errors.Append
+                    ((Label          => LR.Message,
+                      First_Terminal => Trace.Descriptor.First_Terminal,
+                      Last_Terminal  => Trace.Descriptor.Last_Terminal,
+                      Recover        => <>,
+                      Msg            => +"error during resume"));
+                  raise Syntax_Error;
+
+               else
+                  --  This is ok if a conflict occured during resume - we 
assume this is
+                  --  a branch that failed during recover as well. Otherwise 
it's a
+                  --  programmer error.
+                  if Check_Parser.State_Ref.Conflict_During_Resume then
+                     Shared_Parser.Parsers.Terminate_Parser
+                       (Check_Parser, "error in conflict during resume", 
Shared_Parser.Trace.all);
+                  else
+                     raise SAL.Programmer_Error with "error during resume";
+                  end if;
+               end if;
+            end if;
+         else
+            Check_Parser.Next;
+         end if;
+      end Check_Error;
+
+   begin
+      if Shared_Parser.User_Data /= null then
+         Shared_Parser.User_Data.Reset;
+      end if;
+
+      Shared_Parser.Lex_All;
+
+      Shared_Parser.String_Quote_Checked := Invalid_Line_Number;
+      Shared_Parser.Shared_Tree.Clear;
+      Shared_Parser.Parsers              := Parser_Lists.New_List
+        (Shared_Tree => Shared_Parser.Shared_Tree'Unchecked_Access);
+
+      Shared_Parser.Parsers.First.State_Ref.Stack.Push 
((Shared_Parser.Table.State_First, others => <>));
+
+      Main_Loop :
+      loop
+         --  exit on Accept_It action or syntax error.
+
+         Parse_Verb (Shared_Parser, Current_Verb, Zombie_Count);
+
+         if Trace_Parse > Extra then
+            Trace.Put_Line ("cycle start; current_verb: " & 
Parse_Action_Verbs'Image (Current_Verb));
+         end if;
+
+         case Current_Verb is
+         when Pause =>
+            null;
+
+         when Shift =>
+            --  We just shifted a token; get the next token from
+            --  Shared_Parser.Terminals.
+
+            for Parser_State of Shared_Parser.Parsers loop
+               if Parser_State.Verb = Error then
+                  if Shared_Parser.Enable_McKenzie_Recover then
+                     Parser_State.Zombie_Token_Count := 
Parser_State.Zombie_Token_Count + 1;
+                     if Trace_Parse > Extra then
+                        Trace.Put_Line
+                          (Integer'Image (Parser_State.Label) & ": zombie (" &
+                             Token_Index'Image
+                               (Shared_Parser.Table.McKenzie_Param.Check_Limit 
- Parser_State.Zombie_Token_Count) &
+                             " tokens remaining)");
+                     end if;
+                  end if;
+
+               elsif Parser_State.Verb = Shift then
+                  if Parser_State.Inc_Shared_Token then
+                     --  Inc_Shared_Token is only set False by 
McKenzie_Recover; see there
+                     --  for when/why.
+                     Parser_State.Shared_Token := Parser_State.Shared_Token + 
1;
+                  else
+                     Parser_State.Inc_Shared_Token := True;
+                  end if;
+
+                  Parser_State.Current_Token := Parser_State.Tree.Add_Terminal
+                    (Parser_State.Shared_Token, Shared_Parser.Terminals);
+
+                  if Trace_Parse > Extra then
+                     Trace.Put_Line
+                       (Integer'Image (Parser_State.Label) & ": current_token" 
& Parser_State.Tree.Image
+                          (Parser_State.Current_Token, Trace.Descriptor.all));
+                  end if;
+               end if;
+            end loop;
+
+         when Shift_Recover =>
+            --  Same as Shift, except input a token inserted by error 
recovery, or
+            --  input from Shared_Parser.Terminals during error recovery.
+
+            for Parser_State of Shared_Parser.Parsers loop
+               --  We don't check for Verb = Error; during recovery, errors 
cause
+               --  parsers to terminate immediately.
+
+               if Parser_State.Verb = Shift_Recover then
+                  if Parser_State.Recover_Insert_Delete.Length > 0 and then
+                    Parser_State.Recover_Insert_Delete.Peek.Op = Insert and 
then
+                    Parser_State.Recover_Insert_Delete.Peek.Token_Index =
+                    (if Parser_State.Inc_Shared_Token
+                     then Parser_State.Shared_Token + 1
+                     else Parser_State.Shared_Token)
+                  then
+                     Parser_State.Current_Token := 
Parser_State.Tree.Add_Terminal
+                       (Parser_State.Recover_Insert_Delete.Get.ID);
+
+                     if Trace_Parse > Extra then
+                        Trace.Put_Line
+                          (Integer'Image (Parser_State.Label) & ": 
current_token" & Parser_State.Tree.Image
+                             (Parser_State.Current_Token, 
Trace.Descriptor.all));
+                     end if;
+
+                  elsif (if Parser_State.Inc_Shared_Token
+                         then Parser_State.Shared_Token + 1
+                         else Parser_State.Shared_Token) <= 
Shared_Parser.Terminals.Last_Index
+                  then
+                     if Parser_State.Inc_Shared_Token then
+                        --  Inc_Shared_Token is only set False by 
McKenzie_Recover, see there
+                        --  for when/why.
+                        Parser_State.Shared_Token := Parser_State.Shared_Token 
+ 1;
+                     else
+                        Parser_State.Inc_Shared_Token := True;
+                     end if;
+
+                     Parser_State.Current_Token := 
Parser_State.Tree.Add_Terminal
+                       (Parser_State.Shared_Token, Shared_Parser.Terminals);
+
+                     if Trace_Parse > Extra then
+                        Trace.Put_Line
+                          (Integer'Image (Parser_State.Label) & ": 
current_token" & Parser_State.Tree.Image
+                             (Parser_State.Current_Token, 
Trace.Descriptor.all));
+                     end if;
+
+                  else
+                     --  Set_Verb set the wrong verb.
+                     raise SAL.Programmer_Error;
+                  end if;
+               end if;
+            end loop;
+
+         when Accept_It =>
+            --  All parsers accepted or are zombies.
+            declare
+               Count : constant SAL.Base_Peek_Type := 
Shared_Parser.Parsers.Count;
+               Temp  : Parser_Lists.Cursor;
+            begin
+               if Count = 1 then
+                  --  Nothing more to do
+                  if Trace_Parse > Outline then
+                     Trace.Put_Line (Integer'Image 
(Shared_Parser.Parsers.First.Label) & ": succeed");
+                  end if;
+                  exit Main_Loop;
+
+               elsif Zombie_Count + 1 = Count then
+                  --  All but one are zombies
+                  Current_Parser := Shared_Parser.Parsers.First;
+                  loop
+                     if Current_Parser.Verb = Accept_It then
+                        if Trace_Parse > Outline then
+                           Trace.Put_Line (Integer'Image 
(Current_Parser.Label) & ": succeed with zombies");
+                        end if;
+                        Current_Parser.Next;
+                     else
+                        Temp := Current_Parser;
+                        Current_Parser.Next;
+                        Shared_Parser.Parsers.Terminate_Parser (Temp, 
"zombie", Shared_Parser.Trace.all);
+                     end if;
+                     exit when Current_Parser.Is_Done;
+                  end loop;
+
+                  exit Main_Loop;
+
+               else
+                  --  More than one parser is active.
+                  declare
+                     use all type Parser_Lists.Cursor;
+                     Error_Parser_Count     : Integer := (if 
Shared_Parser.Lexer.Errors.Length > 0 then 1 else 0);
+                     Recover_Ops_Length     : Ada.Containers.Count_Type;
+                     Min_Recover_Ops_Length : Ada.Containers.Count_Type := 
Ada.Containers.Count_Type'Last;
+                     Min_Recover_Ops_Cur    : Parser_Lists.Cursor;
+                  begin
+                     Current_Parser := Shared_Parser.Parsers.First;
+                     loop
+                        if Current_Parser.Verb = Accept_It then
+                           if Current_Parser.State_Ref.Errors.Length > 0 then
+                              Error_Parser_Count := Error_Parser_Count + 1;
+                           end if;
+                           Current_Parser.Next;
+                        else
+                           Temp := Current_Parser;
+                           Current_Parser.Next;
+                           Shared_Parser.Parsers.Terminate_Parser (Temp, 
"zombie", Shared_Parser.Trace.all);
+                        end if;
+                        exit when Current_Parser.Is_Done;
+                     end loop;
+
+                     if Error_Parser_Count > 0 then
+                        --  There was at least one error. We assume that 
caused the ambiguous
+                        --  parse, and we pick the parser with the minimum 
recover ops length
+                        --  to allow the parse to succeed. We terminate the 
other parsers so
+                        --  the first parser executes actions.
+                        --
+                        --  Note all surviving parsers must have the same 
error count, or only
+                        --  the one with the lowest would get here.
+                        Current_Parser := Shared_Parser.Parsers.First;
+                        loop
+                           Recover_Ops_Length := 
Current_Parser.Max_Recover_Ops_Length;
+                           if Recover_Ops_Length < Min_Recover_Ops_Length then
+                              Min_Recover_Ops_Length := Recover_Ops_Length;
+                              Min_Recover_Ops_Cur    := Current_Parser;
+                           end if;
+                           Current_Parser.Next;
+                           exit when Current_Parser.Is_Done;
+                        end loop;
+
+                        Current_Parser := Shared_Parser.Parsers.First;
+                        loop
+                           if Current_Parser = Min_Recover_Ops_Cur then
+                              Current_Parser.Next;
+                           else
+                              Temp := Current_Parser;
+                              Current_Parser.Next;
+                              Shared_Parser.Parsers.Terminate_Parser (Temp, 
"errors", Shared_Parser.Trace.all);
+                           end if;
+                           exit when Current_Parser.Is_Done;
+                        end loop;
+
+                        if Trace_Parse > Outline then
+                           Trace.Put_Line ("ambiguous with error");
+                        end if;
+
+                        exit Main_Loop;
+
+                     else
+                        --  There were no previous errors. We allow the parse 
to fail, on the
+                        --  assumption that an otherwise correct input should 
not yield an
+                        --  ambiguous parse.
+                        declare
+                           Token : Base_Token renames Shared_Parser.Terminals 
(Shared_Parser.Terminals.Last_Index);
+                        begin
+                           raise WisiToken.Parse_Error with Error_Message
+                             (Shared_Parser.Lexer.File_Name, Token.Line, 
Token.Column,
+                              "Ambiguous parse:" & SAL.Base_Peek_Type'Image 
(Count) & " parsers active.");
+                        end;
+                     end if;
+                  end;
+               end if;
+            end;
+
+         when Reduce =>
+            null;
+
+         when Error =>
+            --  All parsers errored; attempt recovery
+            declare
+               use all type McKenzie_Recover.Recover_Status;
+               Recover_Result : McKenzie_Recover.Recover_Status := 
McKenzie_Recover.Recover_Status'First;
+            begin
+               --  Recover algorithms expect current token at
+               --  Parsers(*).Current_Token, will set
+               --  Parsers(*).Recover_Insert_Delete with new input tokens and
+               --  deletions, adjust Parsers(*).Stack, and set
+               --  Parsers(*).Current_Token and Parsers(*).Verb.
+
+               if Shared_Parser.Enable_McKenzie_Recover then
+                  if Trace_Parse > Outline then
+                     Trace.Put_Line ("recover");
+                  end if;
+                  Recover_Result := McKenzie_Recover.Recover (Shared_Parser);
+
+                  if Trace_Parse > Outline then
+                     if Recover_Result = Success  then
+                        Trace.Put_Line
+                          ("recover: succeed, parser count" & 
SAL.Base_Peek_Type'Image (Shared_Parser.Parsers.Count));
+                     else
+                        Trace.Put_Line
+                          ("recover: fail " & 
McKenzie_Recover.Recover_Status'Image (Recover_Result) &
+                             ", parser count" & SAL.Base_Peek_Type'Image 
(Shared_Parser.Parsers.Count));
+                     end if;
+                  end if;
+
+                  if Ada.Text_IO.Is_Open (Shared_Parser.Recover_Log_File) then
+                     declare
+                        use Ada.Text_IO;
+                     begin
+                        Put
+                          (Shared_Parser.Recover_Log_File,
+                           Ada.Calendar.Formatting.Image (Ada.Calendar.Clock) 
& " " &
+                             McKenzie_Recover.Recover_Status'Image 
(Recover_Result) & " " &
+                             SAL.Base_Peek_Type'Image 
(Shared_Parser.Parsers.Count) & " '" &
+                             Shared_Parser.Lexer.File_Name & "'");
+
+                        for Parser of Shared_Parser.Parsers loop
+                           Put
+                             (Shared_Parser.Recover_Log_File,
+                              Integer'Image (Parser.Recover.Enqueue_Count) &
+                                Integer'Image (Parser.Recover.Check_Count) & " 
" &
+                                Boolean'Image (Parser.Recover.Success));
+                        end loop;
+                        New_Line (Shared_Parser.Recover_Log_File);
+                        Flush (Shared_Parser.Recover_Log_File);
+                     end;
+                  end if;
+               else
+                  if Trace_Parse > Outline then
+                     Trace.Put_Line ("recover disabled or not defined");
+                  end if;
+               end if;
+
+               if Recover_Result = Success then
+                  declare
+                     Shift_Recover_Count : Integer := 0;
+                  begin
+                     for Parser_State of Shared_Parser.Parsers loop
+                        Parser_State.Resume_Active          := True;
+                        Parser_State.Conflict_During_Resume := False;
+
+                        if Trace_Parse > Outline then
+                           Trace.Put_Line
+                             (Integer'Image (Parser_State.Label) & ": 
Current_Token " &
+                                Parser_State.Tree.Image 
(Parser_State.Current_Token, Trace.Descriptor.all) &
+                                " Shared_Token " & Image
+                                  (Parser_State.Shared_Token, 
Shared_Parser.Terminals, Trace.Descriptor.all));
+
+                           if Trace_Parse > Detail then
+                              Shared_Parser.Trace.Put_Line
+                                (Integer'Image (Parser_State.Label) & ": 
resume_active: True, token goal" &
+                                   Token_Index'Image 
(Parser_State.Resume_Token_Goal));
+                           end if;
+                        end if;
+
+                        case Parser_State.Verb is
+                        when Shift_Recover =>
+                           Shift_Recover_Count := Shift_Recover_Count + 1;
+
+                           Parser_State.Zombie_Token_Count := 0;
+
+                        when Reduce =>
+                           Current_Verb := Reduce;
+
+                           Parser_State.Zombie_Token_Count := 0;
+
+                        when Error =>
+                           --  Force this parser to be terminated.
+                           if Shared_Parser.Enable_McKenzie_Recover then
+                              Parser_State.Zombie_Token_Count := 
Shared_Parser.Table.McKenzie_Param.Check_Limit + 1;
+                           end if;
+
+                        when Shift =>
+                           if Current_Verb /= Reduce then
+                              Current_Verb := Shift;
+                           end if;
+
+                        when Pause | Accept_It =>
+                           raise SAL.Programmer_Error;
+                        end case;
+                     end loop;
+
+                     if Shift_Recover_Count > 0 then
+                        Current_Verb := Shift_Recover;
+                     end if;
+                  end;
+
+               else
+                  --  Terminate with error. Parser_State has all the required 
info on
+                  --  the original error (recorded by Error in Do_Action); 
report reason
+                  --  recover failed.
+                  for Parser_State of Shared_Parser.Parsers loop
+                     Parser_State.Errors.Append
+                       ((Label          => LR.Message,
+                         First_Terminal => Trace.Descriptor.First_Terminal,
+                         Last_Terminal  => Trace.Descriptor.Last_Terminal,
+                         Recover        => <>,
+                         Msg            => +"recover: fail " & 
McKenzie_Recover.Recover_Status'Image (Recover_Result)));
+                  end loop;
+                  raise WisiToken.Syntax_Error;
+               end if;
+            end;
+         end case;
+
+         --  We don't use 'for Parser_State of Parsers loop' here,
+         --  because terminate on error and spawn on conflict require
+         --  changing the parser list.
+         Current_Parser := Shared_Parser.Parsers.First;
+         Action_Loop :
+         loop
+            exit Action_Loop when Current_Parser.Is_Done;
+
+            if Shared_Parser.Terminate_Same_State and
+              Current_Verb in Shift | Shift_Recover and
+              (for all Parser of Shared_Parser.Parsers => 
Parser.Recover_Insert_Delete.Count = 0)
+            then
+               Shared_Parser.Parsers.Duplicate_State (Current_Parser, 
Shared_Parser.Trace.all);
+               --  If Duplicate_State terminated Current_Parser, 
Current_Parser now
+               --  points to the next parser. Otherwise it is unchanged.
+            end if;
+
+            exit Action_Loop when Current_Parser.Is_Done;
+
+            if Trace_Parse > Extra then
+               Trace.Put_Line
+                 ("current_verb: " & Parse_Action_Verbs'Image (Current_Verb) &
+                    "," & Integer'Image (Current_Parser.Label) &
+                    ".verb: " & Parse_Action_Verbs'Image 
(Current_Parser.Verb));
+            end if;
+
+            --  Each branch of the following 'if' calls either 
Current_Parser.Free
+            --  (which advances to the next parser) or Current_Parser.Next.
+
+            if Current_Parser.Verb = Error then
+               --  This parser is a zombie; see Check_Error above.
+               --
+               --  Check to see if it is time to terminate it
+               if Shared_Parser.Enable_McKenzie_Recover and then
+                 Current_Parser.State_Ref.Zombie_Token_Count <= 
Shared_Parser.Table.McKenzie_Param.Check_Limit
+               then
+                  if Trace_Parse > Detail then
+                     Trace.Put_Line (Integer'Image (Current_Parser.Label) & ": 
zombie");
+                  end if;
+
+                  Current_Parser.Next;
+               else
+                  Shared_Parser.Parsers.Terminate_Parser (Current_Parser, 
"zombie", Shared_Parser.Trace.all);
+               end if;
+
+            elsif Current_Parser.Verb = Current_Verb then
+               if Trace_Parse > Extra then
+                  Parser_Lists.Put_Top_10 (Trace, Current_Parser);
+               end if;
+
+               declare
+                  State : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref.Element.all;
+               begin
+                  Action := Action_For
+                    (Table => Shared_Parser.Table.all,
+                     State => State.Stack.Peek.State,
+                     ID    => State.Tree.ID (State.Current_Token));
+               end;
+
+               if Action.Next /= null then
+                  --  Conflict; spawn a new parser (before modifying 
Current_Parser
+                  --  stack).
+
+                  Current_Parser.State_Ref.Conflict_During_Resume := 
Current_Parser.State_Ref.Resume_Active;
+
+                  if Shared_Parser.Parsers.Count = Shared_Parser.Max_Parallel 
then
+                     --  If errors were recovered, terminate a parser that 
used the
+                     --  highest cost solution.
+                     declare
+                        use all type WisiToken.Parse.LR.Parser_Lists.Cursor;
+                        Max_Recover_Cost : Integer             := 0;
+                        Max_Parser       : Parser_Lists.Cursor;
+                        Cur              : Parser_Lists.Cursor := 
Shared_Parser.Parsers.First;
+                     begin
+                        loop
+                           exit when Cur.Is_Done;
+                           if Cur.Total_Recover_Cost > Max_Recover_Cost then
+                              Max_Parser       := Cur;
+                              Max_Recover_Cost := Cur.Total_Recover_Cost;
+                           end if;
+                           Cur.Next;
+                        end loop;
+
+                        if Max_Recover_Cost > 0 then
+                           if Max_Parser = Current_Parser then
+                              Current_Parser.Next;
+                              Shared_Parser.Parsers.Terminate_Parser
+                                (Current_Parser, "too many parsers; max error 
repair cost", Trace);
+                              exit Action_Loop;
+                           else
+                              Shared_Parser.Parsers.Terminate_Parser
+                                (Max_Parser, "too many parsers; max error 
repair cost", Trace);
+                           end if;
+                        end if;
+                     end;
+                  end if;
+
+                  if Shared_Parser.Parsers.Count = Shared_Parser.Max_Parallel 
then
+                     declare
+                        Parser_State : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref;
+                        Token : Base_Token renames Shared_Parser.Terminals 
(Parser_State.Shared_Token);
+                     begin
+                        raise WisiToken.Parse_Error with Error_Message
+                          (Shared_Parser.Lexer.File_Name, Token.Line, 
Token.Column,
+                           "too many parallel parsers required in grammar 
state" &
+                             State_Index'Image (Parser_State.Stack.Peek.State) 
&
+                             "; simplify grammar, or increase max-parallel (" &
+                             SAL.Base_Peek_Type'Image 
(Shared_Parser.Max_Parallel) & ")");
+                     end;
+
+                  else
+                     if Trace_Parse > Outline then
+                        Trace.Put_Line
+                          (Integer'Image (Current_Parser.Label) & ": spawn" &
+                             Integer'Image (Shared_Parser.Parsers.Last_Label + 
1) & ", (" &
+                             Trimmed_Image (1 + Integer 
(Shared_Parser.Parsers.Count)) & " active)");
+                     end if;
+
+                     Shared_Parser.Parsers.Prepend_Copy (Current_Parser);
+                     Do_Action (Action.Next.Item, Shared_Parser.Parsers.First, 
Shared_Parser);
+
+                     --  We must terminate error parsers immediately in order 
to avoid
+                     --  zombie parsers during recovery.
+                     declare
+                        Temp : Parser_Lists.Cursor := 
Shared_Parser.Parsers.First;
+                     begin
+                        Check_Error (Temp);
+                     end;
+                  end if;
+               end if;
+
+               Do_Action (Action.Item, Current_Parser, Shared_Parser);
+               Check_Error (Current_Parser);
+
+            else
+               --  Current parser is waiting for others to catch up
+               Current_Parser.Next;
+            end if;
+         end loop Action_Loop;
+      end loop Main_Loop;
+
+      --  We don't raise Syntax_Error for lexer errors, since they are all
+      --  recovered, either by inserting a quote, or by ignoring the
+      --  character.
+   exception
+   when Syntax_Error | WisiToken.Parse_Error =>
+      raise;
+
+   when E : others =>
+      declare
+         Msg : constant String := Ada.Exceptions.Exception_Name (E) & ": " & 
Ada.Exceptions.Exception_Message (E);
+      begin
+         --  Emacs displays errors in the *syntax-errors* buffer
+         Shared_Parser.Parsers.First_State_Ref.Errors.Append
+           ((Label          => LR.Message,
+             First_Terminal => Trace.Descriptor.First_Terminal,
+             Last_Terminal  => Trace.Descriptor.Last_Terminal,
+             Recover        => <>,
+             Msg            => +Msg));
+
+         --  Emacs displays the exception message in the echo area; easy to 
miss
+         raise WisiToken.Parse_Error with Msg;
+      end;
+   end Parse;
+
+   overriding
+   procedure Execute_Actions (Parser : in out LR.Parser.Parser)
+   is
+      use all type SAL.Base_Peek_Type;
+      use all type Syntax_Trees.User_Data_Access;
+
+      Descriptor : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+
+      procedure Process_Node
+        (Tree : in out Syntax_Trees.Tree;
+         Node : in     Syntax_Trees.Valid_Node_Index)
+      is
+         use all type Syntax_Trees.Node_Label;
+      begin
+         if Tree.Label (Node) /= Nonterm then
+            return;
+         end if;
+
+         declare
+            use all type Syntax_Trees.Semantic_Action;
+            Tree_Children : constant Syntax_Trees.Valid_Node_Index_Array := 
Tree.Children (Node);
+         begin
+            Parser.User_Data.Reduce (Tree, Node, Tree_Children);
+
+            if Tree.Action (Node) /= null then
+               Tree.Action (Node) (Parser.User_Data.all, Tree, Node, 
Tree_Children);
+            end if;
+         end;
+      end Process_Node;
+
+   begin
+      if Parser.User_Data /= null then
+         if (for some Par of Parser.Parsers =>
+               (for some Err of Par.Errors => Any (Err.Recover.Ops, Delete)))
+         then
+            if Parser.Parsers.Count > 1 then
+               raise Syntax_Error with "ambiguous parse with deleted tokens; 
can't execute actions";
+            end if;
+            for Err of Parser.Parsers.First_State_Ref.Errors loop
+               for Op of Err.Recover.Ops loop
+                  case Op.Op is
+                  when Delete =>
+                     Parser.User_Data.Delete_Token (Op.Token_Index);
+                  when others =>
+                     null;
+                  end case;
+               end loop;
+            end loop;
+         end if;
+
+         for Parser_State of Parser.Parsers loop
+            if Trace_Action > Outline then
+               Parser.Trace.Put_Line
+                 (Integer'Image (Parser_State.Label) & ": root node: " & 
Parser_State.Tree.Image
+                    (Parser_State.Tree.Root, Descriptor));
+            end if;
+
+            Parser_State.Tree.Process_Tree (Process_Node'Access);
+         end loop;
+      end if;
+   end Execute_Actions;
+
+   overriding function Any_Errors (Parser : in LR.Parser.Parser) return Boolean
+   is
+      use all type SAL.Base_Peek_Type;
+      use all type Ada.Containers.Count_Type;
+      Parser_State : Parser_Lists.Parser_State renames 
Parser.Parsers.First_Constant_State_Ref;
+   begin
+      pragma Assert (Parser_State.Tree.Flushed);
+      return Parser.Parsers.Count > 1 or Parser_State.Errors.Length > 0 or 
Parser.Lexer.Errors.Length > 0;
+   end Any_Errors;
+
+   overriding procedure Put_Errors (Parser : in LR.Parser.Parser)
+   is
+      use all type SAL.Base_Peek_Type;
+      use Ada.Text_IO;
+
+      Parser_State : Parser_Lists.Parser_State renames 
Parser.Parsers.First_Constant_State_Ref;
+      Descriptor   : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+   begin
+      for Item of Parser.Lexer.Errors loop
+         Put_Line
+           (Current_Error,
+            Parser.Lexer.File_Name & ":0:0: lexer unrecognized character at" & 
Buffer_Pos'Image (Item.Char_Pos));
+      end loop;
+
+      for Item of Parser_State.Errors loop
+         case Item.Label is
+         when Action =>
+            declare
+               Index : constant Base_Token_Index := 
Parser_State.Tree.Min_Terminal_Index (Item.Error_Token);
+            begin
+               if Index = Invalid_Token_Index then
+                  --  Error_Token is virtual
+                  Put_Line
+                    (Current_Error,
+                     Error_Message
+                       (Parser.Lexer.File_Name, 1, 0,
+                        "syntax error: expecting " & Image (Item.Expecting, 
Descriptor) &
+                          ", found " & Image (Parser_State.Tree.ID 
(Item.Error_Token), Descriptor)));
+               else
+                  declare
+                     Token : Base_Token renames Parser.Terminals (Index);
+                  begin
+                     Put_Line
+                       (Current_Error,
+                        Error_Message
+                          (Parser.Lexer.File_Name, Token.Line, Token.Column,
+                           "syntax error: expecting " & Image (Item.Expecting, 
Descriptor) &
+                             ", found '" & Parser.Lexer.Buffer_Text 
(Token.Byte_Region) & "'"));
+                  end;
+               end if;
+            end;
+         when Check =>
+            Put_Line
+              (Current_Error,
+               Parser.Lexer.File_Name & ":0:0: semantic check error: " &
+                 Semantic_Checks.Image (Item.Check_Status, Descriptor));
+         when Message =>
+            Put_Line (Current_Error, -Item.Msg);
+         end case;
+
+         if Item.Recover.Stack.Depth /= 0 then
+            Put_Line (Current_Error, "   recovered: " & Image 
(Item.Recover.Ops, Descriptor));
+         end if;
+      end loop;
+   end Put_Errors;
+
+end WisiToken.Parse.LR.Parser;
diff --git a/wisitoken-parse-lr-parser.ads b/wisitoken-parse-lr-parser.ads
new file mode 100644
index 0000000..8aa5906
--- /dev/null
+++ b/wisitoken-parse-lr-parser.ads
@@ -0,0 +1,145 @@
+--  Abstract :
+--
+--  A generalized LR parser.
+--
+--  In a child package of Parser.LR partly for historical reasons,
+--  partly to allow McKenzie_Recover to be in a sibling package.
+--
+--  Copyright (C) 2002, 2003, 2009, 2010, 2013-2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  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 WisiToken.Parse.LR.Parser_Lists;
+with WisiToken.Lexer;
+with WisiToken.Parse;
+with WisiToken.Syntax_Trees;
+package WisiToken.Parse.LR.Parser is
+
+   Default_Max_Parallel : constant := 15;
+
+   type Language_Fixes_Access is access procedure
+     (Trace             : in out WisiToken.Trace'Class;
+      Lexer             : access constant WisiToken.Lexer.Instance'Class;
+      Parser_Label      : in     Natural;
+      Parse_Table       : in     WisiToken.Parse.LR.Parse_Table;
+      Terminals         : in     Base_Token_Arrays.Vector;
+      Tree              : in     Syntax_Trees.Tree;
+      Local_Config_Heap : in out Config_Heaps.Heap_Type;
+      Config            : in     Configuration);
+   --  Config encountered a parse table Error action, or failed a
+   --  semantic check; attempt to provide a language-specific fix,
+   --  enqueuing new configs on Local_Config_Heap.
+   --
+   --  For a failed semantic check, Config.Stack is in the pre-reduce
+   --  state, Config.Error_Token gives the nonterm token,
+   --  Config.Check_Token_Count the token count for the reduce. May be
+   --  called with Nonterm.Virtual = True or Tree.Valid_Indices (stack
+   --  top token_count items) false.
+   --
+   --  For an Error action, Config.Error_Token gives the terminal that
+   --  caused the error.
+
+   type Language_Use_Minimal_Complete_Actions_Access is access function
+     (Next_Token : in Token_ID;
+      Config     : in Configuration)
+     return Boolean;
+   --  Return True if using Minimal_Complete_Actions is appropriate.
+   --
+   --  For example, if Next_Token is a block end, return True to complete
+   --  the current statement/declaration as quickly as possible..
+
+   type Language_String_ID_Set_Access is access function
+     (Descriptor        : in WisiToken.Descriptor;
+      String_Literal_ID : in Token_ID)
+     return Token_ID_Set;
+   --  Return a Token_ID_Set containing String_Literal_ID and
+   --  nonterminals that can contain String_Literal_ID as part of an
+   --  expression.
+
+   type Post_Recover_Access is access procedure;
+
+   type Parser is new WisiToken.Parse.Base_Parser with record
+      Table                                 : Parse_Table_Ptr;
+      Language_Fixes                        : Language_Fixes_Access;
+      Language_Use_Minimal_Complete_Actions : 
Language_Use_Minimal_Complete_Actions_Access;
+      Language_String_ID_Set                : Language_String_ID_Set_Access;
+
+      String_Quote_Checked : Line_Number_Type := Invalid_Line_Number;
+      --  Max line checked for missing string quote.
+
+      Post_Recover : Post_Recover_Access;
+      --  Gather data for tests.
+
+      Shared_Tree : aliased Syntax_Trees.Base_Tree;
+      --  Each parser (normal and recover) has its own branched syntax tree,
+      --  all branched from this tree. Terminals are added to the tree when
+      --  they become the current token.
+      --
+      --  It is never the case that terminals are added to this shared tree
+      --  when there is more than one task active, so we don't need a
+      --  protected tree.
+      --
+      --  See WisiToken.LR.Parser_Lists Parser_State for more discussion of
+      --  Shared_Tree.
+
+      Parsers : aliased Parser_Lists.List;
+
+      Max_Parallel            : SAL.Base_Peek_Type;
+      Terminate_Same_State    : Boolean;
+      Enable_McKenzie_Recover : Boolean;
+      Recover_Log_File        : Ada.Text_IO.File_Type;
+   end record;
+
+   overriding procedure Finalize (Object : in out LR.Parser.Parser);
+   --  Deep free Object.Table.
+
+   procedure New_Parser
+     (Parser                                :    out          LR.Parser.Parser;
+      Trace                                 : not null access 
WisiToken.Trace'Class;
+      Lexer                                 : in              
WisiToken.Lexer.Handle;
+      Table                                 : in              Parse_Table_Ptr;
+      Language_Fixes                        : in              
Language_Fixes_Access;
+      Language_Use_Minimal_Complete_Actions : in              
Language_Use_Minimal_Complete_Actions_Access;
+      Language_String_ID_Set                : in              
Language_String_ID_Set_Access;
+      User_Data                             : in              
WisiToken.Syntax_Trees.User_Data_Access;
+      Max_Parallel                          : in              
SAL.Base_Peek_Type := Default_Max_Parallel;
+      Terminate_Same_State                  : in              Boolean          
  := True);
+
+   overriding procedure Parse (Shared_Parser : aliased in out 
LR.Parser.Parser);
+   --  Attempt a parse. Calls Parser.Lexer.Reset, runs lexer to end of
+   --  input setting Shared_Parser.Terminals, then parses tokens.
+   --
+   --  If an error is encountered, Parser.Lexer_Errors and
+   --  Parsers(*).Errors contain information about the errors. If a
+   --  recover strategy succeeds, no exception is raised. If recover does
+   --  not succeed, raises Syntax_Error.
+   --
+   --  For errors where no recovery is possible, raises Parse_Error with
+   --  an appropriate error message.
+
+   overriding procedure Execute_Actions (Parser : in out LR.Parser.Parser);
+   --  Execute the grammar actions in Parser.
+
+   overriding function Any_Errors (Parser : in LR.Parser.Parser) return 
Boolean;
+   --  Return True if any errors where encountered, recovered or not.
+
+   overriding procedure Put_Errors (Parser : in LR.Parser.Parser);
+   --  Put user-friendly error messages from the parse to
+   --  Ada.Text_IO.Current_Error.
+
+end WisiToken.Parse.LR.Parser;
diff --git a/wisitoken-parse-lr-parser_lists.adb 
b/wisitoken-parse-lr-parser_lists.adb
new file mode 100644
index 0000000..b2ba8be
--- /dev/null
+++ b/wisitoken-parse-lr-parser_lists.adb
@@ -0,0 +1,405 @@
+--  Abstract :
+--
+--  see spec
+--
+--  Copyright (C) 2014-2018  All Rights Reserved.
+--
+--  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);
+
+package body WisiToken.Parse.LR.Parser_Lists is
+
+   function Parser_Stack_Image
+     (Stack      : in Parser_Stacks.Stack;
+      Descriptor : in WisiToken.Descriptor;
+      Tree       : in Syntax_Trees.Tree;
+      Depth      : in SAL.Base_Peek_Type := 0)
+     return String
+   is
+      use all type Syntax_Trees.Node_Index;
+      use all type SAL.Base_Peek_Type;
+      use Ada.Strings.Unbounded;
+
+      Last : constant SAL.Base_Peek_Type :=
+        (if Depth = 0
+         then Stack.Depth
+         else SAL.Base_Peek_Type'Min (Depth, Stack.Depth));
+
+      Result : Unbounded_String := +"(";
+   begin
+      for I in 1 .. Last loop
+         declare
+            Item : Parser_Stack_Item renames Stack.Peek (I);
+         begin
+            Result := Result &
+              ((if Item.State = Unknown_State then " " else Trimmed_Image 
(Item.State)) & " :" &
+                 (if I = Stack.Depth
+                  then ""
+                  else
+                    (if Item.Token = Syntax_Trees.Invalid_Node_Index -- From 
recover fast-forward
+                     then ""
+                     else Tree.Image (Item.Token, Descriptor) & ", ")));
+         end;
+      end loop;
+      return To_String (Result & ")");
+   end Parser_Stack_Image;
+
+   function New_List (Shared_Tree : in Syntax_Trees.Base_Tree_Access) return 
List
+   is
+      First_Parser_Label : constant := 0;
+      Parser : Parser_State := (Label => First_Parser_Label, others => <>);
+   begin
+      Parser.Tree.Initialize (Shared_Tree, Flush => True);
+
+      return Result : List
+      do
+         Result.Parser_Label := First_Parser_Label;
+
+         Result.Elements.Append (Parser);
+      end return;
+   end New_List;
+
+   function Last_Label (List : in Parser_Lists.List) return Natural
+   is begin
+      return List.Parser_Label;
+   end Last_Label;
+
+   function Count (List : in Parser_Lists.List) return SAL.Base_Peek_Type
+   is begin
+      return List.Elements.Length;
+   end Count;
+
+   function First (List : aliased in out Parser_Lists.List'Class) return Cursor
+   is begin
+      return (Elements => List.Elements'Access, Ptr => List.Elements.First);
+   end First;
+
+   procedure Next (Cursor : in out Parser_Lists.Cursor)
+   is begin
+      Parser_State_Lists.Next (Cursor.Ptr);
+   end Next;
+
+   function Is_Done (Cursor : in Parser_Lists.Cursor) return Boolean
+   is
+      use Parser_State_Lists;
+   begin
+      return Cursor.Ptr = No_Element;
+   end Is_Done;
+
+   function Active_Parser_Count (Cursor : in Parser_Lists.Cursor) return 
SAL.Base_Peek_Type
+   is begin
+      return Cursor.Elements.Length;
+   end Active_Parser_Count;
+
+   function Label (Cursor : in Parser_Lists.Cursor) return Natural
+   is begin
+      return Parser_State_Lists.Constant_Reference (Cursor.Ptr).Label;
+   end Label;
+
+   function Total_Recover_Cost (Cursor : in Parser_Lists.Cursor) return Integer
+   is
+      Result : Integer := 0;
+   begin
+      for Error of Parser_State_Lists.Constant_Reference (Cursor.Ptr).Errors 
loop
+         Result := Error.Recover.Cost;
+      end loop;
+      return Result;
+   end Total_Recover_Cost;
+
+   function Max_Recover_Ops_Length (Cursor : in Parser_Lists.Cursor) return 
Ada.Containers.Count_Type
+   is
+      use Ada.Containers;
+      Result : Count_Type := 0;
+      Errors : Parse_Error_Lists.List renames 
Parser_State_Lists.Constant_Reference (Cursor.Ptr).Errors;
+   begin
+      for Error of Errors loop
+         if Error.Recover.Ops.Length > Result then
+            Result := Error.Recover.Ops.Length;
+         end if;
+      end loop;
+      return Result;
+   end Max_Recover_Ops_Length;
+
+   procedure Set_Verb (Cursor : in Parser_Lists.Cursor; Verb : in 
All_Parse_Action_Verbs)
+   is begin
+      Parser_State_Lists.Reference (Cursor.Ptr).Verb := Verb;
+   end Set_Verb;
+
+   function Verb (Cursor : in Parser_Lists.Cursor) return 
All_Parse_Action_Verbs
+   is begin
+      return Parser_State_Lists.Constant_Reference (Cursor.Ptr).Verb;
+   end Verb;
+   procedure Terminate_Parser
+     (Parsers : in out List;
+      Current : in out Cursor'Class;
+      Message : in     String;
+      Trace   : in out WisiToken.Trace'Class)
+   is
+      use all type SAL.Base_Peek_Type;
+   begin
+      if Trace_Parse > Outline then
+         Trace.Put_Line
+           (Integer'Image (Current.Label) & ": terminate (" &
+              Trimmed_Image (Integer (Parsers.Count) - 1) & " active)" &
+              (if Message'Length > 0 then ": " & Message else ""));
+      end if;
+
+      Current.Free;
+
+      if Parsers.Count = 1 then
+         Parsers.First.State_Ref.Tree.Flush;
+      end if;
+   end Terminate_Parser;
+
+   procedure Duplicate_State
+     (Parsers : in out List;
+      Current : in out Cursor'Class;
+      Trace   : in out WisiToken.Trace'Class)
+   is
+      use all type SAL.Base_Peek_Type;
+      use all type Ada.Containers.Count_Type;
+
+      function Compare
+        (Stack_1 : in Parser_Stacks.Stack;
+         Tree_1  : in Syntax_Trees.Tree;
+         Stack_2 : in Parser_Stacks.Stack;
+         Tree_2  : in Syntax_Trees.Tree)
+        return Boolean
+      is
+      begin
+         if Stack_1.Depth /= Stack_2.Depth then
+            return False;
+         else
+            for I in reverse 1 .. Stack_1.Depth - 1 loop
+               --  Assume they differ near the top; no point in comparing 
bottom
+               --  item. The syntax trees will differ even if the tokens on 
the stack
+               --  are the same, so compare the tokens.
+               declare
+                  Item_1 : Parser_Stack_Item renames Stack_1 (I);
+                  Item_2 : Parser_Stack_Item renames Stack_2 (I);
+               begin
+                  if Item_1.State /= Item_2.State then
+                     return False;
+                  else
+                     if not Syntax_Trees.Same_Token (Tree_1, Item_1.Token, 
Tree_2, Item_2.Token) then
+                        return False;
+                     end if;
+                  end if;
+               end;
+            end loop;
+            return True;
+         end if;
+      end Compare;
+
+      Other : Cursor := Parsers.First;
+   begin
+      loop
+         exit when Other.Is_Done;
+         declare
+            Other_Parser : Parser_State renames Other.State_Ref;
+         begin
+            if Other.Label /= Current.Label and then
+              Other.Verb /= Error and then
+              Compare
+                (Other_Parser.Stack, Other_Parser.Tree, 
Current.State_Ref.Stack, Current.State_Ref.Tree)
+            then
+               exit;
+            end if;
+         end;
+         Other.Next;
+      end loop;
+
+      if not Other.Is_Done then
+         --  Both have the same number of errors, otherwise one would have been
+         --  terminated earlier.
+         if Other.Total_Recover_Cost = Current.Total_Recover_Cost then
+            if Other.Max_Recover_Ops_Length = Current.Max_Recover_Ops_Length 
then
+               Parsers.Terminate_Parser (Other, "duplicate state: random", 
Trace);
+            else
+               if Other.Max_Recover_Ops_Length > 
Current.Max_Recover_Ops_Length then
+                  null;
+               else
+                  Other := Cursor (Current);
+                  Current.Next;
+               end if;
+               Parsers.Terminate_Parser (Other, "duplicate state: ops length", 
Trace);
+            end if;
+         else
+            if Other.Total_Recover_Cost > Current.Total_Recover_Cost then
+               null;
+            else
+               Other := Cursor (Current);
+               Current.Next;
+            end if;
+            Parsers.Terminate_Parser (Other, "duplicate state: cost", Trace);
+         end if;
+      end if;
+   end Duplicate_State;
+
+   function State_Ref (Position : in Cursor) return State_Reference
+   is begin
+      return (Element => Parser_State_Lists.Constant_Reference 
(Position.Ptr).Element);
+   end State_Ref;
+
+   function First_State_Ref (List : in Parser_Lists.List'Class) return 
State_Reference
+   is begin
+      return (Element => Parser_State_Lists.Constant_Reference 
(List.Elements.First).Element);
+   end First_State_Ref;
+
+   function First_Constant_State_Ref (List : in Parser_Lists.List'Class) 
return Constant_State_Reference
+   is begin
+      return (Element => Parser_State_Lists.Constant_Reference 
(List.Elements.First).Element);
+   end First_Constant_State_Ref;
+
+   procedure Put_Top_10 (Trace : in out WisiToken.Trace'Class; Cursor : in 
Parser_Lists.Cursor)
+   is
+      Parser_State : Parser_Lists.Parser_State renames 
Parser_State_Lists.Constant_Reference (Cursor.Ptr);
+   begin
+      Trace.Put (Natural'Image (Parser_State.Label) & " stack: ");
+      Trace.Put_Line (Image (Parser_State.Stack, Trace.Descriptor.all, 
Parser_State.Tree, Depth => 10));
+   end Put_Top_10;
+
+   procedure Prepend_Copy
+     (List   : in out Parser_Lists.List;
+      Cursor : in     Parser_Lists.Cursor'Class)
+   is
+      New_Item : Parser_State;
+   begin
+      List.Parser_Label := List.Parser_Label + 1;
+      declare
+         Item : Parser_State renames Parser_State_Lists.Reference 
(Cursor.Ptr).Element.all;
+         --  We can't do 'Prepend' in the scope of this 'renames';
+         --  that would be tampering with cursors.
+      begin
+         Item.Tree.Set_Flush_False;
+
+         --  We specify all items individually, rather copy Item and then
+         --  override a few, to avoid copying large items like Recover.
+         --  We copy Recover.Enqueue_Count, .Check_Count for unit tests.
+         New_Item :=
+           (Shared_Token           => Item.Shared_Token,
+            Recover_Insert_Delete  => Item.Recover_Insert_Delete,
+            Current_Token          => Item.Current_Token,
+            Inc_Shared_Token       => Item.Inc_Shared_Token,
+            Stack                  => Item.Stack,
+            Tree                   => Item.Tree,
+            Recover                =>
+              (Enqueue_Count       => Item.Recover.Enqueue_Count,
+               Check_Count         => Item.Recover.Check_Count,
+               others              => <>),
+            Resume_Active          => Item.Resume_Active,
+            Resume_Token_Goal      => Item.Resume_Token_Goal,
+            Conflict_During_Resume => Item.Conflict_During_Resume,
+            Zombie_Token_Count     => 0,
+            Errors                 => Item.Errors,
+            Label                  => List.Parser_Label,
+            Verb                   => Item.Verb);
+      end;
+      List.Elements.Prepend (New_Item);
+   end Prepend_Copy;
+
+   procedure Free (Cursor : in out Parser_Lists.Cursor'Class)
+   is
+      Temp : Parser_State_Lists.Cursor := Cursor.Ptr;
+   begin
+      Parser_State_Lists.Next (Cursor.Ptr);
+      Parser_State_Lists.Delete (Cursor.Elements.all, Temp);
+   end Free;
+
+   ----------
+   --  stuff for iterators
+
+   function To_Cursor (Ptr : in Parser_Node_Access) return Cursor
+   is begin
+      return (Ptr.Elements, Ptr.Ptr);
+   end To_Cursor;
+
+   function Constant_Reference
+     (Container : aliased in List'Class;
+      Position  :         in Parser_Node_Access)
+     return Constant_Reference_Type
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Parser_State_Lists.Constant_Reference 
(Position.Ptr).Element);
+   end Constant_Reference;
+
+   function Reference
+     (Container : aliased in out List'Class;
+      Position  :         in     Parser_Node_Access)
+     return State_Reference
+   is
+      pragma Unreferenced (Container);
+   begin
+      return (Element => Parser_State_Lists.Reference (Position.Ptr).Element);
+   end Reference;
+
+   function Persistent_State_Ref (Position : in Parser_Node_Access) return 
State_Access
+   is begin
+      return State_Access (Parser_State_Lists.Persistent_Ref (Position.Ptr));
+   end Persistent_State_Ref;
+
+   type List_Access is access all List;
+
+   type Iterator is new Iterator_Interfaces.Forward_Iterator with record
+      Container : List_Access;
+   end record;
+
+   overriding function First (Object : Iterator) return Parser_Node_Access;
+   overriding function Next
+     (Object   : in Iterator;
+      Position : in Parser_Node_Access)
+     return Parser_Node_Access;
+
+   overriding function First (Object : Iterator) return Parser_Node_Access
+   is begin
+      return (Elements => Object.Container.Elements'Access, Ptr => 
Object.Container.Elements.First);
+   end First;
+
+   overriding function Next
+     (Object   : in Iterator;
+      Position : in Parser_Node_Access)
+     return Parser_Node_Access
+   is
+      pragma Unreferenced (Object);
+   begin
+      return (Position.Elements, Parser_State_Lists.Next (Position.Ptr));
+   end Next;
+
+   function Iterate (Container : aliased in out List) return 
Iterator_Interfaces.Forward_Iterator'Class
+   is begin
+      return Iterator'(Container => Container'Access);
+   end Iterate;
+
+   function Has_Element (Iterator : in Parser_Node_Access) return Boolean
+   is begin
+      return Parser_State_Lists.Has_Element (Iterator.Ptr);
+   end Has_Element;
+
+   function Label (Iterator : in Parser_State) return Natural
+   is begin
+      return Iterator.Label;
+   end Label;
+
+   function Verb (Iterator : in Parser_State) return All_Parse_Action_Verbs
+   is begin
+      return Iterator.Verb;
+   end Verb;
+
+   procedure Set_Verb (Iterator : in out Parser_State; Verb : in 
All_Parse_Action_Verbs)
+   is begin
+      Iterator.Verb := Verb;
+   end Set_Verb;
+
+end WisiToken.Parse.LR.Parser_Lists;
diff --git a/wisitoken-parse-lr-parser_lists.ads 
b/wisitoken-parse-lr-parser_lists.ads
new file mode 100644
index 0000000..6c44892
--- /dev/null
+++ b/wisitoken-parse-lr-parser_lists.ads
@@ -0,0 +1,260 @@
+--  Abstract :
+--
+--  Generalized LR parser state.
+--
+--  Copyright (C) 2014-2015, 2017, 2018 Stephe Leake
+--
+--  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.Iterator_Interfaces;
+with SAL.Gen_Indefinite_Doubly_Linked_Lists;
+with WisiToken.Syntax_Trees;
+package WisiToken.Parse.LR.Parser_Lists is
+
+   type Parser_Stack_Item is record
+      State : Unknown_State_Index     := Unknown_State;
+      Token : Syntax_Trees.Node_Index := Syntax_Trees.Invalid_Node_Index;
+   end record;
+
+   package Parser_Stacks is new SAL.Gen_Unbounded_Definite_Stacks 
(Parser_Stack_Item);
+
+   function Parser_Stack_Image
+     (Stack      : in Parser_Stacks.Stack;
+      Descriptor : in WisiToken.Descriptor;
+      Tree       : in Syntax_Trees.Tree;
+      Depth      : in SAL.Base_Peek_Type := 0)
+     return String;
+   --  If Depth = 0, put all of Stack. Otherwise put Min (Depth,
+   --  Stack.Depth) items.
+   --
+   --  Unique name for calling from debugger
+
+   function Image
+     (Stack      : in Parser_Stacks.Stack;
+      Descriptor : in WisiToken.Descriptor;
+      Tree       : in Syntax_Trees.Tree;
+      Depth      : in SAL.Base_Peek_Type := 0)
+     return String renames Parser_Stack_Image;
+
+   type Base_Parser_State is tagged
+   record
+      --  Visible components for direct access
+
+      Shared_Token : Base_Token_Index := Invalid_Token_Index;
+      --  Last token read from Shared_Parser.Terminals.
+
+      Recover_Insert_Delete : Config_Op_Queues.Queue;
+      --  Tokens in that were inserted during error recovery, or should be
+      --  deleted/skipped when read. Contains only Insert and Delete ops.
+
+      Current_Token : Syntax_Trees.Node_Index := 
Syntax_Trees.Invalid_Node_Index;
+      --  Current terminal, in Tree
+
+      Inc_Shared_Token : Boolean := True;
+
+      Stack : Parser_Stacks.Stack;
+      --  There is no need to use a branched stack; max stack length is
+      --  proportional to source text nesting depth, not source text length.
+
+      Tree : Syntax_Trees.Tree;
+      --  We use a branched tree to avoid copying large trees for each
+      --  spawned parser; tree size is proportional to source text size. In
+      --  normal parsing, parallel parsers are short-lived; they each process
+      --  a few tokens, to resolve a grammar conflict.
+      --
+      --  When there is only one parser, tree nodes are written directly to
+      --  the shared tree (via the branched tree, with Flush => True).
+      --
+      --  When there is more than one, tree nodes are written to the
+      --  branched tree. Then when all but one parsers are terminated, the
+      --  remaining branched tree is flushed into the shared tree.
+
+      Recover : aliased LR.McKenzie_Data := (others => <>);
+
+      Zombie_Token_Count : Base_Token_Index := 0;
+      --  If Zombie_Token_Count > 0, this parser has errored, but is waiting
+      --  to see if other parsers do also.
+
+      Resume_Active          : Boolean          := False;
+      Resume_Token_Goal      : Base_Token_Index := Invalid_Token_Index;
+      Conflict_During_Resume : Boolean          := False;
+      --  Resume is complete for this parser Shared_Token reaches this
+      --  Resume_Token_Goal.
+
+      Errors : Parse_Error_Lists.List;
+   end record;
+
+   type Parser_State is new Base_Parser_State with private;
+   type State_Access is access all Parser_State;
+
+   type List is tagged private
+   with
+     Constant_Indexing => Constant_Reference,
+     Variable_Indexing => Reference,
+     Default_Iterator  => Iterate,
+     Iterator_Element  => Parser_State;
+
+   function New_List (Shared_Tree : in Syntax_Trees.Base_Tree_Access) return 
List;
+
+   function Last_Label (List : in Parser_Lists.List) return Natural;
+
+   function Count (List : in Parser_Lists.List) return SAL.Base_Peek_Type;
+
+   type Cursor is tagged private;
+
+   function First (List : aliased in out Parser_Lists.List'Class) return 
Cursor;
+   procedure Next (Cursor : in out Parser_Lists.Cursor);
+   function Is_Done (Cursor : in Parser_Lists.Cursor) return Boolean;
+   function Has_Element (Cursor : in Parser_Lists.Cursor) return Boolean is 
(not Is_Done (Cursor));
+
+   function Active_Parser_Count (Cursor : in Parser_Lists.Cursor) return 
SAL.Base_Peek_Type;
+
+   function Label (Cursor : in Parser_Lists.Cursor) return Natural;
+   function Total_Recover_Cost (Cursor : in Parser_Lists.Cursor) return 
Integer;
+   function Max_Recover_Ops_Length (Cursor : in Parser_Lists.Cursor) return 
Ada.Containers.Count_Type;
+
+   procedure Set_Verb (Cursor : in Parser_Lists.Cursor; Verb : in 
All_Parse_Action_Verbs);
+   function Verb (Cursor : in Parser_Lists.Cursor) return 
All_Parse_Action_Verbs;
+
+   procedure Terminate_Parser
+     (Parsers : in out List;
+      Current : in out Cursor'Class;
+      Message : in     String;
+      Trace   : in out WisiToken.Trace'Class);
+   --  Terminate Current. Current is set to no element.
+
+   procedure Duplicate_State
+     (Parsers : in out List;
+      Current : in out Cursor'Class;
+      Trace   : in out WisiToken.Trace'Class);
+   --  If any other parser in Parsers has a stack equivalent to Current,
+   --  Terminate one of them. Current is either unchanged, or advanced to
+   --  the next parser.
+
+   type State_Reference (Element : not null access Parser_State) is null record
+   with Implicit_Dereference => Element;
+
+   function State_Ref (Position : in Cursor) return State_Reference;
+   --  Direct access to visible components of Parser_State
+
+   function First_State_Ref (List : in Parser_Lists.List'Class) return 
State_Reference;
+   --  Direct access to visible components of first parser's Parser_State
+
+   type Constant_State_Reference (Element : not null access constant 
Parser_State) is null record
+   with Implicit_Dereference => Element;
+
+   function First_Constant_State_Ref (List : in Parser_Lists.List'Class) 
return Constant_State_Reference;
+   --  Direct access to visible components of first parser's Parser_State
+
+   procedure Put_Top_10 (Trace : in out WisiToken.Trace'Class; Cursor : in 
Parser_Lists.Cursor);
+   --  Put image of top 10 stack items to Trace.
+
+   procedure Prepend_Copy (List : in out Parser_Lists.List; Cursor : in 
Parser_Lists.Cursor'Class);
+   --  Copy parser at Cursor, prepend to current list. New copy will not
+   --  appear in Cursor.Next ...; it is accessible as First (List).
+   --
+   --  Copy.Recover is set to default.
+
+   procedure Free (Cursor : in out Parser_Lists.Cursor'Class);
+   --  Delete the Cursor parser. It will not appear in future
+   --  iterations. On return, Cursor points to next parser, or none.
+
+   ----------
+   --  Stuff for iterators, to allow
+   --  'for Parser of Parsers loop'
+   --  'for I in Parsers.Iterate loop'
+   --
+   --  requires Parser_State to be not an incomplete type.
+
+   --  We'd like to use Cursor here, but we want that to be tagged, to
+   --  allow 'Cursor.operation' syntax, and the requirements of
+   --  iterators prevent a tagged iterator type (two tagged types on
+   --  First in this package body). So we use Parser_Node_Access as
+   --  the iterator type for Iterators, and typical usage is:
+   --
+   --  for I in Parsers.Iterate loop
+   --     declare
+   --        Cursor : Parser_Lists.Cursor renames To_Cursor (Parsers, I);
+   --     begin
+   --        Cursor.<cursor operation>
+   --
+   --        ... Parsers (I).<visible parser_state component> ...
+   --     end;
+   --  end loop;
+   --
+   --  or:
+   --  for Current_Parser of Parsers loop
+   --     ... Current_Parser.<visible parser_state component> ...
+   --  end loop;
+
+   type Parser_Node_Access is private;
+
+   function To_Cursor (Ptr : in Parser_Node_Access) return Cursor;
+
+   type Constant_Reference_Type (Element : not null access constant 
Parser_State) is null record
+   with Implicit_Dereference => Element;
+
+   function Constant_Reference
+     (Container : aliased in List'Class;
+      Position  :         in Parser_Node_Access)
+     return Constant_Reference_Type;
+
+   function Reference
+     (Container : aliased in out List'Class;
+      Position  :         in     Parser_Node_Access)
+     return State_Reference;
+
+   function Persistent_State_Ref (Position : in Parser_Node_Access) return 
State_Access;
+
+   function Has_Element (Iterator : in Parser_Node_Access) return Boolean;
+
+   package Iterator_Interfaces is new Ada.Iterator_Interfaces 
(Parser_Node_Access, Has_Element);
+
+   function Iterate (Container : aliased in out List) return 
Iterator_Interfaces.Forward_Iterator'Class;
+
+   --  Access to some private Parser_State components
+
+   function Label (Iterator : in Parser_State) return Natural;
+   procedure Set_Verb (Iterator : in out Parser_State; Verb : in 
All_Parse_Action_Verbs);
+   function Verb (Iterator : in Parser_State) return All_Parse_Action_Verbs;
+
+private
+
+   type Parser_State is new Base_Parser_State with record
+      Label : Natural; -- for debugging/verbosity
+
+      Verb : All_Parse_Action_Verbs := Shift; -- current action to perform
+   end record;
+
+   package Parser_State_Lists is new SAL.Gen_Indefinite_Doubly_Linked_Lists 
(Parser_State);
+
+   type List is tagged record
+      Elements     : aliased Parser_State_Lists.List;
+      Parser_Label : Natural; -- label of last added parser.
+   end record;
+
+   type Cursor is tagged record
+      Elements : access Parser_State_Lists.List;
+      Ptr      : Parser_State_Lists.Cursor;
+   end record;
+
+   type Parser_Node_Access is record
+      Elements : access Parser_State_Lists.List;
+      Ptr      : Parser_State_Lists.Cursor;
+   end record;
+
+end WisiToken.Parse.LR.Parser_Lists;
diff --git a/wisitoken-parse-lr-parser_no_recover.adb 
b/wisitoken-parse-lr-parser_no_recover.adb
new file mode 100644
index 0000000..c82544a
--- /dev/null
+++ b/wisitoken-parse-lr-parser_no_recover.adb
@@ -0,0 +1,492 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2002 - 2005, 2008 - 2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+package body WisiToken.Parse.LR.Parser_No_Recover is
+
+   procedure Reduce_Stack_1
+     (Current_Parser : in     Parser_Lists.Cursor;
+      Action         : in     Reduce_Action_Rec;
+      Nonterm        :    out WisiToken.Syntax_Trees.Valid_Node_Index;
+      Trace          : in out WisiToken.Trace'Class)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      Parser_State  : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref.Element.all;
+      Children_Tree : Syntax_Trees.Valid_Node_Index_Array (1 .. 
SAL.Base_Peek_Type (Action.Token_Count));
+      --  for Set_Children.
+   begin
+      for I in reverse Children_Tree'Range loop
+         Children_Tree (I) := Parser_State.Stack.Pop.Token;
+      end loop;
+
+      Nonterm := Parser_State.Tree.Add_Nonterm
+        (Action.Production, Children_Tree, Action.Action, Default_Virtual => 
False);
+      --  Computes Nonterm.Byte_Region
+
+      if Trace_Parse > Detail then
+         Trace.Put_Line (Parser_State.Tree.Image (Nonterm, 
Trace.Descriptor.all, Include_Children => True));
+      end if;
+   end Reduce_Stack_1;
+
+   procedure Do_Action
+     (Action         : in Parse_Action_Rec;
+      Current_Parser : in Parser_Lists.Cursor;
+      Shared_Parser  : in Parser)
+   is
+      Parser_State : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref;
+      Trace        : WisiToken.Trace'Class renames Shared_Parser.Trace.all;
+      Nonterm      : WisiToken.Syntax_Trees.Valid_Node_Index;
+   begin
+      if Trace_Parse > Detail then
+         Trace.Put
+           (Integer'Image (Current_Parser.Label) & ": " &
+              Trimmed_Image (Parser_State.Stack.Peek.State) & ": " &
+              Parser_State.Tree.Image (Parser_State.Current_Token, 
Trace.Descriptor.all) & " : ");
+         Put (Trace, Action);
+         Trace.New_Line;
+      end if;
+
+      case Action.Verb is
+      when Shift =>
+         Current_Parser.Set_Verb (Shift);
+         Parser_State.Stack.Push ((Action.State, Parser_State.Current_Token));
+         Parser_State.Tree.Set_State (Parser_State.Current_Token, 
Action.State);
+
+      when Reduce =>
+         Current_Parser.Set_Verb (Reduce);
+
+         Reduce_Stack_1 (Current_Parser, Action, Nonterm, Trace);
+
+         Parser_State.Stack.Push
+           ((State    => Goto_For
+               (Table => Shared_Parser.Table.all,
+                State => Parser_State.Stack (1).State,
+                ID    => Action.Production.LHS),
+             Token    => Nonterm));
+
+         Parser_State.Tree.Set_State (Nonterm, Parser_State.Stack (1).State);
+
+         if Trace_Parse > Detail then
+            Trace.Put_Line (" ... goto state " & Trimmed_Image 
(Parser_State.Stack.Peek.State));
+         end if;
+
+      when Accept_It =>
+         Current_Parser.Set_Verb (Accept_It);
+         Reduce_Stack_1
+           (Current_Parser,
+            (Reduce, Action.Production, Action.Action, Action.Check, 
Action.Token_Count),
+            Nonterm, Trace);
+
+         Parser_State.Tree.Set_Root (Nonterm);
+
+      when Error =>
+         Current_Parser.Set_Verb (Action.Verb);
+
+         --  We don't raise Syntax_Error here; another parser may be able to
+         --  continue.
+
+         declare
+            Expecting : constant Token_ID_Set := LR.Expecting
+              (Shared_Parser.Table.all, 
Current_Parser.State_Ref.Stack.Peek.State);
+         begin
+            Parser_State.Errors.Append
+              ((Label          => LR.Action,
+                First_Terminal => Trace.Descriptor.First_Terminal,
+                Last_Terminal  => Trace.Descriptor.Last_Terminal,
+                Error_Token    => Parser_State.Current_Token,
+                Expecting      => Expecting,
+                Recover        => (others => <>)));
+
+            if Trace_Parse > Outline then
+               Put
+                 (Trace,
+                  Integer'Image (Current_Parser.Label) & ": expecting: " &
+                    Image (Expecting, Trace.Descriptor.all));
+               Trace.New_Line;
+            end if;
+         end;
+      end case;
+   end Do_Action;
+
+   --  Return the type of parser cycle to execute.
+   --
+   --  Accept : all Parsers.Verb return Accept - done parsing.
+   --
+   --  Shift : some Parsers.Verb return Shift.
+   --
+   --  Reduce : some Parsers.Verb return Reduce.
+   --
+   --  Error : all Parsers.Verb return Error.
+   procedure Parse_Verb
+     (Shared_Parser : in out Parser;
+      Verb          :    out All_Parse_Action_Verbs)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      Shift_Count  : SAL.Base_Peek_Type := 0;
+      Accept_Count : SAL.Base_Peek_Type := 0;
+      Error_Count  : SAL.Base_Peek_Type := 0;
+   begin
+      for Parser_State of Shared_Parser.Parsers loop
+         case Parser_State.Verb is
+         when Shift =>
+            Shift_Count := Shift_Count + 1;
+
+         when Reduce =>
+            Verb := Reduce;
+            return;
+
+         when Accept_It =>
+            Accept_Count := Accept_Count + 1;
+
+         when Error =>
+            Error_Count := Error_Count + 1;
+
+         when Pause | Shift_Recover =>
+            --  This is parser_no_recover
+            raise SAL.Programmer_Error;
+         end case;
+      end loop;
+
+      if Shared_Parser.Parsers.Count = Accept_Count then
+         Verb := Accept_It;
+
+      elsif Shared_Parser.Parsers.Count = Error_Count then
+         Verb := Error;
+
+      elsif Shift_Count > 0 then
+         Verb := Shift;
+
+      else
+         raise SAL.Programmer_Error;
+      end if;
+   end Parse_Verb;
+
+   ----------
+   --  Public subprograms, declaration order
+
+   overriding procedure Finalize (Object : in out Parser)
+   is begin
+      Free_Table (Object.Table);
+   end Finalize;
+
+   procedure New_Parser
+     (Parser               :    out          LR.Parser_No_Recover.Parser;
+      Trace                : not null access WisiToken.Trace'Class;
+      Lexer                : in              WisiToken.Lexer.Handle;
+      Table                : in              Parse_Table_Ptr;
+      User_Data            : in              
WisiToken.Syntax_Trees.User_Data_Access;
+      Max_Parallel         : in              SAL.Base_Peek_Type := 
Default_Max_Parallel;
+      First_Parser_Label   : in              Integer            := 1;
+      Terminate_Same_State : in              Boolean            := True)
+   is
+      use all type Syntax_Trees.User_Data_Access;
+   begin
+      Parser.Lexer                := Lexer;
+      Parser.Trace                := Trace;
+      Parser.Table                := Table;
+      Parser.User_Data            := User_Data;
+      Parser.Max_Parallel         := Max_Parallel;
+      Parser.First_Parser_Label   := First_Parser_Label;
+      Parser.Terminate_Same_State := Terminate_Same_State;
+
+      if User_Data /= null then
+         User_Data.Set_Lexer_Terminals (Lexer, 
Parser.Terminals'Unchecked_Access);
+      end if;
+   end New_Parser;
+
+   overriding procedure Parse (Shared_Parser : aliased in out Parser)
+   is
+      use all type Syntax_Trees.User_Data_Access;
+      use all type SAL.Base_Peek_Type;
+
+      Trace : WisiToken.Trace'Class renames Shared_Parser.Trace.all;
+
+      Current_Verb   : All_Parse_Action_Verbs;
+      Current_Parser : Parser_Lists.Cursor;
+      Action         : Parse_Action_Node_Ptr;
+
+      procedure Check_Error (Check_Parser : in out Parser_Lists.Cursor)
+      is begin
+         if Check_Parser.Verb = Error then
+            --  This parser errored on last input. This is how grammar 
conflicts
+            --  are resolved when the input text is valid, so we terminate this
+            --  parser.
+
+            if Shared_Parser.Parsers.Count = 1 then
+               raise Syntax_Error;
+            else
+               Shared_Parser.Parsers.Terminate_Parser (Check_Parser, "", 
Shared_Parser.Trace.all);
+            end if;
+         else
+            Check_Parser.Next;
+         end if;
+      end Check_Error;
+
+   begin
+      --  The user must call Lexer.Reset_* to set the input text.
+      Shared_Parser.Lex_All;
+
+      if Shared_Parser.User_Data /= null then
+         Shared_Parser.User_Data.Reset;
+      end if;
+      Shared_Parser.Shared_Tree.Clear;
+
+      Shared_Parser.Parsers := Parser_Lists.New_List
+        (Shared_Tree => Shared_Parser.Shared_Tree'Unchecked_Access);
+
+      Shared_Parser.Parsers.First.State_Ref.Stack.Push 
((Shared_Parser.Table.State_First, others => <>));
+
+      Main_Loop :
+      loop
+         --  exit on Accept_It action or syntax error.
+
+         Parse_Verb (Shared_Parser, Current_Verb);
+
+         case Current_Verb is
+         when Shift =>
+            --  All parsers just shifted a token; get the next token
+
+            for Parser_State of Shared_Parser.Parsers loop
+               Parser_State.Shared_Token  := Parser_State.Shared_Token + 1;
+               Parser_State.Current_Token := Parser_State.Tree.Add_Terminal
+                 (Parser_State.Shared_Token, Shared_Parser.Terminals);
+            end loop;
+
+         when Accept_It =>
+            --  All parsers accepted.
+            declare
+               Count : constant SAL.Base_Peek_Type := 
Shared_Parser.Parsers.Count;
+            begin
+               if Count = 1 then
+                  --  Nothing more to do
+                  if Trace_Parse > Outline then
+                     Trace.Put_Line (Integer'Image 
(Shared_Parser.Parsers.First.Label) & ": succeed");
+                  end if;
+                  exit Main_Loop;
+
+               else
+                  --  More than one parser is active; ambiguous parse.
+                  declare
+                     Token : Base_Token renames Shared_Parser.Terminals 
(Shared_Parser.Terminals.Last_Index);
+                  begin
+                     raise WisiToken.Parse_Error with Error_Message
+                       (Shared_Parser.Lexer.File_Name, Token.Line, 
Token.Column,
+                        "Ambiguous parse:" & SAL.Base_Peek_Type'Image (Count) 
& " parsers active.");
+                  end;
+               end if;
+            end;
+
+         when Reduce =>
+            null;
+
+         when Error =>
+            --  All parsers errored; terminate with error. Semantic_State has 
all
+            --  the required info (recorded by Error in Do_Action), so we just
+            --  raise the exception.
+            raise Syntax_Error;
+
+         when Pause | Shift_Recover =>
+            --  This is parser_no_recover
+            raise SAL.Programmer_Error;
+         end case;
+
+         --  We don't use 'for Parser_State of Parsers loop' here,
+         --  because terminate on error and spawn on conflict require
+         --  changing the parser list.
+         Current_Parser := Shared_Parser.Parsers.First;
+         loop
+            exit when Current_Parser.Is_Done;
+
+            if Shared_Parser.Terminate_Same_State and
+              Current_Verb = Shift
+            then
+               Shared_Parser.Parsers.Duplicate_State (Current_Parser, 
Shared_Parser.Trace.all);
+               --  If Duplicate_State terminated Current_Parser, 
Current_Parser now
+               --  points to the next parser. Otherwise it is unchanged.
+            end if;
+
+            exit when Current_Parser.Is_Done;
+
+            if Trace_Parse > Extra then
+               Trace.Put_Line
+                 ("current_verb: " & Parse_Action_Verbs'Image (Current_Verb) &
+                    "," & Integer'Image (Current_Parser.Label) &
+                    ".verb: " & Parse_Action_Verbs'Image 
(Current_Parser.Verb));
+            end if;
+
+            --  Each branch of the following 'if' calls either 
Current_Parser.Free
+            --  (which advances to the next parser) or Current_Parser.Next.
+
+            if Current_Parser.Verb = Current_Verb then
+               if Trace_Parse > Extra then
+                  Parser_Lists.Put_Top_10 (Trace, Current_Parser);
+               end if;
+
+               declare
+                  State : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref.Element.all;
+               begin
+                  Action := Action_For
+                    (Table => Shared_Parser.Table.all,
+                     State => State.Stack.Peek.State,
+                     ID    => State.Tree.ID (State.Current_Token));
+               end;
+
+               if Action.Next /= null then
+                  --  Conflict; spawn a new parser (before modifying 
Current_Parser
+                  --  stack).
+
+                  if Shared_Parser.Parsers.Count = Shared_Parser.Max_Parallel 
then
+                     declare
+                        Parser_State : Parser_Lists.Parser_State renames 
Current_Parser.State_Ref;
+                        Token : Base_Token renames Shared_Parser.Terminals 
(Parser_State.Shared_Token);
+                     begin
+                        raise WisiToken.Parse_Error with Error_Message
+                          (Shared_Parser.Lexer.File_Name, Token.Line, 
Token.Column,
+                           ": too many parallel parsers required in grammar 
state" &
+                             State_Index'Image (Parser_State.Stack.Peek.State) 
&
+                             "; simplify grammar, or increase max-parallel (" &
+                             SAL.Base_Peek_Type'Image 
(Shared_Parser.Max_Parallel) & ")");
+                     end;
+                  else
+                     if Trace_Parse > Outline then
+                        Trace.Put_Line
+                          ("spawn parser from " & Trimmed_Image 
(Current_Parser.Label) &
+                             " (" & Trimmed_Image (1 + Integer 
(Shared_Parser.Parsers.Count)) & " active)");
+                     end if;
+
+                     Shared_Parser.Parsers.Prepend_Copy (Current_Parser);
+                     Do_Action (Action.Next.Item, Shared_Parser.Parsers.First, 
Shared_Parser);
+
+                     declare
+                        Temp : Parser_Lists.Cursor := 
Shared_Parser.Parsers.First;
+                     begin
+                        Check_Error (Temp);
+                     end;
+                  end if;
+               end if;
+
+               Do_Action (Action.Item, Current_Parser, Shared_Parser);
+               Check_Error (Current_Parser);
+
+            else
+               --  Current parser is waiting for others to catch up
+               Current_Parser.Next;
+            end if;
+         end loop;
+      end loop Main_Loop;
+
+      --  We don't raise Syntax_Error for lexer errors, since they are all
+      --  recovered, either by inserting a quote, or by ignoring the
+      --  character.
+   end Parse;
+
+   overriding procedure Execute_Actions (Parser : in out 
LR.Parser_No_Recover.Parser)
+   is
+      use all type Syntax_Trees.User_Data_Access;
+
+      procedure Process_Node
+        (Tree : in out Syntax_Trees.Tree;
+         Node : in     Syntax_Trees.Valid_Node_Index)
+      is
+         use all type Syntax_Trees.Node_Label;
+      begin
+         if Tree.Label (Node) /= Nonterm then
+            return;
+         end if;
+
+         declare
+            use all type Syntax_Trees.Semantic_Action;
+            Tree_Children : constant Syntax_Trees.Valid_Node_Index_Array := 
Tree.Children (Node);
+         begin
+            Parser.User_Data.Reduce (Tree, Node, Tree_Children);
+
+            if Tree.Action (Node) /= null then
+               Tree.Action (Node) (Parser.User_Data.all, Tree, Node, 
Tree_Children);
+            end if;
+         end;
+      end Process_Node;
+
+   begin
+      if Parser.User_Data /= null then
+         for Parser_State of Parser.Parsers loop
+            Parser_State.Tree.Process_Tree (Process_Node'Access);
+         end loop;
+      end if;
+   end Execute_Actions;
+
+   overriding function Any_Errors (Parser : in LR.Parser_No_Recover.Parser) 
return Boolean
+   is
+      use all type SAL.Base_Peek_Type;
+      use all type Ada.Containers.Count_Type;
+      Parser_State : Parser_Lists.Parser_State renames 
Parser.Parsers.First_Constant_State_Ref;
+   begin
+      pragma Assert (Parser_State.Tree.Flushed);
+      return Parser.Parsers.Count > 1 or Parser_State.Errors.Length > 0 or 
Parser.Lexer.Errors.Length > 0;
+   end Any_Errors;
+
+   overriding procedure Put_Errors (Parser : in LR.Parser_No_Recover.Parser)
+   is
+      use Ada.Text_IO;
+
+      Parser_State : Parser_Lists.Parser_State renames 
Parser.Parsers.First_Constant_State_Ref;
+      Descriptor   : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+   begin
+      for Item of Parser.Lexer.Errors loop
+         Put_Line
+           (Current_Error,
+            Parser.Lexer.File_Name & ":0:0: lexer unrecognized character at" & 
Buffer_Pos'Image (Item.Char_Pos));
+      end loop;
+
+      for Item of Parser_State.Errors loop
+         case Item.Label is
+         when Action =>
+            declare
+               Token : Base_Token renames Parser.Terminals 
(Parser_State.Tree.Min_Terminal_Index (Item.Error_Token));
+            begin
+               Put_Line
+                 (Current_Error,
+                  Error_Message
+                    (Parser.Lexer.File_Name, Token.Line, Token.Column,
+                     "syntax error: expecting " & Image (Item.Expecting, 
Descriptor) &
+                       ", found '" & Parser.Lexer.Buffer_Text 
(Token.Byte_Region) & "'"));
+            end;
+
+         when Check =>
+            null;
+
+         when Message =>
+            Put_Line (Current_Error, -Item.Msg);
+         end case;
+
+      end loop;
+   end Put_Errors;
+
+end WisiToken.Parse.LR.Parser_No_Recover;
diff --git a/wisitoken-parse-lr-parser_no_recover.ads 
b/wisitoken-parse-lr-parser_no_recover.ads
new file mode 100644
index 0000000..17d1766
--- /dev/null
+++ b/wisitoken-parse-lr-parser_no_recover.ads
@@ -0,0 +1,84 @@
+--  Abstract :
+--
+--  A generalized LR parser, with no error recovery, no semantic checks.
+--
+--  This allows wisi-generate (which uses the generated wisi_grammar)
+--  to not depend on wisitoken-lr-mckenzie_recover, so editing that
+--  does not cause everything to be regenerated/compiled.
+--
+--  Copyright (C) 2002, 2003, 2009, 2010, 2013-2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  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 WisiToken.Lexer;
+with WisiToken.Parse.LR.Parser_Lists;
+with WisiToken.Syntax_Trees;
+package WisiToken.Parse.LR.Parser_No_Recover is
+
+   Default_Max_Parallel : constant := 15;
+
+   type Parser is new WisiToken.Parse.Base_Parser with record
+      Table       : Parse_Table_Ptr;
+      Shared_Tree : aliased Syntax_Trees.Base_Tree;
+      --  Each parser has its own branched syntax tree, all branched from
+      --  this tree.
+      --
+      --  See WisiToken.LR.Parser_Lists Parser_State for more discussion of
+      --  Shared_Tree.
+
+      Parsers : aliased Parser_Lists.List;
+
+      Max_Parallel         : SAL.Base_Peek_Type;
+      First_Parser_Label   : Integer;
+      Terminate_Same_State : Boolean;
+   end record;
+
+   overriding procedure Finalize (Object : in out LR.Parser_No_Recover.Parser);
+   --  Deep free Object.Table.
+
+   procedure New_Parser
+     (Parser               :    out          LR.Parser_No_Recover.Parser;
+      Trace                : not null access WisiToken.Trace'Class;
+      Lexer                : in              WisiToken.Lexer.Handle;
+      Table                : in              Parse_Table_Ptr;
+      User_Data            : in              Syntax_Trees.User_Data_Access;
+      Max_Parallel         : in              SAL.Base_Peek_Type := 
Default_Max_Parallel;
+      First_Parser_Label   : in              Integer            := 1;
+      Terminate_Same_State : in              Boolean            := True);
+
+   overriding procedure Parse (Shared_Parser : aliased in out 
LR.Parser_No_Recover.Parser);
+   --  Attempt a parse. Calls Parser.Lexer.Reset, runs lexer to end of
+   --  input setting Shared_Parser.Terminals, then parses tokens.
+   --
+   --  If a parse error is encountered, raises Syntax_Error.
+   --  Parser.Lexer_Errors and Parsers(*).Errors contain information
+   --  about the errors.
+   --
+   --  For other errors, raises Parse_Error with an appropriate error
+   --  message.
+
+   overriding function Any_Errors (Parser : in LR.Parser_No_Recover.Parser) 
return Boolean;
+
+   overriding procedure Put_Errors (Parser : in LR.Parser_No_Recover.Parser);
+   --  Put user-friendly error messages from the parse to
+   --  Ada.Text_IO.Current_Error.
+
+   overriding procedure Execute_Actions (Parser : in out 
LR.Parser_No_Recover.Parser);
+   --  Execute the grammar actions in Parser.
+
+end WisiToken.Parse.LR.Parser_No_Recover;
diff --git a/wisitoken-parse-lr.adb b/wisitoken-parse-lr.adb
new file mode 100644
index 0000000..307985c
--- /dev/null
+++ b/wisitoken-parse-lr.adb
@@ -0,0 +1,856 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2013-2015, 2017, 2018 Stephe Leake
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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 (GPL);
+
+with Ada.Exceptions;
+with Ada.Strings.Maps;
+with Ada.Text_IO;
+package body WisiToken.Parse.LR is
+
+   ----------
+   --  Public subprograms, declaration order
+
+   function Image (Item : in Parse_Action_Rec; Descriptor : in 
WisiToken.Descriptor) return String
+   is
+      use Ada.Containers;
+   begin
+      case Item.Verb is
+      when Shift =>
+         return "(Shift," & State_Index'Image (Item.State) & ")";
+
+      when Reduce =>
+         return "(Reduce," & Count_Type'Image (Item.Token_Count) & ", " &
+           Image (Item.Production.LHS, Descriptor) & "," & Trimmed_Image 
(Item.Production.RHS) & ")";
+      when Accept_It =>
+         return "(Accept It)";
+      when Error =>
+         return "(Error)";
+      end case;
+   end Image;
+
+   procedure Put (Trace : in out WisiToken.Trace'Class; Item : in 
Parse_Action_Rec)
+   is
+      use Ada.Containers;
+   begin
+      case Item.Verb is
+      when Shift =>
+         Trace.Put ("shift and goto state" & State_Index'Image (Item.State));
+
+      when Reduce =>
+         Trace.Put
+           ("reduce" & Count_Type'Image (Item.Token_Count) & " tokens to " &
+              Image (Item.Production.LHS, Trace.Descriptor.all));
+      when Accept_It =>
+         Trace.Put ("accept it");
+      when Error =>
+         Trace.Put ("ERROR");
+      end case;
+   end Put;
+
+   function Equal (Left, Right : in Parse_Action_Rec) return Boolean
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Left.Verb = Right.Verb then
+         case Left.Verb is
+         when Shift =>
+            return Left.State = Right.State;
+
+         when Reduce | Accept_It =>
+            return Left.Production.LHS = Right.Production.LHS and 
Left.Token_Count = Right.Token_Count;
+
+         when Error =>
+            return True;
+         end case;
+      else
+         return False;
+      end if;
+   end Equal;
+
+   procedure Add
+     (List   : in out Action_Node_Ptr;
+      Symbol : in     Token_ID;
+      Action : in     Parse_Action_Rec)
+   is
+      New_Item : constant Action_Node_Ptr := new Action_Node'(Symbol, new 
Parse_Action_Node'(Action, null), null);
+      I        : Action_Node_Ptr          := List;
+   begin
+      if I = null then
+         List := New_Item;
+      else
+         if List.Symbol > Symbol then
+            New_Item.Next := List;
+            List          := New_Item;
+         else
+            if List.Next = null then
+               List.Next := New_Item;
+            else
+               I := List;
+               loop
+                  exit when I.Next = null or else I.Next.Symbol > Symbol;
+                  I := I.Next;
+               end loop;
+               New_Item.Next := I.Next;
+               I.Next        := New_Item;
+            end if;
+         end if;
+      end if;
+   end Add;
+
+   function Symbol (List : in Goto_Node_Ptr) return Token_ID
+   is begin
+      return List.Symbol;
+   end Symbol;
+
+   function State (List : in Goto_Node_Ptr) return State_Index
+   is begin
+      return List.State;
+   end State;
+
+   function Next (List : in Goto_Node_Ptr) return Goto_Node_Ptr
+   is begin
+      return List.Next;
+   end Next;
+
+   function Compare_Minimal_Action (Left, Right : in Minimal_Action) return 
SAL.Compare_Result
+   is begin
+      if Left.Verb > Right.Verb then
+         return SAL.Greater;
+      elsif Left.Verb < Right.Verb then
+         return SAL.Less;
+      else
+         case Left.Verb is
+         when Shift =>
+            if Left.ID > Right.ID then
+               return SAL.Greater;
+            elsif Left.ID < Right.ID then
+               return SAL.Less;
+            else
+               return SAL.Equal;
+            end if;
+         when Reduce =>
+            if Left.Nonterm > Right.Nonterm then
+               return SAL.Greater;
+            elsif Left.Nonterm < Right.Nonterm then
+               return SAL.Less;
+            else
+               return SAL.Equal;
+            end if;
+         end case;
+      end if;
+   end Compare_Minimal_Action;
+
+   function Strict_Image (Item : in Minimal_Action) return String
+   is begin
+      case Item.Verb is
+      when Shift =>
+         return "(Shift," & Token_ID'Image (Item.ID) & "," & State_Index'Image 
(Item.State) & ")";
+      when Reduce =>
+         return "(Reduce," & Token_ID'Image (Item.Nonterm) & "," &
+           Ada.Containers.Count_Type'Image (Item.Token_Count) & ")";
+      end case;
+   end Strict_Image;
+
+   procedure Set_Minimal_Action (List : out Minimal_Action_Lists.List; Actions 
: in Minimal_Action_Array)
+   is begin
+      for Action of Actions loop
+         List.Insert (Action);
+      end loop;
+   end Set_Minimal_Action;
+
+   function First (State : in Parse_State) return Action_List_Iterator
+   is begin
+      return Iter : Action_List_Iterator := (Node => State.Action_List, Item 
=> null) do
+         loop
+            exit when Iter.Node = null;
+            Iter.Item := Iter.Node.Action;
+            exit when Iter.Item /= null;
+            Iter.Node := Iter.Node.Next;
+         end loop;
+      end return;
+   end First;
+
+   function Is_Done (Iter : in Action_List_Iterator) return Boolean
+   is begin
+      return Iter.Node = null;
+   end Is_Done;
+
+   procedure Next (Iter : in out Action_List_Iterator)
+   is begin
+      if Iter.Node = null then
+         return;
+      end if;
+
+      if Iter.Item.Next = null then
+         loop
+            Iter.Node := Iter.Node.Next;
+            exit when Iter.Node = null;
+            Iter.Item := Iter.Node.Action;
+            exit when Iter.Item /= null;
+         end loop;
+      else
+         Iter.Item := Iter.Item.Next; -- a conflict
+      end if;
+   end Next;
+
+   function Symbol (Iter : in Action_List_Iterator) return Token_ID
+   is begin
+      return Iter.Node.Symbol;
+   end Symbol;
+
+   function Action (Iter : in Action_List_Iterator) return Parse_Action_Rec
+   is begin
+      return Iter.Item.Item;
+   end Action;
+
+   procedure Add_Action
+     (State       : in out LR.Parse_State;
+      Symbol      : in     Token_ID;
+      State_Index : in     WisiToken.State_Index)
+   is
+      Action   : constant Parse_Action_Rec := (Shift, State_Index);
+      New_Node : constant Action_Node_Ptr  := new Action_Node'(Symbol, new 
Parse_Action_Node'(Action, null), null);
+      Node     : Action_Node_Ptr;
+   begin
+      if State.Action_List = null then
+         State.Action_List := New_Node;
+      else
+         Node := State.Action_List;
+         loop
+            exit when Node.Next = null;
+            Node := Node.Next;
+         end loop;
+         Node.Next := New_Node;
+      end if;
+   end Add_Action;
+
+   procedure Add_Action
+     (State           : in out LR.Parse_State;
+      Symbol          : in     Token_ID;
+      Verb            : in     LR.Parse_Action_Verbs;
+      Production      : in     Production_ID;
+      RHS_Token_Count : in     Ada.Containers.Count_Type;
+      Semantic_Action : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check  : in     Semantic_Checks.Semantic_Check)
+   is
+      Action   : Parse_Action_Rec;
+      New_Node : Action_Node_Ptr;
+      Node     : Action_Node_Ptr;
+   begin
+      case Verb is
+      when Reduce =>
+         Action := (Reduce, Production, Semantic_Action, Semantic_Check, 
RHS_Token_Count);
+      when Accept_It =>
+         Action := (Accept_It, Production, Semantic_Action, Semantic_Check, 
RHS_Token_Count);
+      when others =>
+         null;
+      end case;
+      New_Node := new Action_Node'(Symbol, new Parse_Action_Node'(Action, 
null), null);
+      if State.Action_List = null then
+         State.Action_List := New_Node;
+      else
+         Node := State.Action_List;
+         loop
+            exit when Node.Next = null;
+            Node := Node.Next;
+         end loop;
+         Node.Next := New_Node;
+      end if;
+   end Add_Action;
+
+   procedure Add_Action
+     (State           : in out Parse_State;
+      Symbols         : in     Token_ID_Array;
+      Production      : in     Production_ID;
+      RHS_Token_Count : in     Ada.Containers.Count_Type;
+      Semantic_Action : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check  : in     WisiToken.Semantic_Checks.Semantic_Check)
+   is begin
+      --  We assume Duplicate_Reduce is True for this state; no
+      --  conflicts, all the same action.
+      for Symbol of Symbols loop
+         Add_Action
+           (State, Symbol, Reduce, Production, RHS_Token_Count,
+            Semantic_Action, Semantic_Check);
+      end loop;
+      Add_Error (State);
+   end Add_Action;
+
+   procedure Add_Action
+     (State             : in out LR.Parse_State;
+      Symbol            : in     Token_ID;
+      State_Index       : in     WisiToken.State_Index;
+      Reduce_Production : in     Production_ID;
+      RHS_Token_Count   : in     Ada.Containers.Count_Type;
+      Semantic_Action   : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check    : in     Semantic_Checks.Semantic_Check)
+   is
+      Action_1 : constant Parse_Action_Rec := (Shift, State_Index);
+      Action_2 : constant Parse_Action_Rec :=
+        (Reduce, Reduce_Production, Semantic_Action, Semantic_Check, 
RHS_Token_Count);
+   begin
+      State.Action_List := new Action_Node'
+        (Symbol, new Parse_Action_Node'(Action_1, new 
Parse_Action_Node'(Action_2, null)), State.Action_List);
+   end Add_Action;
+
+   procedure Add_Action
+     (State             : in out LR.Parse_State;
+      Symbol            : in     Token_ID;
+      Verb              : in     LR.Parse_Action_Verbs;
+      Production_1      : in     Production_ID;
+      RHS_Token_Count_1 : in     Ada.Containers.Count_Type;
+      Semantic_Action_1 : in     Syntax_Trees.Semantic_Action;
+      Semantic_Check_1  : in     Semantic_Checks.Semantic_Check;
+      Production_2      : in     Production_ID;
+      RHS_Token_Count_2 : in     Ada.Containers.Count_Type;
+      Semantic_Action_2 : in     Syntax_Trees.Semantic_Action;
+      Semantic_Check_2  : in     Semantic_Checks.Semantic_Check)
+   is
+      Action_1 : constant Parse_Action_Rec :=
+        (case Verb is
+         when Reduce    =>
+           (Reduce, Production_1, Semantic_Action_1, Semantic_Check_1, 
RHS_Token_Count_1),
+         when Accept_It =>
+           (Accept_It, Production_1, Semantic_Action_1, Semantic_Check_1, 
RHS_Token_Count_1),
+         when others => raise SAL.Programmer_Error);
+
+      Action_2 : constant Parse_Action_Rec :=
+        (Reduce, Production_2, Semantic_Action_2, Semantic_Check_2, 
RHS_Token_Count_2);
+   begin
+      State.Action_List := new Action_Node'
+        (Symbol, new Parse_Action_Node'(Action_1, new 
Parse_Action_Node'(Action_2, null)), State.Action_List);
+   end Add_Action;
+
+   procedure Add_Error (State  : in out LR.Parse_State)
+   is
+      Action : constant Parse_Action_Rec := (Verb => Error);
+      Node   : Action_Node_Ptr           := State.Action_List;
+   begin
+      if Node = null then
+         raise SAL.Programmer_Error with "adding an error action to a parse 
table state before other actions.";
+      end if;
+      loop
+         exit when Node.Next = null;
+         Node := Node.Next;
+      end loop;
+      Node.Next := new Action_Node'(Invalid_Token_ID, new 
Parse_Action_Node'(Action, null), null);
+   end Add_Error;
+
+   procedure Add_Goto
+     (State      : in out LR.Parse_State;
+      Symbol     : in     Token_ID;
+      To_State   : in     State_Index)
+   is
+      List     : Goto_Node_Ptr renames State.Goto_List;
+      New_Item : constant Goto_Node_Ptr := new Goto_Node'(Symbol, To_State, 
null);
+      I        : Goto_Node_Ptr := List;
+   begin
+      if I = null then
+         List := New_Item;
+      else
+         if List.Symbol > Symbol then
+            New_Item.Next := List;
+            List          := New_Item;
+         else
+            if List.Next = null then
+               List.Next := New_Item;
+            else
+               I := List;
+               loop
+                  exit when I.Next = null or List.Symbol > Symbol;
+                  I := I.Next;
+               end loop;
+               New_Item.Next := I.Next;
+               I.Next        := New_Item;
+            end if;
+         end if;
+      end if;
+   end Add_Goto;
+
+   procedure Set_Production
+     (Prod     : in out Productions.Instance;
+      LHS      : in     Token_ID;
+      RHS_Last : in     Natural)
+   is begin
+      Prod.LHS := LHS;
+      Prod.RHSs.Set_First (0);
+      Prod.RHSs.Set_Last (RHS_Last);
+   end Set_Production;
+
+   procedure Set_RHS
+     (Prod      : in out Productions.Instance;
+      RHS_Index : in     Natural;
+      Tokens    : in     Token_ID_Array;
+      Action    : in     WisiToken.Syntax_Trees.Semantic_Action   := null;
+      Check     : in     WisiToken.Semantic_Checks.Semantic_Check := null)
+   is begin
+      if Tokens'Length > 0 then
+         Prod.RHSs (RHS_Index).Tokens.Set_First (1);
+         Prod.RHSs (RHS_Index).Tokens.Set_Last (Tokens'Length);
+         for I in Tokens'Range loop
+            Prod.RHSs (RHS_Index).Tokens (I) := Tokens (I);
+         end loop;
+         Prod.RHSs (RHS_Index).Action := Action;
+         Prod.RHSs (RHS_Index).Check  := Check;
+      end if;
+   end Set_RHS;
+
+   function Goto_For
+     (Table : in Parse_Table;
+      State : in State_Index;
+      ID    : in Token_ID)
+     return Unknown_State_Index
+   is
+      Goto_Node : constant Goto_Node_Ptr := Goto_For (Table, State, ID);
+   begin
+      if Goto_Node = null then
+         --  We can only get here during error recovery.
+         return Unknown_State;
+      else
+         return Goto_Node.State;
+      end if;
+   end Goto_For;
+
+   function Goto_For
+     (Table : in Parse_Table;
+      State : in State_Index;
+      ID    : in Token_ID)
+     return Goto_Node_Ptr
+   is
+      Goto_Node : Goto_Node_Ptr := Table.States (State).Goto_List;
+   begin
+      while Goto_Node /= null and then Goto_Node.Symbol /= ID loop
+         Goto_Node := Goto_Node.Next;
+      end loop;
+
+      return Goto_Node;
+   end Goto_For;
+
+   function Action_For
+     (Table : in Parse_Table;
+      State : in State_Index;
+      ID    : in Token_ID)
+     return Parse_Action_Node_Ptr
+   is
+      Action_Node : Action_Node_Ptr := Table.States (State).Action_List;
+   begin
+      if Action_Node = null then
+         raise SAL.Programmer_Error with "no actions for state" & 
Unknown_State_Index'Image (State);
+      end if;
+
+      while Action_Node.Next /= null and Action_Node.Symbol /= ID loop
+         Action_Node := Action_Node.Next;
+      end loop;
+
+      return Action_Node.Action;
+   end Action_For;
+
+   function Expecting (Table : in Parse_Table; State : in State_Index) return 
Token_ID_Set
+   is
+      Result : Token_ID_Set    := (Table.First_Terminal .. Table.Last_Terminal 
=> False);
+      Action : Action_Node_Ptr := Table.States (State).Action_List;
+   begin
+      loop
+         --  Last action is error; don't include it.
+         exit when Action.Next = null;
+
+         Result (Action.Symbol) := True;
+         Action := Action.Next;
+      end loop;
+      return Result;
+   end Expecting;
+
+   procedure Free_Table (Table : in out Parse_Table_Ptr)
+   is
+
+      procedure Free is new Ada.Unchecked_Deallocation (Parse_Table, 
Parse_Table_Ptr);
+      Action            : Action_Node_Ptr;
+      Temp_Action       : Action_Node_Ptr;
+      Parse_Action      : Parse_Action_Node_Ptr;
+      Temp_Parse_Action : Parse_Action_Node_Ptr;
+      Got               : Goto_Node_Ptr;
+      Temp_Got          : Goto_Node_Ptr;
+   begin
+      if Table = null then
+         return;
+      end if;
+
+      for State of Table.States loop
+         Action := State.Action_List;
+         loop
+            exit when Action = null;
+            Parse_Action := Action.Action;
+            loop
+               exit when Parse_Action = null;
+               Temp_Parse_Action := Parse_Action;
+               Parse_Action := Parse_Action.Next;
+               Free (Temp_Parse_Action);
+            end loop;
+
+            Temp_Action := Action;
+            Action := Action.Next;
+            Free (Temp_Action);
+         end loop;
+
+         Got := State.Goto_List;
+         loop
+            exit when Got = null;
+            Temp_Got := Got;
+            Got := Got.Next;
+            Free (Temp_Got);
+         end loop;
+      end loop;
+
+      Free (Table);
+   end Free_Table;
+
+   function Get_Action
+     (Prod        : in Production_ID;
+      Productions : in WisiToken.Productions.Prod_Arrays.Vector)
+     return WisiToken.Syntax_Trees.Semantic_Action
+   is begin
+      return Productions (Prod.LHS).RHSs (Prod.RHS).Action;
+   end Get_Action;
+
+   function Get_Check
+     (Prod        : in Production_ID;
+      Productions : in WisiToken.Productions.Prod_Arrays.Vector)
+     return WisiToken.Semantic_Checks.Semantic_Check
+   is begin
+      return Productions (Prod.LHS).RHSs (Prod.RHS).Check;
+   end Get_Check;
+
+   function Get_Text_Rep
+     (File_Name      : in String;
+      McKenzie_Param : in McKenzie_Param_Type;
+      Productions    : in WisiToken.Productions.Prod_Arrays.Vector)
+     return Parse_Table_Ptr
+   is
+      use Ada.Text_IO;
+      use Ada.Strings.Unbounded;
+
+      File  : File_Type;
+      Line  : Unbounded_String;
+      First : Integer;
+      Last  : Integer := 0;
+
+      Delimiters : constant Ada.Strings.Maps.Character_Set := 
Ada.Strings.Maps.To_Set (" ;");
+
+      function Last_Char return Character
+      is begin
+         if Last = 0 then
+            return Element (Line, Last + 1);
+         else
+            return Element (Line, Last);
+         end if;
+      end Last_Char;
+
+      procedure Skip_Char
+      is begin
+         if Last > 0 then
+            Last := Last + 1;
+            if Last > Length (Line) then
+               Last := 0;
+            end if;
+         end if;
+         if Last = 0 then
+            Line := +Get_Line (File);
+            Last := -1 + Index_Non_Blank (Line);
+         end if;
+      end Skip_Char;
+
+      function Next_Value return String
+      is begin
+         First := Last + 1;
+         Last  := Index (Line, Delimiters, First);
+         return Result : constant String := Slice (Line, First, (if Last = 0 
then Length (Line) else Last - 1))
+         do
+            if Last = 0 then
+               Line := +Get_Line (File);
+               Last := -1 + Index_Non_Blank (Line);
+            end if;
+         end return;
+      end Next_Value;
+
+      generic
+         type Value_Type is (<>);
+         Name : in String;
+      function Gen_Next_Value return Value_Type;
+
+      function Gen_Next_Value return Value_Type
+      is
+         Val : constant String := Next_Value;
+      begin
+         return Value_Type'Value (Val);
+      exception
+      when Constraint_Error =>
+         raise SAL.Programmer_Error with Error_Message
+           (File_Name, Line_Number_Type (Ada.Text_IO.Line (File) - 1), 
Ada.Text_IO.Count (First),
+            "expecting " & Name & ", found '" & Val & "'");
+      end Gen_Next_Value;
+
+      function Next_State_Index is new Gen_Next_Value (State_Index, 
"State_Index");
+      function Next_Token_ID is new Gen_Next_Value (Token_ID, "Token_ID");
+      function Next_Integer is new Gen_Next_Value (Integer, "Integer");
+      function Next_Parse_Action_Verbs is new Gen_Next_Value 
(Parse_Action_Verbs, "Parse_Action_Verbs");
+      function Next_Boolean is new Gen_Next_Value (Boolean, "Boolean");
+      function Next_Count_Type is new Gen_Next_Value 
(Ada.Containers.Count_Type, "Count_Type");
+   begin
+      Open (File, In_File, File_Name);
+      Line := +Get_Line (File);
+
+      declare
+         --  We don't read the discriminants in the aggregate, because
+         --  aggregate evaluation order is not guaranteed.
+         State_First       : constant State_Index := Next_State_Index;
+         State_Last        : constant State_Index := Next_State_Index;
+         First_Terminal    : constant Token_ID    := Next_Token_ID;
+         Last_Terminal     : constant Token_ID    := Next_Token_ID;
+         First_Nonterminal : constant Token_ID    := Next_Token_ID;
+         Last_Nonterminal  : constant Token_ID    := Next_Token_ID;
+
+         Table : constant Parse_Table_Ptr := new Parse_Table
+           (State_First, State_Last, First_Terminal, Last_Terminal, 
First_Nonterminal, Last_Nonterminal);
+      begin
+         Table.McKenzie_Param := McKenzie_Param;
+
+         for State of Table.States loop
+            State.Productions.Set_First (Next_Integer);
+            State.Productions.Set_Last (Next_Integer);
+            for I in State.Productions.First_Index .. 
State.Productions.Last_Index loop
+               State.Productions (I).LHS := Next_Token_ID;
+               State.Productions (I).RHS := Next_Integer;
+            end loop;
+
+            declare
+               Node_I       : Action_Node_Ptr := new Action_Node;
+               Actions_Done : Boolean         := False;
+            begin
+               State.Action_List := Node_I;
+               loop
+                  declare
+                     Node_J      : Parse_Action_Node_Ptr := new 
Parse_Action_Node;
+                     Action_Done : Boolean := False;
+                     Verb        : Parse_Action_Verbs;
+                  begin
+                     Node_I.Action := Node_J;
+                     loop
+                        Verb := Next_Parse_Action_Verbs;
+                        Node_J.Item :=
+                          (case Verb is
+                           when Shift     => (Verb => Shift, others => <>),
+                           when Reduce    => (Verb => Reduce, others => <>),
+                           when Accept_It => (Verb => Accept_It, others => <>),
+                           when Error     => (Verb => Error));
+
+                        case Verb is
+                        when Shift =>
+                           Node_J.Item.State := Next_State_Index;
+
+                        when Reduce | Accept_It =>
+                           Node_J.Item.Production.LHS := Next_Token_ID;
+                           Node_J.Item.Production.RHS := Next_Integer;
+                           if Next_Boolean then
+                              Node_J.Item.Action := Get_Action 
(Node_J.Item.Production, Productions);
+                           else
+                              Node_J.Item.Action := null;
+                           end if;
+                           if Next_Boolean then
+                              Node_J.Item.Check := Get_Check 
(Node_J.Item.Production, Productions);
+                           else
+                              Node_J.Item.Check := null;
+                           end if;
+                           Node_J.Item.Token_Count := Next_Count_Type;
+
+                        when Error =>
+                           Actions_Done := True;
+                        end case;
+
+                        if Element (Line, Last) = ';' then
+                           Skip_Char;
+                           Action_Done := True;
+
+                           if not Actions_Done then
+                              Node_I.Symbol := Next_Token_ID;
+                           end if;
+                        end if;
+
+                        exit when Action_Done;
+
+                        Node_J.Next := new Parse_Action_Node;
+                        Node_J      := Node_J.Next;
+                     end loop;
+                  end;
+
+                  exit when Actions_Done;
+                  Node_I.Next := new Action_Node;
+                  Node_I      := Node_I.Next;
+               end loop;
+            end;
+
+            if Element (Line, 1) = ';' then
+               --  No Gotos
+               Skip_Char;
+            else
+               declare
+                  Node_I : Goto_Node_Ptr := new Goto_Node;
+               begin
+                  State.Goto_List  := Node_I;
+                  loop
+                     Node_I.Symbol := Next_Token_ID;
+                     Node_I.State  := Next_State_Index;
+                     exit when Element (Line, Last) = ';';
+                     Node_I.Next   := new Goto_Node;
+                     Node_I        := Node_I.Next;
+                  end loop;
+                  Skip_Char;
+               end;
+            end if;
+
+            declare
+               Verb         : Minimal_Verbs;
+               ID           : Token_ID;
+               Action_State : State_Index;
+               Count        : Ada.Containers.Count_Type;
+            begin
+               loop
+                  if Last_Char = ';' then
+                     Skip_Char;
+                     exit;
+                  end if;
+
+                  Verb := Next_Parse_Action_Verbs;
+                  case Verb is
+                  when Shift =>
+                     ID           := Next_Token_ID;
+                     Action_State := Next_State_Index;
+                     State.Minimal_Complete_Actions.Insert ((Shift, ID, 
Action_State));
+                  when Reduce =>
+                     ID    := Next_Token_ID;
+                     Count := Next_Count_Type;
+                     State.Minimal_Complete_Actions.Insert ((Reduce, ID, 
Count));
+                  end case;
+               end loop;
+            end;
+            --  loop exits on End_Error
+         end loop;
+         --  real return value in End_Error handler; this satisfies the 
compiler
+         return null;
+      exception
+      when End_Error =>
+         Close (File);
+         return Table;
+      end;
+   exception
+   when Name_Error =>
+      raise User_Error with "parser table text file '" & File_Name & "' not 
found.";
+
+   when SAL.Programmer_Error =>
+      if Is_Open (File) then
+         Close (File);
+      end if;
+      raise;
+   when E : others =>
+      if Is_Open (File) then
+         Close (File);
+      end if;
+      raise SAL.Programmer_Error with Error_Message
+        (File_Name, Line_Number_Type (Ada.Text_IO.Line (File) - 1), 
Ada.Text_IO.Count (First),
+         Ada.Exceptions.Exception_Name (E) & ": " & 
Ada.Exceptions.Exception_Message (E));
+   end Get_Text_Rep;
+
+   function Compare (Left, Right : in Insert_Delete_Op) return 
SAL.Compare_Result
+   is begin
+      if Left.Token_Index < Right.Token_Index then
+         return SAL.Less;
+      elsif Left.Token_Index = Right.Token_Index then
+         return SAL.Equal;
+      else
+         return SAL.Greater;
+      end if;
+   end Compare;
+
+   function None_Since_FF (Ops : in Config_Op_Arrays.Vector; Op : in 
Config_Op_Label) return Boolean
+   is begin
+      for O of reverse Ops loop
+         exit when O.Op = Fast_Forward;
+         if O.Op = Op then
+            return False;
+         end if;
+      end loop;
+      return True;
+   end None_Since_FF;
+
+   function Match_Since_FF (Ops : in Config_Op_Arrays.Vector; Op : in 
Config_Op) return Boolean
+   is begin
+      for O of reverse Ops loop
+         exit when O.Op = Fast_Forward;
+         if O = Op then
+            return True;
+         end if;
+      end loop;
+      return False;
+   end Match_Since_FF;
+
+   function Valid_Tree_Indices (Stack : in Recover_Stacks.Stack; Depth : in 
SAL.Base_Peek_Type) return Boolean
+   is
+      use all type Syntax_Trees.Node_Index;
+   begin
+      for I in 1 .. Depth loop
+         if Stack (I).Tree_Index = Syntax_Trees.Invalid_Node_Index then
+            return False;
+         end if;
+      end loop;
+      return True;
+   end Valid_Tree_Indices;
+
+   procedure Set_Key (Item : in out Configuration; Key : in Integer)
+   is begin
+      Item.Cost := Key;
+   end Set_Key;
+
+   function Image
+     (Item       : in Parse_Error;
+      Tree       : in Syntax_Trees.Tree;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is begin
+      case Item.Label is
+      when Action =>
+         return "Action, expecting: " & Image (Item.Expecting, Descriptor) &
+           ", found" & Tree.Image (Item.Error_Token, Descriptor);
+
+      when Check =>
+         return "Check, " & Semantic_Checks.Image (Item.Check_Status, 
Descriptor);
+
+      when Message =>
+         return -Item.Msg;
+      end case;
+   end Image;
+
+end WisiToken.Parse.LR;
diff --git a/wisitoken-parse-lr.ads b/wisitoken-parse-lr.ads
new file mode 100644
index 0000000..7bd38ad
--- /dev/null
+++ b/wisitoken-parse-lr.ads
@@ -0,0 +1,624 @@
+--  Abstract :
+--
+--  Root package of an implementation of an LR (Left-to-right scanning
+--  Rightmost-deriving) parser. Includes operations for building the
+--  parse table at runtime. See the child packages .Parse and
+--  .Parse_No_Recover for running the parser.
+--
+--  References :
+--
+--  See wisitoken.ads
+--
+--  Copyright (C) 2002, 2003, 2009, 2010, 2013-2015, 2017 - 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with Ada.Containers.Indefinite_Doubly_Linked_Lists;
+with Ada.Unchecked_Deallocation;
+with SAL.Gen_Bounded_Definite_Vectors.Gen_Sorted;
+with SAL.Gen_Bounded_Definite_Vectors.Gen_Image_Aux;
+with SAL.Gen_Definite_Doubly_Linked_Lists_Sorted.Gen_Image;
+with SAL.Gen_Unbounded_Definite_Min_Heaps_Fibonacci;
+with SAL.Gen_Unbounded_Definite_Queues.Gen_Image_Aux;
+with SAL.Gen_Unbounded_Definite_Stacks.Gen_Image_Aux;
+with System.Multiprocessors;
+with WisiToken.Productions;
+with WisiToken.Semantic_Checks;
+with WisiToken.Syntax_Trees;
+package WisiToken.Parse.LR is
+
+   type All_Parse_Action_Verbs is (Pause, Shift_Recover, Shift, Reduce, 
Accept_It, Error);
+   subtype Parse_Action_Verbs is All_Parse_Action_Verbs range Shift .. Error;
+   subtype Minimal_Verbs is All_Parse_Action_Verbs range Shift .. Reduce;
+   --  Pause, Shift_Recover are only used for error recovery.
+
+   type Parse_Action_Rec (Verb : Parse_Action_Verbs := Shift) is record
+      case Verb is
+      when Shift =>
+         State : State_Index := State_Index'Last;
+
+      when Reduce | Accept_It =>
+         Production : Production_ID;
+         --  The result nonterm and production index. Most uses need only
+         --  Production.LHS; elisp code generation, and debug output, needs
+         --  Production.RHS
+
+         Action      : WisiToken.Syntax_Trees.Semantic_Action   := null;
+         Check       : WisiToken.Semantic_Checks.Semantic_Check := null;
+         Token_Count : Ada.Containers.Count_Type                := 0;
+
+      when Error =>
+         null;
+      end case;
+   end record;
+   subtype Shift_Action_Rec is Parse_Action_Rec (Shift);
+   subtype Reduce_Action_Rec is Parse_Action_Rec (Reduce);
+
+   function Image (Item : in Parse_Action_Rec; Descriptor : in 
WisiToken.Descriptor) return String;
+   --  Ada aggregate syntax, leaving out Action, Check in reduce; for debug 
output
+
+   procedure Put (Trace : in out WisiToken.Trace'Class; Item : in 
Parse_Action_Rec);
+   --  Put a line for Item in parse trace format.
+
+   function Equal (Left, Right : in Parse_Action_Rec) return Boolean;
+   --  Ignore Action, Check.
+
+   type Parse_Action_Node;
+   type Parse_Action_Node_Ptr is access Parse_Action_Node;
+
+   type Parse_Action_Node is record
+      Item : Parse_Action_Rec;
+      Next : Parse_Action_Node_Ptr; -- non-null only for conflicts
+   end record;
+   procedure Free is new Ada.Unchecked_Deallocation (Parse_Action_Node, 
Parse_Action_Node_Ptr);
+
+   type Action_Node;
+   type Action_Node_Ptr is access Action_Node;
+
+   type Action_Node is record
+      Symbol : Token_ID := Invalid_Token_ID; -- ignored if Action is Error
+      Action : Parse_Action_Node_Ptr;
+      Next   : Action_Node_Ptr;
+   end record;
+   procedure Free is new Ada.Unchecked_Deallocation (Action_Node, 
Action_Node_Ptr);
+
+   procedure Add
+     (List   : in out Action_Node_Ptr;
+      Symbol : in     Token_ID;
+      Action : in     Parse_Action_Rec);
+   --  Add action to List, sorted on ascending Symbol.
+
+   type Goto_Node is private;
+   type Goto_Node_Ptr is access Goto_Node;
+
+   function Symbol (List : in Goto_Node_Ptr) return Token_ID;
+   function State (List : in Goto_Node_Ptr) return State_Index;
+   function Next (List : in Goto_Node_Ptr) return Goto_Node_Ptr;
+
+   type Minimal_Action (Verb : Minimal_Verbs := Shift) is record
+      case Verb is
+      when Shift =>
+         ID    : Token_ID;
+         State : State_Index;
+
+      when Reduce =>
+         Nonterm     : Token_ID;
+         Token_Count : Ada.Containers.Count_Type;
+      end case;
+   end record;
+
+   function Compare_Minimal_Action (Left, Right : in Minimal_Action) return 
SAL.Compare_Result;
+
+   type Minimal_Action_Array is array (Positive range <>) of Minimal_Action;
+
+   package Minimal_Action_Lists is new 
SAL.Gen_Definite_Doubly_Linked_Lists_Sorted
+     (Minimal_Action, Compare_Minimal_Action);
+
+   function Strict_Image (Item : in Minimal_Action) return String;
+   --  Strict Ada aggregate syntax, for generated code.
+
+   function Image is new Minimal_Action_Lists.Gen_Image (Strict_Image);
+
+   procedure Set_Minimal_Action (List : out Minimal_Action_Lists.List; Actions 
: in Minimal_Action_Array);
+
+   type Parse_State is record
+      Productions : Production_ID_Arrays.Vector;
+      --  Used in error recovery.
+      Action_List : Action_Node_Ptr;
+      Goto_List   : Goto_Node_Ptr;
+
+      Minimal_Complete_Actions : Minimal_Action_Lists.List;
+      --  Set of parse actions that will most quickly complete the
+      --  productions in this state; used in error recovery
+   end record;
+
+   type Parse_State_Array is array (State_Index range <>) of Parse_State;
+
+   type Action_List_Iterator is tagged private;
+   --  Iterates over all shift/reduce actions for a state, including
+   --  conflicts.
+
+   function First (State : in Parse_State) return Action_List_Iterator;
+   function Is_Done (Iter : in Action_List_Iterator) return Boolean;
+   procedure Next (Iter : in out Action_List_Iterator);
+
+   function Symbol (Iter : in Action_List_Iterator) return Token_ID;
+   function Action (Iter : in Action_List_Iterator) return Parse_Action_Rec;
+
+   procedure Add_Action
+     (State       : in out Parse_State;
+      Symbol      : in     Token_ID;
+      State_Index : in     WisiToken.State_Index);
+   --  Add a Shift action to tail of State action list.
+
+   procedure Add_Action
+     (State           : in out Parse_State;
+      Symbol          : in     Token_ID;
+      Verb            : in     Parse_Action_Verbs;
+      Production      : in     Production_ID;
+      RHS_Token_Count : in     Ada.Containers.Count_Type;
+      Semantic_Action : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check  : in     WisiToken.Semantic_Checks.Semantic_Check);
+   --  Add a Reduce or Accept_It action to tail of State action list.
+
+   procedure Add_Action
+     (State           : in out Parse_State;
+      Symbols         : in     Token_ID_Array;
+      Production      : in     Production_ID;
+      RHS_Token_Count : in     Ada.Containers.Count_Type;
+      Semantic_Action : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check  : in     WisiToken.Semantic_Checks.Semantic_Check);
+   --  Add duplicate Reduce actions, and final Error action, to tail of
+   --  State action list.
+
+   procedure Add_Action
+     (State             : in out Parse_State;
+      Symbol            : in     Token_ID;
+      State_Index       : in     WisiToken.State_Index;
+      Reduce_Production : in     Production_ID;
+      RHS_Token_Count   : in     Ada.Containers.Count_Type;
+      Semantic_Action   : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check    : in     WisiToken.Semantic_Checks.Semantic_Check);
+   --  Add a Shift/Reduce conflict to State.
+
+   procedure Add_Action
+     (State             : in out Parse_State;
+      Symbol            : in     Token_ID;
+      Verb              : in     Parse_Action_Verbs;
+      Production_1      : in     Production_ID;
+      RHS_Token_Count_1 : in     Ada.Containers.Count_Type;
+      Semantic_Action_1 : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check_1  : in     WisiToken.Semantic_Checks.Semantic_Check;
+      Production_2      : in     Production_ID;
+      RHS_Token_Count_2 : in     Ada.Containers.Count_Type;
+      Semantic_Action_2 : in     WisiToken.Syntax_Trees.Semantic_Action;
+      Semantic_Check_2  : in     WisiToken.Semantic_Checks.Semantic_Check);
+   --  Add an Accept/Reduce or Reduce/Reduce conflict action to State.
+
+   procedure Add_Error (State  : in out Parse_State);
+   --  Add an Error action to State, at tail of action list.
+
+   procedure Add_Goto
+     (State    : in out Parse_State;
+      Symbol   : in     Token_ID;
+      To_State : in     State_Index);
+   --  Add a goto item to State goto list; keep goto list sorted in ascending 
order on Symbol.
+
+   type McKenzie_Param_Type
+     (First_Terminal    : Token_ID;
+      Last_Terminal     : Token_ID;
+      First_Nonterminal : Token_ID;
+      Last_Nonterminal  : Token_ID)
+   is record
+      Insert    : Token_ID_Array_Natural (First_Terminal .. Last_Terminal);
+      Delete    : Token_ID_Array_Natural (First_Terminal .. Last_Terminal);
+      Push_Back : Token_ID_Array_Natural (First_Terminal .. Last_Nonterminal);
+      --  Cost of operations on config stack, input.
+
+      Ignore_Check_Fail : Natural;
+      --  Cost of ignoring a semantic check failure. Should be at least the
+      --  cost of a typical fix for such a failure.
+
+      Task_Count : System.Multiprocessors.CPU_Range;
+      --  Number of parallel tasks during recovery. If 0, use
+      --  System.Multiprocessors.Number_Of_CPUs - 1.
+
+      Cost_Limit        : Natural;     -- max cost of configurations to look at
+      Check_Limit       : Token_Index; -- max tokens to parse ahead when 
checking a configuration.
+      Check_Delta_Limit : Natural;     -- max configs checked, delta over 
successful parser.
+      Enqueue_Limit     : Natural;     -- max configs enqueued.
+   end record;
+
+   Default_McKenzie_Param : constant McKenzie_Param_Type :=
+     (First_Terminal    => Token_ID'Last,
+      Last_Terminal     => Token_ID'First,
+      First_Nonterminal => Token_ID'Last,
+      Last_Nonterminal  => Token_ID'First,
+      Insert            => (others => 0),
+      Delete            => (others => 0),
+      Push_Back         => (others => 0),
+      Ignore_Check_Fail => 0,
+      Task_Count        => System.Multiprocessors.CPU_Range'Last,
+      Cost_Limit        => Natural'Last,
+      Check_Limit       => Token_Index'Last,
+      Check_Delta_Limit => Natural'Last,
+      Enqueue_Limit     => Natural'Last);
+
+   procedure Set_Production
+     (Prod     : in out Productions.Instance;
+      LHS      : in     Token_ID;
+      RHS_Last : in     Natural);
+
+   procedure Set_RHS
+     (Prod      : in out Productions.Instance;
+      RHS_Index : in     Natural;
+      Tokens    : in     Token_ID_Array;
+      Action    : in     WisiToken.Syntax_Trees.Semantic_Action   := null;
+      Check     : in     WisiToken.Semantic_Checks.Semantic_Check := null);
+
+   type Parse_Table
+     (State_First       : State_Index;
+      State_Last        : State_Index;
+      First_Terminal    : Token_ID;
+      Last_Terminal     : Token_ID;
+      First_Nonterminal : Token_ID;
+      Last_Nonterminal  : Token_ID)
+     is tagged
+   record
+      States         : Parse_State_Array (State_First .. State_Last);
+      McKenzie_Param : McKenzie_Param_Type (First_Terminal, Last_Terminal, 
First_Nonterminal, Last_Nonterminal);
+   end record;
+
+   function Goto_For
+     (Table : in Parse_Table;
+      State : in State_Index;
+      ID    : in Token_ID)
+     return Unknown_State_Index;
+   function Goto_For
+     (Table : in Parse_Table;
+      State : in State_Index;
+      ID    : in Token_ID)
+     return Goto_Node_Ptr;
+   --  Return next state after reducing stack by nonterminal ID;
+   --  Unknown_State if none (only possible during error recovery).
+   --  Second form allows retrieving Production.
+
+   function Action_For
+     (Table : in Parse_Table;
+      State : in State_Index;
+      ID    : in Token_ID)
+     return Parse_Action_Node_Ptr;
+   --  Return the action for State, terminal ID.
+
+   function Expecting (Table : in Parse_Table; State : in State_Index) return 
Token_ID_Set;
+
+   type Parse_Table_Ptr is access Parse_Table;
+   procedure Free_Table (Table : in out Parse_Table_Ptr);
+
+   function Get_Action
+     (Prod        : in Production_ID;
+      Productions : in WisiToken.Productions.Prod_Arrays.Vector)
+     return WisiToken.Syntax_Trees.Semantic_Action;
+
+   function Get_Check
+     (Prod        : in Production_ID;
+      Productions : in WisiToken.Productions.Prod_Arrays.Vector)
+     return WisiToken.Semantic_Checks.Semantic_Check;
+
+   function Get_Text_Rep
+     (File_Name      : in String;
+      McKenzie_Param : in McKenzie_Param_Type;
+      Productions    : in WisiToken.Productions.Prod_Arrays.Vector)
+     return Parse_Table_Ptr;
+   --  Read machine-readable text format of states from a file File_Name.
+   --  Result has actions, checks from Productions.
+
+   ----------
+   --  For McKenzie_Recover. Declared here because Parser_Lists needs
+   --  these, Mckenzie_Recover needs Parser_Lists.
+   --
+   --  We don't maintain a syntax tree during recover; it's too slow, and
+   --  not needed for any operations. The parser syntax tree is used for
+   --  Undo_Reduce, which is only done on nonterms reduced by the main
+   --  parser, not virtual nonterms produced by recover.
+
+   package Fast_Token_ID_Arrays is new SAL.Gen_Bounded_Definite_Vectors
+     (SAL.Peek_Type, Token_ID, Capacity => 20);
+
+   No_Insert_Delete : constant SAL.Base_Peek_Type := 0;
+
+   function Image
+     (Index      : in SAL.Peek_Type;
+      Tokens     : in Fast_Token_ID_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+     is (SAL.Peek_Type'Image (Index) & ":" & SAL.Peek_Type'Image 
(Tokens.Last_Index) & ":" &
+           Image (Tokens (Index), Descriptor));
+
+   type Config_Op_Label is (Fast_Forward, Undo_Reduce, Push_Back, Insert, 
Delete);
+   subtype Insert_Delete_Op_Label is Config_Op_Label range Insert .. Delete;
+   --  Fast_Forward is a placeholder to mark a fast_forward parse; that
+   --  resets what operations are allowed to be done on a config.
+   --
+   --  Undo_Reduce is the inverse of Reduce.
+   --
+   --  Push_Back pops the top stack item, and moves the input stream
+   --  pointer back to the first shared_terminal contained by that item.
+   --
+   --  Insert inserts a new token in the token input stream, before the
+   --  given point in Terminals.
+   --
+   --  Delete deletes one item from the token input stream, at the given
+   --  point.
+
+   type Config_Op (Op : Config_Op_Label := Fast_Forward) is record
+      --  We store enough information to perform the operation on the main
+      --  parser stack and input stream point when the config is the result
+      --  of a successful recover.
+      --
+      --  After a recover, the main parser must reparse any inserted tokens,
+      --  and skip any deleted tokens. Therefore, when all the recover ops
+      --  are applied, the main parser stack will be the same or shorter
+      --  than it was, so we only need to store token counts for stack
+      --  operations (Unknown_State is pushed when a state is needed; none
+      --  will be left on the main stack). We also store IDs, so we can
+      --  check that everything is in sync, and for debugging.
+
+      case Op is
+      when Fast_Forward =>
+         FF_Token_Index : WisiToken.Token_Index;
+         --  Config.Current_Shared_Token after the operation is done; the last
+         --  token shifted.
+
+      when Undo_Reduce =>
+         Nonterm : Token_ID;
+         --  The nonterminal popped off the stack.
+
+         Token_Count : Ada.Containers.Count_Type;
+         --  The number of tokens pushed on the stack.
+
+      when Push_Back | Insert | Delete =>
+         ID : Token_ID;
+         --  For Push_Back, ID is the nonterm ID popped off the stack.
+         --  For Insert | Delete, ID is the token inserted or deleted.
+
+         Token_Index : WisiToken.Base_Token_Index;
+         --  For Push_Back, Token_Index is Config.Current_Shared_Token after
+         --  the operation is done. If the token is empty, Token_Index is
+         --  Invalid_Token_Index.
+         --
+         --  For Insert, ID is inserted before Token_Index.
+         --
+         --  For Delete, token at Token_Index is deleted.
+
+      end case;
+   end record;
+   subtype Insert_Delete_Op is Config_Op with Dynamic_Predicate => 
(Insert_Delete_Op.Op in Insert_Delete_Op_Label);
+
+   function Compare (Left, Right : in Insert_Delete_Op) return 
SAL.Compare_Result;
+
+   package Config_Op_Queues is new SAL.Gen_Unbounded_Definite_Queues 
(Config_Op);
+
+   package Config_Op_Arrays is new SAL.Gen_Bounded_Definite_Vectors
+     (Positive_Index_Type, Config_Op, Capacity => 80);
+   --  Using a fixed size vector significantly speeds up
+   --  McKenzie_Recover. The capacity is determined by the maximum number
+   --  of repair operations, which is limited by the cost_limit McKenzie
+   --  parameter plus an arbitrary number from the language-specific
+   --  repairs; in practice, a capacity of 80 is enough so far. If a
+   --  config does hit that limit, it is abandoned; some other config is
+   --  likely to be cheaper.
+
+   package Insert_Delete_Arrays is new SAL.Gen_Bounded_Definite_Vectors
+     (Positive_Index_Type, Insert_Delete_Op, Capacity => 80);
+
+   package Sorted_Insert_Delete_Arrays is new Insert_Delete_Arrays.Gen_Sorted 
(Compare);
+
+   function Config_Op_Image (Item : in Config_Op; Descriptor : in 
WisiToken.Descriptor) return String
+     is ("(" & Config_Op_Label'Image (Item.Op) & ", " &
+           (case Item.Op is
+            when Fast_Forward => WisiToken.Token_Index'Image 
(Item.FF_Token_Index),
+            when Undo_Reduce => Image (Item.Nonterm, Descriptor) & "," &
+                 Ada.Containers.Count_Type'Image (Item.Token_Count),
+            when Push_Back | Insert | Delete => Image (Item.ID, Descriptor) & 
"," &
+                 WisiToken.Token_Index'Image (Item.Token_Index))
+           & ")");
+
+   function Image (Item : in Config_Op; Descriptor : in WisiToken.Descriptor) 
return String
+     renames Config_Op_Image;
+
+   function Image is new Config_Op_Queues.Gen_Image_Aux (WisiToken.Descriptor, 
Image);
+   function Config_Op_Array_Image is new Config_Op_Arrays.Gen_Image_Aux 
(WisiToken.Descriptor, Image);
+   function Image (Item : in Config_Op_Arrays.Vector; Descriptor : in 
WisiToken.Descriptor) return String
+     renames Config_Op_Array_Image;
+
+   function None (Ops : in Config_Op_Arrays.Vector; Op : in Config_Op_Label) 
return Boolean
+   is (for all O of Ops => O.Op /= Op);
+   --  True if Ops contains no Op.
+
+   function None_Since_FF (Ops : in Config_Op_Arrays.Vector; Op : in 
Config_Op_Label) return Boolean;
+   --  True if Ops contains no Op after the last Fast_Forward (or ops.first, if
+   --  no Fast_Forward).
+
+   function Match_Since_FF (Ops : in Config_Op_Arrays.Vector; Op : in 
Config_Op) return Boolean;
+   --  True if Ops contains an Op after the last Fast_Forward (or ops.first, if
+   --  no Fast_Forward) that equals Op.
+
+   function Any (Ops : in Config_Op_Arrays.Vector; Op : in Config_Op_Label) 
return Boolean
+   is (for some O of Ops => O.Op = Op);
+   --  True if Ops contains at least one Op.
+
+   type Recover_Stack_Item is record
+      State      : Unknown_State_Index;
+      Tree_Index : Syntax_Trees.Node_Index;
+      Token      : Recover_Token;
+   end record;
+
+   package Recover_Stacks is new SAL.Gen_Unbounded_Definite_Stacks 
(Recover_Stack_Item);
+
+   function Image (Item : in Recover_Stack_Item; Descriptor : in 
WisiToken.Descriptor) return String
+     is ((if Item.State = Unknown_State then " " else Trimmed_Image 
(Item.State)) & " : " &
+           Image (Item.Token, Descriptor));
+
+   function Recover_Stack_Image is new Recover_Stacks.Gen_Image_Aux 
(WisiToken.Descriptor, Image);
+   --  Unique name for calling from debugger
+
+   function Image
+     (Stack      : in Recover_Stacks.Stack;
+      Descriptor : in WisiToken.Descriptor;
+      Depth      : in SAL.Base_Peek_Type := 0)
+     return String
+     renames Recover_Stack_Image;
+
+   function Valid_Tree_Indices (Stack : in Recover_Stacks.Stack; Depth : in 
SAL.Base_Peek_Type) return Boolean;
+   --  Return True if Stack top Depth items have valid Tree_Indices,
+   --  which is true if they were copied from the parser stack, and not
+   --  pushed by recover.
+
+   type Configuration is record
+      Stack : Recover_Stacks.Stack;
+      --  Initially built from the parser stack, then the stack after the
+      --  Ops below have been performed.
+
+      Resume_Token_Goal : Token_Index := Token_Index'Last;
+      --  A successful solution shifts this token. Per-config because it
+      --  increases with Delete; we increase Shared_Parser.Resume_Token_Goal
+      --  only from successful configs.
+
+      Current_Shared_Token : Base_Token_Index := Token_Index'Last;
+      --  Index into Shared_Parser.Terminals for current input token, after
+      --  all of Inserted is input. Initially the error token.
+
+      String_Quote_Checked : Line_Number_Type := Invalid_Line_Number;
+      --  Max line checked for missing string quote.
+
+      Insert_Delete : Sorted_Insert_Delete_Arrays.Vector;
+      --  Edits to the input stream that are not yet parsed; contains only
+      --  Insert and Delete ops, in token_index order.
+
+      Current_Insert_Delete : SAL.Base_Peek_Type := No_Insert_Delete;
+      --  Index of the next op in Insert_Delete. If No_Insert_Delete, use
+      --  Current_Shared_Token.
+
+      Error_Token       : Recover_Token;
+      Check_Token_Count : Ada.Containers.Count_Type;
+      Check_Status      : Semantic_Checks.Check_Status;
+      --  If parsing this config ended with a parse error, Error_Token is
+      --  the token that failed to shift, Check_Status.Label is Ok.
+      --
+      --  If parsing this config ended with a semantic check fail,
+      --  Error_Token is the nonterm created by the reduction,
+      --  Check_Token_Count the number of tokens in the right hand side, and
+      --  Check_Status is the error.
+      --
+      --  Error_Token is set to Invalid_Token_ID when Config is parsed
+      --  successfully, or modified so the error is no longer meaningful (ie
+      --  in explore when adding an op, or in language_fixes when adding a
+      --  fix).
+
+      Ops : Config_Op_Arrays.Vector;
+      --  Record of operations applied to this Config, in application order.
+      --  Insert and Delete ops that are not yet parsed are reflected in
+      --  Insert_Delete, in token_index order.
+
+      Current_Ops : SAL.Base_Peek_Type := No_Insert_Delete;
+      --  If No_Insert_Delete, append new ops to Ops. Otherwise insert
+      --  before Current_Ops. This happens when Fast_Forward fails with the
+      --  remaining ops at Current_Shared_Token.
+
+      Cost : Natural := 0;
+   end record;
+   type Configuration_Access is access all Configuration;
+   for Configuration_Access'Storage_Size use 0;
+
+   function Key (A : in Configuration) return Integer is (A.Cost);
+
+   procedure Set_Key (Item : in out Configuration; Key : in Integer);
+
+   package Config_Heaps is new SAL.Gen_Unbounded_Definite_Min_Heaps_Fibonacci
+     (Element_Type   => Configuration,
+      Element_Access => Configuration_Access,
+      Key_Type       => Integer,
+      Key            => Key,
+      Set_Key        => Set_Key);
+
+   type Check_Status is (Success, Abandon, Continue);
+   subtype Non_Success_Status is Check_Status range Abandon .. Continue;
+
+   type McKenzie_Data is tagged record
+      Config_Heap   : Config_Heaps.Heap_Type;
+      Enqueue_Count : Integer := 0;
+      Check_Count   : Integer := 0;
+      Results       : Config_Heaps.Heap_Type;
+      Success       : Boolean := False;
+   end record;
+
+   type McKenzie_Access is access all McKenzie_Data;
+
+   type Parse_Error_Label is (Action, Check, Message);
+
+   type Parse_Error
+     (Label          : Parse_Error_Label;
+      First_Terminal : Token_ID;
+      Last_Terminal  : Token_ID)
+   is record
+      Recover : Configuration;
+
+      case Label is
+      when Action =>
+         Error_Token : Syntax_Trees.Valid_Node_Index; -- index into Parser.Tree
+         Expecting   : Token_ID_Set (First_Terminal .. Last_Terminal);
+
+      when Check =>
+         Check_Status : Semantic_Checks.Check_Status;
+
+      when Message =>
+         Msg : Ada.Strings.Unbounded.Unbounded_String;
+      end case;
+   end record;
+
+   package Parse_Error_Lists is new 
Ada.Containers.Indefinite_Doubly_Linked_Lists (Parse_Error);
+
+   function Image
+     (Item       : in Parse_Error;
+      Tree       : in Syntax_Trees.Tree;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+
+private
+
+   type Goto_Node is record
+      Symbol     : Token_ID;
+      State      : State_Index;
+      Next       : Goto_Node_Ptr;
+   end record;
+   procedure Free is new Ada.Unchecked_Deallocation (Goto_Node, Goto_Node_Ptr);
+
+   type Action_List_Iterator is tagged record
+      Node : Action_Node_Ptr;
+      Item : Parse_Action_Node_Ptr;
+   end record;
+
+end WisiToken.Parse.LR;
diff --git a/wisitoken-parse-packrat-generated.adb 
b/wisitoken-parse-packrat-generated.adb
new file mode 100644
index 0000000..c71e57c
--- /dev/null
+++ b/wisitoken-parse-packrat-generated.adb
@@ -0,0 +1,86 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body WisiToken.Parse.Packrat.Generated is
+
+   overriding procedure Parse (Parser : aliased in out Generated.Parser)
+   is
+      --  'aliased required for Base_Tree'Access. WORKAROUND: that was
+      --  enough when Parser type was declared in generated Main; now that
+      --  it's a derived type, it doesn't work. So we use Unchecked_Access.
+
+      Descriptor : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+
+      Junk : WisiToken.Syntax_Trees.Valid_Node_Index;
+      pragma Unreferenced (Junk);
+
+      Result : Memo_Entry;
+   begin
+      Parser.Base_Tree.Clear;
+      Parser.Tree.Initialize (Parser.Base_Tree'Unchecked_Access, Flush => 
True);
+      Parser.Lex_All;
+      Parser.Derivs.Set_First (Descriptor.First_Nonterminal);
+      Parser.Derivs.Set_Last (Descriptor.Last_Nonterminal);
+
+      for Nonterm in Descriptor.First_Nonterminal .. 
Parser.Trace.Descriptor.Last_Nonterminal loop
+         Parser.Derivs (Nonterm).Clear;
+         Parser.Derivs (Nonterm).Set_First (Parser.Terminals.First_Index);
+         Parser.Derivs (Nonterm).Set_Last (Parser.Terminals.Last_Index);
+      end loop;
+
+      for Token_Index in Parser.Terminals.First_Index .. 
Parser.Terminals.Last_Index loop
+         Junk := Parser.Tree.Add_Terminal (Token_Index, Parser.Terminals);
+         --  FIXME: move this into Lex_All, delete Terminals, just use 
Syntax_Tree
+      end loop;
+
+      Result := Parser.Parse_WisiToken_Accept (Parser, 
Parser.Terminals.First_Index - 1);
+
+      if Result.State /= Success then
+         if Trace_Parse > Outline then
+            Parser.Trace.Put_Line ("parse failed");
+         end if;
+
+         raise Syntax_Error with "parse failed"; --  FIXME: need better error 
message!
+      else
+         Parser.Tree.Set_Root (Result.Result);
+      end if;
+
+   end Parse;
+
+   overriding function Any_Errors (Parser : in Generated.Parser) return Boolean
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      return Parser.Lexer.Errors.Length > 0;
+   end Any_Errors;
+
+   overriding procedure Put_Errors (Parser : in Generated.Parser)
+   is
+      use Ada.Text_IO;
+   begin
+      for Item of Parser.Lexer.Errors loop
+         Put_Line
+           (Current_Error,
+            Parser.Lexer.File_Name & ":0:0: lexer unrecognized character at" & 
Buffer_Pos'Image (Item.Char_Pos));
+      end loop;
+
+      --  FIXME: Packrat parser does not report errors yet.
+   end Put_Errors;
+
+end WisiToken.Parse.Packrat.Generated;
diff --git a/wisitoken-parse-packrat-generated.ads 
b/wisitoken-parse-packrat-generated.ads
new file mode 100644
index 0000000..408d48e
--- /dev/null
+++ b/wisitoken-parse-packrat-generated.ads
@@ -0,0 +1,70 @@
+--  Abstract :
+--
+--  Types and operations for a packrat parser runtime, with nonterm
+--  parsing subprograms generated by wisi-generate.
+--
+--  References:
+--
+--  see 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);
+with WisiToken.Syntax_Trees;
+package WisiToken.Parse.Packrat.Generated is
+
+   Recursive : exception; -- FIXME: delete
+
+   type Memo_State is (No_Result, Failure, Success);
+   subtype Result_States is Memo_State range Failure .. Success;
+
+   type Memo_Entry (State : Memo_State := No_Result) is record
+
+      case State is
+      when No_Result =>
+         Recursive : Boolean := False; --  FIXME: delete
+
+      when Failure =>
+         null;
+
+      when Success =>
+         Result : aliased WisiToken.Syntax_Trees.Valid_Node_Index;
+
+         Last_Token : Base_Token_Index; --  FIXME: change to Last_Pos
+
+      end case;
+   end record;
+
+   package Memos is new SAL.Gen_Unbounded_Definite_Vectors (Token_Index, 
Memo_Entry);
+
+   subtype Result_Type is Memo_Entry
+   with Dynamic_Predicate => Result_Type.State in Result_States;
+
+   package Derivs is new SAL.Gen_Unbounded_Definite_Vectors (Token_ID, 
Memos.Vector);
+
+   type Parse_WisiToken_Accept is access
+     --  WORKAROUND: using Packrat.Parser'Class here hits a GNAT Bug box in 
GPL 2018.
+     function (Parser : in out Base_Parser'Class; Last_Pos : in 
Base_Token_Index) return Result_Type;
+
+   type Parser is new Packrat.Parser with record
+      Derivs : Generated.Derivs.Vector; --  FIXME: use discriminated array, as 
in procedural
+
+      Parse_WisiToken_Accept : Generated.Parse_WisiToken_Accept;
+   end record;
+
+   overriding procedure Parse (Parser : aliased in out Generated.Parser);
+   overriding function Any_Errors (Parser : in Generated.Parser) return 
Boolean;
+   overriding procedure Put_Errors (Parser : in Generated.Parser);
+
+end WisiToken.Parse.Packrat.Generated;
diff --git a/wisitoken-parse-packrat-procedural.adb 
b/wisitoken-parse-packrat-procedural.adb
new file mode 100644
index 0000000..8ec9559
--- /dev/null
+++ b/wisitoken-parse-packrat-procedural.adb
@@ -0,0 +1,251 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body WisiToken.Parse.Packrat.Procedural is
+
+   function Apply_Rule
+     (Parser   : in out Procedural.Parser;
+      R        : in     Token_ID;
+      Last_Pos : in     Base_Token_Index)
+     return Memo_Entry
+   with Post => Apply_Rule'Result.State in Failure .. Success;
+
+   function Eval
+     (Parser   : in out Procedural.Parser;
+      R        : in     Token_ID;
+      Last_Pos : in     Base_Token_Index)
+     return Memo_Entry
+   with Post => Eval'Result.State in Failure .. Success;
+
+   ----------
+   --  bodies
+
+   function Eval
+     (Parser   : in out Procedural.Parser;
+      R        : in     Token_ID;
+      Last_Pos : in     Base_Token_Index)
+     return Memo_Entry
+   is
+      Descriptor : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+
+      subtype Terminal is Token_ID range Descriptor.First_Terminal .. 
Descriptor.Last_Terminal;
+
+      Pos : Base_Token_Index := Last_Pos; --  last token parsed.
+   begin
+      for RHS_Index in Parser.Grammar (R).RHSs.First_Index .. Parser.Grammar 
(R).RHSs.Last_Index loop
+         declare
+            use all type Ada.Containers.Count_Type;
+            RHS  : WisiToken.Productions.Right_Hand_Side renames 
Parser.Grammar (R).RHSs (RHS_Index);
+            Memo : Memo_Entry; --  for temporary or intermediate results
+         begin
+            if RHS.Tokens.Length = 0 then
+               return
+                 (State              => Success,
+                  Result             => Parser.Tree.Add_Nonterm
+                    (Production      => (R, RHS_Index),
+                     Action          => RHS.Action,
+                     Children        => (1 .. 0 => 
Syntax_Trees.Invalid_Node_Index),
+                     Default_Virtual => False),
+                  Last_Pos           => Pos);
+            else
+               declare
+                  Children : Syntax_Trees.Valid_Node_Index_Array
+                    (SAL.Base_Peek_Type (RHS.Tokens.First_Index) .. 
SAL.Base_Peek_Type (RHS.Tokens.Last_Index));
+               begin
+                  for I in RHS.Tokens.First_Index .. RHS.Tokens.Last_Index loop
+                     if RHS.Tokens (I) in Terminal then
+                        if Pos = Parser.Terminals.Last_Index then
+                           goto Fail_RHS;
+
+                        elsif Parser.Terminals (Pos + 1).ID = RHS.Tokens (I) 
then
+                           Pos := Pos + 1;
+                           Children (SAL.Base_Peek_Type (I)) := Tree_Index 
(Pos);
+                        else
+                           goto Fail_RHS;
+                        end if;
+                     else
+                        Memo := Apply_Rule (Parser, RHS.Tokens (I), Pos);
+                        case Memo.State is
+                        when Success =>
+                           Children (SAL.Base_Peek_Type (I)) := Memo.Result;
+                           Pos := Memo.Last_Pos;
+
+                        when Failure =>
+                           goto Fail_RHS;
+                        when No_Result =>
+                           raise SAL.Programmer_Error;
+                        end case;
+                     end if;
+                  end loop;
+
+                  return
+                    (State              => Success,
+                     Result             => Parser.Tree.Add_Nonterm
+                       (Production      => (R, RHS_Index),
+                        Action          => RHS.Action,
+                        Children        => Children,
+                        Default_Virtual => False),
+                     Last_Pos           => Pos);
+
+                  <<Fail_RHS>>
+                  Pos := Last_Pos;
+               end;
+            end if;
+         end;
+      end loop;
+      --  get here when all RHSs fail
+
+      return (State => Failure);
+   end Eval;
+
+   function Apply_Rule
+     (Parser   : in out Procedural.Parser;
+      R        : in     Token_ID;
+      Last_Pos : in     Base_Token_Index)
+     return Memo_Entry
+   is
+      Descriptor : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+
+      Pos       : Base_Token_Index     := Last_Pos;     --  last token parsed.
+      Start_Pos : constant Token_Index := Last_Pos + 1; --  first token in 
current nonterm
+      Memo      : Memo_Entry           := Parser.Derivs (R)(Start_Pos);
+
+      Pos_Recurse_Last : Base_Token_Index := Last_Pos;
+      Result_Recurse   : Memo_Entry;
+   begin
+      case Memo.State is
+      when Success =>
+         return Memo;
+
+      when Failure =>
+         return (State => Failure);
+
+      when No_Result =>
+         if Parser.Direct_Left_Recursive (R) then
+            Parser.Derivs (R).Replace_Element (Start_Pos, (State => Failure));
+         else
+            Memo := Eval (Parser, R, Last_Pos);
+            if Trace_Parse > Detail and then Memo.State = Success then
+               Parser.Trace.Put_Line (Parser.Tree.Image (Memo.Result, 
Descriptor, Include_Children => True));
+            end if;
+            Parser.Derivs (R).Replace_Element (Start_Pos, Memo);
+            return Memo;
+         end if;
+      end case;
+
+      loop
+         Pos := Last_Pos;
+
+         if Pos > Parser.Terminals.Last_Index then --  FIXME: this can't pass 
here; Last_Pos never > last_index
+            --  There might be an empty nonterm after the last token
+            return (State => Failure);
+         end if;
+
+         Result_Recurse := Eval (Parser, R, Pos);
+
+         if Result_Recurse.State = Success then
+            if Result_Recurse.Last_Pos > Pos_Recurse_Last then
+               Parser.Derivs (R).Replace_Element (Start_Pos, Result_Recurse);
+               Pos              := Result_Recurse.Last_Pos;
+               Pos_Recurse_Last := Pos;
+
+               if WisiToken.Trace_Parse > Detail then
+                  Parser.Trace.Put_Line
+                    (Parser.Tree.Image (Result_Recurse.Result, Descriptor, 
Include_Children => True));
+               end if;
+               --  continue looping
+
+            elsif Result_Recurse.Last_Pos = Pos_Recurse_Last then
+               if Parser.Tree.Is_Empty (Result_Recurse.Result) then
+                  Parser.Derivs (R).Replace_Element (Start_Pos, 
Result_Recurse);
+               end if;
+               exit;
+            else
+               --  Result_Recurse.Last_Pos < Pos_Recurse_Last
+               exit;
+            end if;
+         else
+            exit;
+         end if;
+      end loop;
+      return Parser.Derivs (R)(Start_Pos);
+   end Apply_Rule;
+
+   ----------
+   --  Public subprograms
+
+   function Create
+     (Grammar               : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Direct_Left_Recursive : in     Token_ID_Set;
+      Start_ID              : in     Token_ID;
+      Trace                 : access WisiToken.Trace'Class;
+      Lexer                 :        WisiToken.Lexer.Handle;
+      User_Data             :        WisiToken.Syntax_Trees.User_Data_Access)
+     return Procedural.Parser
+   is begin
+      return Parser : Procedural.Parser (Grammar.First_Index, 
Grammar.Last_Index) do
+         Parser.Trace                 := Trace;
+         Parser.Lexer                 := Lexer;
+         Parser.User_Data             := User_Data;
+         Parser.Grammar               := Grammar;
+         Parser.Start_ID              := Start_ID;
+         Parser.Direct_Left_Recursive := Direct_Left_Recursive;
+      end return;
+   end Create;
+
+   overriding procedure Parse (Parser : aliased in out Procedural.Parser)
+   is
+      Descriptor : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+
+      Junk : WisiToken.Syntax_Trees.Valid_Node_Index;
+      pragma Unreferenced (Junk);
+
+      Result : Memo_Entry;
+   begin
+      Parser.Base_Tree.Clear;
+      Parser.Tree.Initialize (Parser.Base_Tree'Unchecked_Access, Flush => 
True);
+      Parser.Lex_All;
+
+      for Nonterm in Descriptor.First_Nonterminal .. 
Parser.Trace.Descriptor.Last_Nonterminal loop
+         Parser.Derivs (Nonterm).Clear;
+         Parser.Derivs (Nonterm).Set_First (Parser.Terminals.First_Index);
+
+         --  There might be an empty nonterm after the last token
+         Parser.Derivs (Nonterm).Set_Last (Parser.Terminals.Last_Index + 1);
+      end loop;
+
+      for Token_Index in Parser.Terminals.First_Index .. 
Parser.Terminals.Last_Index loop
+         Junk := Parser.Tree.Add_Terminal (Token_Index, Parser.Terminals);
+         --  FIXME: move this into Lex_All, delete Terminals, just use 
Syntax_Tree
+      end loop;
+
+      Result := Apply_Rule (Parser, Parser.Start_ID, 
Parser.Terminals.First_Index - 1);
+
+      if Result.State /= Success then
+         if Trace_Parse > Outline then
+            Parser.Trace.Put_Line ("parse failed");
+         end if;
+
+         raise Syntax_Error with "parse failed"; --  FIXME: need better error 
message!
+      else
+         Parser.Tree.Set_Root (Result.Result);
+      end if;
+   end Parse;
+
+end WisiToken.Parse.Packrat.Procedural;
diff --git a/wisitoken-parse-packrat-procedural.ads 
b/wisitoken-parse-packrat-procedural.ads
new file mode 100644
index 0000000..bd6d86f
--- /dev/null
+++ b/wisitoken-parse-packrat-procedural.ads
@@ -0,0 +1,80 @@
+--  Abstract :
+--
+--  Procedural packrat parser, supporting only direct left recursion.
+--
+--  Coding style, algorithm is the same as generated by
+--  wisi-generate_packrat, but in procedural form.
+--
+--  References:
+--
+--  See 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);
+
+with WisiToken.Productions;
+package WisiToken.Parse.Packrat.Procedural is
+
+   --  These types duplicate Packrat.Generated. We keep them separate so
+   --  we can experiment with ways of implementing indirect left
+   --  recursion.
+
+   type Memo_State is (No_Result, Failure, Success);
+   subtype Result_States is Memo_State range Failure .. Success;
+
+   type Memo_Entry (State : Memo_State := No_Result) is record
+      case State is
+      when No_Result =>
+         null;
+
+      when Failure =>
+         null;
+
+      when Success =>
+         Result   : WisiToken.Syntax_Trees.Valid_Node_Index;
+         Last_Pos : Base_Token_Index;
+
+      end case;
+   end record;
+
+   package Memos is new SAL.Gen_Unbounded_Definite_Vectors (Token_Index, 
Memo_Entry);
+   type Derivs is array (Token_ID range <>) of Memos.Vector;
+
+   type Parser (First_Nonterminal, Last_Nonterminal : Token_ID) is new 
Packrat.Parser with
+   record
+      Grammar               : WisiToken.Productions.Prod_Arrays.Vector;
+      Start_ID              : Token_ID;
+      Direct_Left_Recursive : Token_ID_Set (First_Nonterminal .. 
Last_Nonterminal);
+      Derivs                : Procedural.Derivs (First_Nonterminal .. 
Last_Nonterminal);
+   end record;
+
+   function Create
+     (Grammar               : in     WisiToken.Productions.Prod_Arrays.Vector;
+      Direct_Left_Recursive : in     Token_ID_Set;
+      Start_ID              : in     Token_ID;
+      Trace                 : access WisiToken.Trace'Class;
+      Lexer                 :        WisiToken.Lexer.Handle;
+      User_Data             :        WisiToken.Syntax_Trees.User_Data_Access)
+     return Procedural.Parser;
+
+   overriding procedure Parse (Parser : aliased in out Procedural.Parser);
+   overriding function Any_Errors (Parser : in Procedural.Parser) return 
Boolean
+     is (False);
+   --  All errors are reported by Parse raising Syntax_Error.
+
+   overriding procedure Put_Errors (Parser : in Procedural.Parser)
+   is null;
+
+end WisiToken.Parse.Packrat.Procedural;
diff --git a/wisitoken-parse-packrat.adb b/wisitoken-parse-packrat.adb
new file mode 100644
index 0000000..fd5f586
--- /dev/null
+++ b/wisitoken-parse-packrat.adb
@@ -0,0 +1,56 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body WisiToken.Parse.Packrat is
+
+   overriding procedure Execute_Actions (Parser : in out Packrat.Parser)
+   is
+      Descriptor : WisiToken.Descriptor renames Parser.Trace.Descriptor.all;
+
+      procedure Process_Node
+        (Tree : in out Syntax_Trees.Tree;
+         Node : in     Syntax_Trees.Valid_Node_Index)
+      is
+         use all type Syntax_Trees.Node_Label;
+      begin
+         if Tree.Label (Node) /= Nonterm then
+            return;
+         end if;
+
+         declare
+            use all type Syntax_Trees.Semantic_Action;
+            Tree_Children : constant Syntax_Trees.Valid_Node_Index_Array := 
Tree.Children (Node);
+         begin
+            Parser.User_Data.Reduce (Tree, Node, Tree_Children);
+
+            if Tree.Action (Node) /= null then
+               Tree.Action (Node) (Parser.User_Data.all, Tree, Node, 
Tree_Children);
+            end if;
+         end;
+      end Process_Node;
+
+   begin
+      if Trace_Action > Outline then
+         Parser.Trace.Put_Line ("root node: " & Parser.Tree.Image 
(Parser.Tree.Root, Descriptor));
+      end if;
+
+      Parser.Tree.Process_Tree (Process_Node'Access);
+   end Execute_Actions;
+
+end WisiToken.Parse.Packrat;
diff --git a/wisitoken-parse-packrat.ads b/wisitoken-parse-packrat.ads
new file mode 100644
index 0000000..50ff2f0
--- /dev/null
+++ b/wisitoken-parse-packrat.ads
@@ -0,0 +1,71 @@
+--  Abstract :
+--
+--  Types and operations for a packrat parser runtime.
+--
+--  References:
+--
+--  [ford thesis] Bryan Ford thesis http://bford.info/pub/lang/thesis
+--
+--  [langkit]     AdaCore langkit   https://github.com/adacore/langkit
+--
+--  [tratt 2010]  http://tratt.net/laurie/research/pubs/papers/
+--                tratt__direct_left_recursive_parsing_expression_grammars.pdf
+--
+--  [warth 2008]  Warth, A., Douglass, J.R. and Millstein, T.D., 2008. Packrat
+--                parsers can support left recursion. PEPM, 8, pp.103-110.
+--
+--  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.
+
+--  Design:
+--
+--  [ford thesis] uses Haskell lazy evaluation, and does not use a
+--  lexer. We use a lexer to reduce the memory requirement. Although
+--  eliminating the lexer would make it easier to support additional
+--  syntax for a preprocessor or template generator.
+--
+--  [langkit] uses a lexer, and implements lazy evaluation via
+--  Memo_State, Memo_Entry as we do here, except that their result
+--  type is a specific AST type provided by a generic parameter; we
+--  use the general purpose Syntax_Tree.Tree type.
+--
+--  [langkit] also applies a memory optimization; it only saves the
+--  last 16 results for each nonterminal. We don't do that yet, so we
+--  can get some data on how well that works.
+
+pragma License (Modified_GPL);
+with WisiToken.Syntax_Trees;
+package WisiToken.Parse.Packrat is
+
+   function Tree_Index (Terminal_Index : in Token_Index) return 
Syntax_Trees.Valid_Node_Index
+     is (Syntax_Trees.Valid_Node_Index (Terminal_Index));
+   --  All tokens are read and entered into the syntax tree before any
+   --  nonterms are reduced, so the mapping from Terminals token_index to
+   --  Tree node_index is identity.
+
+   type Parser is abstract new Base_Parser with record
+      --  Dynamic parsing data
+
+      Base_Tree : aliased WisiToken.Syntax_Trees.Base_Tree;
+      Tree      : WisiToken.Syntax_Trees.Tree;
+      --  FIXME: Current we only need Base_Tree for Execute_Actions, except
+      --  that Syntax_Trees only declares the needed operations on Tree. But
+      --  we may need more trees for error recovery; if not, fix
+      --  Syntax_Trees, move Base_Tree and Execute_Actions up to
+      --  base_parser.
+
+   end record;
+
+   overriding procedure Execute_Actions (Parser : in out Packrat.Parser);
+
+end WisiToken.Parse.Packrat;
diff --git a/wisitoken-parse.adb b/wisitoken-parse.adb
new file mode 100644
index 0000000..0573a95
--- /dev/null
+++ b/wisitoken-parse.adb
@@ -0,0 +1,88 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+package body WisiToken.Parse is
+
+   function Next_Grammar_Token (Parser : in out Base_Parser) return Token_ID
+   is
+      use all type Syntax_Trees.User_Data_Access;
+
+      Token : Base_Token;
+      Error : Boolean;
+   begin
+      loop
+         Error := Parser.Lexer.Find_Next (Token);
+
+         --  We don't handle Error until later; we assume it was recovered.
+
+         if Parser.User_Data /= null then
+            Parser.User_Data.Lexer_To_Augmented (Token, Parser.Lexer);
+         end if;
+
+         if Token.Line /= Invalid_Line_Number then
+            --  Some lexers don't support line numbers.
+            if Parser.Lexer.First then
+               Parser.Line_Begin_Token.Set_Length (Ada.Containers.Count_Type 
(Token.Line));
+               Parser.Line_Begin_Token (Token.Line) := 
Parser.Terminals.Last_Index +
+                 (if Token.ID >= Parser.Trace.Descriptor.First_Terminal then 1 
else 0);
+
+            elsif Token.ID = Parser.Trace.Descriptor.EOF_ID then
+               Parser.Line_Begin_Token.Set_Length (Ada.Containers.Count_Type 
(Token.Line + 1));
+               Parser.Line_Begin_Token (Token.Line + 1) := 
Parser.Terminals.Last_Index + 1;
+            end if;
+         end if;
+
+         if Trace_Parse > Lexer_Debug then
+            Parser.Trace.Put_Line (Image (Token, Parser.Trace.Descriptor.all));
+         end if;
+
+         exit when Token.ID >= Parser.Trace.Descriptor.First_Terminal;
+      end loop;
+      Parser.Terminals.Append (Token);
+
+      if Error then
+         declare
+            Error : WisiToken.Lexer.Error renames 
Parser.Lexer.Errors.Reference (Parser.Lexer.Errors.Last);
+         begin
+            if Error.Recover_Char (1) /= ASCII.NUL then
+               Error.Recover_Token := Parser.Terminals.Last_Index;
+            end if;
+         end;
+      end if;
+
+      return Token.ID;
+   end Next_Grammar_Token;
+
+   procedure Lex_All (Parser : in out Base_Parser)
+   is
+      EOF_ID : constant Token_ID := Parser.Trace.Descriptor.EOF_ID;
+   begin
+      Parser.Lexer.Errors.Clear;
+      Parser.Terminals.Clear;
+      Parser.Line_Begin_Token.Clear;
+      loop
+         exit when EOF_ID = Next_Grammar_Token (Parser);
+      end loop;
+      if Trace_Parse > Outline then
+         Parser.Trace.Put_Line (Token_Index'Image 
(Parser.Terminals.Last_Index) & " tokens lexed");
+      end if;
+
+   end Lex_All;
+
+end WisiToken.Parse;
diff --git a/wisitoken-parse.ads b/wisitoken-parse.ads
new file mode 100644
index 0000000..271163f
--- /dev/null
+++ b/wisitoken-parse.ads
@@ -0,0 +1,66 @@
+--  Abstract :
+--
+--  Subprograms common to more than one parser, higher-level than in 
wisitoken.ads
+--
+--  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);
+
+with Ada.Finalization;
+with WisiToken.Lexer;
+with WisiToken.Syntax_Trees;
+package WisiToken.Parse is
+
+   type Base_Parser is abstract new Ada.Finalization.Limited_Controlled with 
record
+      Trace            : access WisiToken.Trace'Class;
+      Lexer            : WisiToken.Lexer.Handle;
+      User_Data        : WisiToken.Syntax_Trees.User_Data_Access;
+      Terminals        : aliased WisiToken.Base_Token_Arrays.Vector;
+      Line_Begin_Token : aliased WisiToken.Line_Begin_Token_Vectors.Vector;
+   end record;
+   --  Common to all parsers. Finalize should free any allocated objects.
+
+   function Next_Grammar_Token (Parser : in out Base_Parser) return Token_ID;
+   --  Get next token from Lexer, call User_Data.Lexer_To_Augmented. If
+   --  it is a grammar token, store in Terminals and return its id.
+   --  Otherwise, repeat.
+   --
+   --  Propagates Fatal_Error from Lexer.
+
+   procedure Lex_All (Parser : in out Base_Parser);
+   --  Clear Terminals, Line_Begin_Token; reset User_Data. Then call
+   --  Next_Grammar_Token repeatedly until EOF_ID is returned.
+   --
+   --  The user must first call Lexer.Reset_* to set the input text.
+
+   procedure Parse (Shared_Parser : aliased in out Base_Parser) is abstract;
+   --  Call Lex_All, then execute parse algorithm to parse the tokens,
+   --  storing the result in Parser for Execute_Actions.
+   --
+   --  If a parse error is encountered, raises Syntax_Error.
+   --  Parser.Lexer_Errors and Parser contain information about the
+   --  errors.
+   --
+   --  For other errors, raises Parse_Error with an appropriate error
+   --  message.
+
+   function Any_Errors (Parser : in Base_Parser) return Boolean is abstract;
+
+   procedure Put_Errors (Parser : in Base_Parser) is abstract;
+   --  Output error messages to Ada.Text_IO.Current_Error.
+
+   procedure Execute_Actions (Parser : in out Base_Parser) is abstract;
+   --  Execute all actions in Parser.Tree.
+
+end WisiToken.Parse;
diff --git a/wisitoken-productions.adb b/wisitoken-productions.adb
new file mode 100644
index 0000000..4d5079a
--- /dev/null
+++ b/wisitoken-productions.adb
@@ -0,0 +1,51 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2018 Stephe Leake
+--
+--  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.Text_IO;
+package body WisiToken.Productions is
+
+   function Image
+     (LHS        : in Token_ID;
+      RHS_Index  : in Natural;
+      RHS        : in Token_ID_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is
+      use Ada.Strings.Unbounded;
+      Result : Unbounded_String := +Trimmed_Image ((LHS, RHS_Index)) & ": " & 
Image (LHS, Descriptor) & " <=";
+   begin
+      for ID of RHS loop
+         Result := Result & ' ' & Image (ID, Descriptor);
+      end loop;
+      return To_String (Result);
+   end Image;
+
+   procedure Put (Grammar : Prod_Arrays.Vector; Descriptor : in 
WisiToken.Descriptor)
+   is begin
+      for P of Grammar loop
+         for R in P.RHSs.First_Index .. P.RHSs.Last_Index loop
+            Ada.Text_IO.Put_Line (Image (P.LHS, R, P.RHSs (R).Tokens, 
Descriptor));
+         end loop;
+      end loop;
+   end Put;
+
+end WisiToken.Productions;
diff --git a/wisitoken-productions.ads b/wisitoken-productions.ads
new file mode 100644
index 0000000..19f8dfa
--- /dev/null
+++ b/wisitoken-productions.ads
@@ -0,0 +1,64 @@
+--  Abstract :
+--
+--  Type and operations for building grammar productions.
+--
+--  Copyright (C) 2018 Stephe Leake
+--
+--  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 SAL.Gen_Unbounded_Definite_Vectors;
+with WisiToken.Semantic_Checks;
+with WisiToken.Syntax_Trees;
+package WisiToken.Productions is
+
+   type Right_Hand_Side is record
+      Tokens : Token_ID_Arrays.Vector;
+      Action : WisiToken.Syntax_Trees.Semantic_Action;
+      Check  : WisiToken.Semantic_Checks.Semantic_Check;
+   end record;
+
+   package RHS_Arrays is new SAL.Gen_Unbounded_Definite_Vectors (Natural, 
Right_Hand_Side);
+
+   type Instance is record
+      LHS  : Token_ID := Invalid_Token_ID;
+      RHSs : RHS_Arrays.Vector;
+   end record;
+
+   package Prod_Arrays is new SAL.Gen_Unbounded_Definite_Vectors (Token_ID, 
Instance);
+
+   function Image
+     (LHS        : in Token_ID;
+      RHS_Index  : in Natural;
+      RHS        : in Token_ID_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+   --  For comments in generated code, diagnostic messages.
+
+   procedure Put (Grammar : Prod_Arrays.Vector; Descriptor : in 
WisiToken.Descriptor);
+   --  Put Image of each production to Ada.Text_IO.Current_Output.
+
+   package Line_Number_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Natural, WisiToken.Line_Number_Type);
+
+   type Prod_Source_Line_Map is record
+      Line    : Line_Number_Type;
+      RHS_Map : Line_Number_Arrays.Vector;
+   end record;
+
+   package Source_Line_Maps is new SAL.Gen_Unbounded_Definite_Vectors 
(Token_ID, Prod_Source_Line_Map);
+   --  For line numbers of productions in source files.
+
+end WisiToken.Productions;
diff --git a/wisitoken-regexp.adb b/wisitoken-regexp.adb
new file mode 100644
index 0000000..b978bf9
--- /dev/null
+++ b/wisitoken-regexp.adb
@@ -0,0 +1,1347 @@
+--  Abstract:
+--
+--  See spec.
+--
+--  Copyright (C) 2015, 2017 Stephen Leake
+--  Copyright (C) 1998-2010, AdaCore
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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.
+--
+--  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.
+--
+--  You should have received a copy of the GNU General Public License
+--  and a copy of the GCC Runtime Library Exception distributed with
+--  the WisiToken package; see files GPL.txt and GPL_runtime.txt. If
+--  not, see <http://www.gnu.org/licenses/>.
+
+pragma License (Modified_GPL);
+
+with Ada.Unchecked_Deallocation;
+with GNAT.Case_Util;
+package body WisiToken.Regexp is
+
+   Initial_Max_States_In_Primary_Table : constant := 100;
+   --  Initial size for the number of states in the indefinite state
+   --  machine. The number of states will be increased as needed.
+   --
+   --  This is also used as the maximal number of meta states (groups of
+   --  states) in the secondary table.
+
+   Open_Paren    : constant Character := '(';
+   Close_Paren   : constant Character := ')';
+   Open_Bracket  : constant Character := '[';
+   Close_Bracket : constant Character := ']';
+
+   type State_Index is new Natural;
+   type Column_Index is new Natural;
+
+   type Regexp_Array is array
+     (State_Index range <>, Column_Index range <>) of State_Index;
+   --  First index is for the state number. Second index is for the character
+   --  type. Contents is the new State.
+
+   type Regexp_Array_Access is access Regexp_Array;
+   --  Use this type through the functions Set below, so that it can grow
+   --  dynamically depending on the needs.
+
+   type Mapping is array (Character'Range) of Column_Index;
+   --  Mapping between characters and column in the Regexp_Array
+
+   type Boolean_Array is array (State_Index range <>) of Boolean;
+
+   type Regexp_Value
+     (Alphabet_Size : Column_Index;
+      Num_States    : State_Index) is
+   record
+      Map            : Mapping;
+      States         : Regexp_Array (1 .. Num_States, 0 .. Alphabet_Size);
+      Is_Final       : Boolean_Array (1 .. Num_States);
+      Case_Sensitive : Boolean;
+      State          : State_Index;
+   end record;
+   --  Deterministic finite-state machine
+
+   -----------------------
+   -- Local Subprograms --
+   -----------------------
+
+   procedure Set
+     (Table  : in out Regexp_Array_Access;
+      State  : State_Index;
+      Column : Column_Index;
+      Value  : State_Index);
+   --  Sets a value in the table. If the table is too small, reallocate it
+   --  dynamically so that (State, Column) is a valid index in it.
+
+   function Get
+     (Table  : Regexp_Array_Access;
+      State  : State_Index;
+      Column : Column_Index) return State_Index;
+   --  Returns the value in the table at (State, Column). If this index does
+   --  not exist in the table, returns zero.
+
+   procedure Free is new Ada.Unchecked_Deallocation
+     (Regexp_Array, Regexp_Array_Access);
+
+   overriding procedure Adjust (R : in out Regexp) is
+      Tmp : Regexp_Access;
+   begin
+      if R.R /= null then
+         Tmp := new Regexp_Value (Alphabet_Size => R.R.Alphabet_Size,
+                                  Num_States    => R.R.Num_States);
+         Tmp.all := R.R.all;
+         R.R := Tmp;
+      end if;
+   end Adjust;
+
+   procedure Clear (R : in out Regexp)
+   is begin
+      R.R.State := 1;
+   end Clear;
+
+   function Compile
+     (Pattern        : String;
+      Case_Sensitive : Boolean := True) return Regexp
+   is
+      S : String := Pattern;
+      --  The pattern which is really compiled (when the pattern is case
+      --  insensitive, we convert this string to lower-cases
+
+      Map : Mapping := (others => 0);
+      --  Mapping between characters and columns in the tables
+
+      Alphabet_Size : Column_Index := 0;
+      --  Number of significant characters in the regular expression.
+      --  This total does not include special operators, such as *, (, ...
+
+      procedure Check_Well_Formed_Pattern;
+      --  Check that the pattern to compile is well-formed, so that subsequent
+      --  code can rely on this without performing each time the checks to
+      --  avoid accessing the pattern outside its bounds. However, not all
+      --  well-formedness rules are checked. In particular, rules about special
+      --  characters not being treated as regular characters are not checked.
+
+      procedure Create_Mapping;
+      --  Creates a mapping between characters in the regexp and columns
+      --  in the tables representing the regexp. Test that the regexp is
+      --  well-formed Modifies Alphabet_Size and Map
+
+      procedure Create_Primary_Table
+        (Table       : out Regexp_Array_Access;
+         Num_States  : out State_Index;
+         Start_State : out State_Index;
+         End_State   : out State_Index);
+      --  Creates the first version of the regexp (this is a non deterministic
+      --  finite state machine, which is unadapted for a fast pattern
+      --  matching algorithm). We use a recursive algorithm to process the
+      --  parenthesis sub-expressions.
+      --
+      --  Table : at the end of the procedure : Column 0 is for any character
+      --  ('.') and the last columns are for no character (closure). Num_States
+      --  is set to the number of states in the table Start_State is the number
+      --  of the starting state in the regexp End_State is the number of the
+      --  final state when the regexp matches.
+
+      function Create_Secondary_Table
+        (First_Table : Regexp_Array_Access;
+         Start_State : State_Index;
+         End_State   : State_Index) return Regexp;
+      --  Creates the definitive table representing the regular expression
+      --  This is actually a transformation of the primary table First_Table,
+      --  where every state is grouped with the states in its 'no-character'
+      --  columns. The transitions between the new states are then recalculated
+      --  and if necessary some new states are created.
+      --
+      --  Note that the resulting finite-state machine is not optimized in
+      --  terms of the number of states : it would be more time-consuming to
+      --  add a third pass to reduce the number of states in the machine, with
+      --  no speed improvement...
+
+      procedure Raise_Exception (M : String; Index : Integer);
+      pragma No_Return (Raise_Exception);
+      --  Raise an exception, indicating an error at character Index in S
+
+      -------------------------------
+      -- Check_Well_Formed_Pattern --
+      -------------------------------
+
+      procedure Check_Well_Formed_Pattern is
+         J : Integer;
+
+         Past_Elmt : Boolean := False;
+         --  Set to True everywhere an elmt has been parsed,
+         --  meaning there can be now an occurrence of '*', '+' and '?'.
+
+         Past_Term : Boolean := False;
+         --  Set to True everywhere a term has been parsed,
+         --  meaning there can be now an occurrence of '|'.
+
+         Parenthesis_Level : Integer := 0;
+
+         Last_Open : Integer := S'First - 1;
+         --  The last occurrence of an opening parenthesis.
+
+         procedure Raise_Exception_If_No_More_Chars (K : Integer := 0);
+         --  If no more characters are raised, call Raise_Exception
+
+         --------------------------------------
+         -- Raise_Exception_If_No_More_Chars --
+         --------------------------------------
+
+         procedure Raise_Exception_If_No_More_Chars (K : Integer := 0) is
+         begin
+            if J + K > S'Last then
+               Raise_Exception ("Ill-formed pattern while parsing", J);
+            end if;
+         end Raise_Exception_If_No_More_Chars;
+
+      --  Start of processing for Check_Well_Formed_Pattern
+
+      begin
+         J := S'First;
+         while J <= S'Last loop
+            case S (J) is
+            when Open_Bracket =>
+               J := J + 1;
+               Raise_Exception_If_No_More_Chars;
+
+               if S (J) = '^' then
+                  J := J + 1;
+                  Raise_Exception_If_No_More_Chars;
+               end if;
+
+               --  The first character never has a special meaning
+
+               if S (J) = ']' or else S (J) = '-' then
+                  J := J + 1;
+                  Raise_Exception_If_No_More_Chars;
+               end if;
+
+               --  The set of characters cannot be empty
+
+               if S (J) = ']' then
+                  Raise_Exception
+                    ("Set of characters cannot be empty in regular "
+                       & "expression", J);
+               end if;
+
+               declare
+                  Possible_Range_Start : Boolean := True;
+                  --  Set True everywhere a range character '-' can occur
+
+               begin
+                  loop
+                     exit when S (J) = Close_Bracket;
+
+                     --  The current character should be followed by a
+                     --  closing bracket.
+
+                     Raise_Exception_If_No_More_Chars (1);
+
+                     if S (J) = '-'
+                       and then S (J + 1) /= Close_Bracket
+                     then
+                        if not Possible_Range_Start then
+                           Raise_Exception
+                             ("No mix of ranges is allowed in "
+                                & "regular expression", J);
+                        end if;
+
+                        J := J + 1;
+                        Raise_Exception_If_No_More_Chars;
+
+                        --  Range cannot be followed by '-' character,
+                        --  except as last character in the set.
+
+                        Possible_Range_Start := False;
+
+                     else
+                        Possible_Range_Start := True;
+                     end if;
+
+                     if S (J) = '\' then
+                        J := J + 1;
+                        Raise_Exception_If_No_More_Chars;
+                     end if;
+
+                     J := J + 1;
+                  end loop;
+               end;
+
+               --  A closing bracket can end an elmt or term
+
+               Past_Elmt := True;
+               Past_Term := True;
+
+            when Close_Bracket =>
+
+               --  A close bracket must follow a open_bracket, and cannot be
+               --  found alone on the line.
+
+               Raise_Exception
+                 ("Incorrect character ']' in regular expression", J);
+
+            when '\' =>
+               if J < S'Last then
+                  J := J + 1;
+
+                  --  Any character can be an elmt or a term
+
+                  Past_Elmt := True;
+                  Past_Term := True;
+
+               else
+                  --  \ not allowed at the end of the regexp
+
+                  Raise_Exception
+                    ("Incorrect character '\' in regular expression", J);
+               end if;
+
+            when Open_Paren =>
+               Parenthesis_Level := Parenthesis_Level + 1;
+               Last_Open := J;
+
+               --  An open parenthesis does not end an elmt or term
+
+               Past_Elmt := False;
+               Past_Term := False;
+
+            when Close_Paren =>
+               Parenthesis_Level := Parenthesis_Level - 1;
+
+               if Parenthesis_Level < 0 then
+                  Raise_Exception
+                    ("')' is not associated with '(' in regular "
+                       & "expression", J);
+               end if;
+
+               if J = Last_Open + 1 then
+                  Raise_Exception
+                    ("Empty parentheses not allowed in regular "
+                       & "expression", J);
+               end if;
+
+               if not Past_Term then
+                  Raise_Exception
+                    ("Closing parenthesis not allowed here in regular "
+                       & "expression", J);
+               end if;
+
+               --  A closing parenthesis can end an elmt or term
+
+               Past_Elmt := True;
+               Past_Term := True;
+
+            when '{' =>
+               --  Any character can be an elmt or a term
+
+               Past_Elmt := True;
+               Past_Term := True;
+
+               --  No need to check for ',' as the code always accepts them
+
+            when '}' =>
+               --  Any character can be an elmt or a term
+
+               Past_Elmt := True;
+               Past_Term := True;
+
+            when '*' | '?' | '+' =>
+               --  These operators must apply to an elmt sub-expression,
+               --  and cannot be found if one has not just been parsed.
+
+               if not Past_Elmt then
+                  Raise_Exception
+                    ("'*', '+' and '?' operators must be "
+                       & "applied to an element in regular expression", J);
+               end if;
+
+               Past_Elmt := False;
+               Past_Term := True;
+
+            when '|' =>
+               --  This operator must apply to a term sub-expression,
+               --  and cannot be found if one has not just been parsed.
+
+               if not Past_Term then
+                  Raise_Exception
+                    ("'|' operator must be "
+                       & "applied to a term in regular expression", J);
+               end if;
+
+               Past_Elmt := False;
+               Past_Term := False;
+
+            when others =>
+               --  Any character can be an elmt or a term
+
+               Past_Elmt := True;
+               Past_Term := True;
+            end case;
+
+            J := J + 1;
+         end loop;
+
+         --  A closing parenthesis must follow an open parenthesis
+
+         if Parenthesis_Level /= 0 then
+            Raise_Exception
+              ("'(' must always be associated with a ')'", J);
+         end if;
+      end Check_Well_Formed_Pattern;
+
+      --------------------
+      -- Create_Mapping --
+      --------------------
+
+      procedure Create_Mapping is
+
+         procedure Add_In_Map (C : Character);
+         --  Add a character in the mapping, if it is not already defined
+
+         ----------------
+         -- Add_In_Map --
+         ----------------
+
+         procedure Add_In_Map (C : Character) is
+         begin
+            if Map (C) = 0 then
+               Alphabet_Size := Alphabet_Size + 1;
+               Map (C) := Alphabet_Size;
+            end if;
+         end Add_In_Map;
+
+         J                 : Integer := S'First;
+         Parenthesis_Level : Integer := 0;
+         Last_Open         : Integer := S'First - 1;
+
+      --  Start of processing for Create_Mapping
+
+      begin
+         while J <= S'Last loop
+            case S (J) is
+            when Open_Bracket =>
+               J := J + 1;
+
+               if S (J) = '^' then
+                  J := J + 1;
+               end if;
+
+               if S (J) = ']' or else S (J) = '-' then
+                  J := J + 1;
+               end if;
+
+               --  The first character never has a special meaning
+
+               loop
+                  if J > S'Last then
+                     Raise_Exception
+                       ("Ran out of characters while parsing ", J);
+                  end if;
+
+                  exit when S (J) = Close_Bracket;
+
+                  if S (J) = '-'
+                    and then S (J + 1) /= Close_Bracket
+                  then
+                     declare
+                        Start : constant Integer := J - 1;
+
+                     begin
+                        J := J + 1;
+
+                        if S (J) = '\' then
+                           J := J + 1;
+                        end if;
+
+                        for Char in S (Start) .. S (J) loop
+                           Add_In_Map (Char);
+                        end loop;
+                     end;
+                  else
+                     if S (J) = '\' then
+                        J := J + 1;
+                     end if;
+
+                     Add_In_Map (S (J));
+                  end if;
+
+                  J := J + 1;
+               end loop;
+
+               --  A close bracket must follow a open_bracket and cannot be
+               --  found alone on the line
+
+            when Close_Bracket =>
+               Raise_Exception
+                 ("Incorrect character ']' in regular expression", J);
+
+            when '\' =>
+               if J < S'Last  then
+                  J := J + 1;
+                  Add_In_Map (S (J));
+
+               else
+                  --  Back slash \ not allowed at the end of the regexp
+
+                  Raise_Exception
+                    ("Incorrect character '\' in regular expression", J);
+               end if;
+
+            when Open_Paren =>
+               Parenthesis_Level := Parenthesis_Level + 1;
+               Last_Open := J;
+
+            when Close_Paren =>
+               Parenthesis_Level := Parenthesis_Level - 1;
+
+               if Parenthesis_Level < 0 then
+                  Raise_Exception
+                    ("')' is not associated with '(' in regular "
+                       & "expression", J);
+               end if;
+
+               if J = Last_Open + 1 then
+                  Raise_Exception
+                    ("Empty parenthesis not allowed in regular "
+                       & "expression", J);
+               end if;
+
+            when '*' | '?' =>
+               if J = S'First then
+                  Raise_Exception
+                    ("'*', '+', '?' and '|' operators cannot be in "
+                       & "first position in regular expression", J);
+               end if;
+
+            when '|' | '+' =>
+               if J = S'First then
+
+                  --  These operators must apply to a sub-expression,
+                  --  and cannot be found at the beginning of the line
+
+                  Raise_Exception
+                    ("'*', '+', '?' and '|' operators cannot be in "
+                       & "first position in regular expression", J);
+               end if;
+
+            when others =>
+               Add_In_Map (S (J));
+            end case;
+
+            J := J + 1;
+         end loop;
+
+         --  A closing parenthesis must follow an open parenthesis
+
+         if Parenthesis_Level /= 0 then
+            Raise_Exception
+              ("'(' must always be associated with a ')'", J);
+         end if;
+
+      end Create_Mapping;
+
+      --------------------------
+      -- Create_Primary_Table --
+      --------------------------
+
+      procedure Create_Primary_Table
+        (Table       : out Regexp_Array_Access;
+         Num_States  : out State_Index;
+         Start_State : out State_Index;
+         End_State   : out State_Index)
+      is
+         Empty_Char : constant Column_Index := Alphabet_Size + 1;
+
+         Current_State : State_Index := 0;
+         --  Index of the last created state
+
+         procedure Add_Empty_Char
+           (State    : State_Index;
+            To_State : State_Index);
+         --  Add a empty-character transition from State to To_State
+
+         procedure Create_Repetition
+           (Repetition : Character;
+            Start_Prev : State_Index;
+            End_Prev   : State_Index;
+            New_Start  : out State_Index;
+            New_End    : in out State_Index);
+         --  Create the table in case we have a '*', '+' or '?'.
+         --  Start_Prev .. End_Prev should indicate respectively the start and
+         --  end index of the previous expression, to which '*', '+' or '?' is
+         --  applied.
+
+         procedure Create_Simple
+           (Start_Index : Integer;
+            End_Index   : Integer;
+            Start_State : out State_Index;
+            End_State   : out State_Index);
+         --  Fill the table for the regexp Simple. This is the recursive
+         --  procedure called to handle () expressions If End_State = 0, then
+         --  the call to Create_Simple creates an independent regexp, not a
+         --  concatenation Start_Index .. End_Index is the starting index in
+         --  the string S.
+         --
+         --  Warning: it may look like we are creating too many empty-string
+         --  transitions, but they are needed to get the correct regexp.
+         --  The table is filled as follow ( s means start-state, e means
+         --  end-state) :
+         --
+         --  regexp   state_num | a b * empty_string
+         --  -------  ------------------------------
+         --    a          1 (s) | 2 - - -
+         --               2 (e) | - - - -
+         --
+         --    ab         1 (s) | 2 - - -
+         --               2     | - - - 3
+         --               3     | - 4 - -
+         --               4 (e) | - - - -
+         --
+         --    a|b        1     | 2 - - -
+         --               2     | - - - 6
+         --               3     | - 4 - -
+         --               4     | - - - 6
+         --               5 (s) | - - - 1,3
+         --               6 (e) | - - - -
+         --
+         --    a*         1     | 2 - - -
+         --               2     | - - - 4
+         --               3 (s) | - - - 1,4
+         --               4 (e) | - - - 3
+         --
+         --    (a)        1 (s) | 2 - - -
+         --               2 (e) | - - - -
+         --
+         --    a+         1     | 2 - - -
+         --               2     | - - - 4
+         --               3 (s) | - - - 1
+         --               4 (e) | - - - 3
+         --
+         --    a?         1     | 2 - - -
+         --               2     | - - - 4
+         --               3 (s) | - - - 1,4
+         --               4 (e) | - - - -
+         --
+         --    .          1 (s) | 2 2 2 -
+         --               2 (e) | - - - -
+
+         function Next_Sub_Expression
+           (Start_Index : Integer;
+            End_Index   : Integer) return Integer;
+         --  Returns the index of the last character of the next sub-expression
+         --  in Simple. Index cannot be greater than End_Index.
+
+         --------------------
+         -- Add_Empty_Char --
+         --------------------
+
+         procedure Add_Empty_Char
+           (State    : State_Index;
+            To_State : State_Index)
+         is
+            J : Column_Index := Empty_Char;
+
+         begin
+            while Get (Table, State, J) /= 0 loop
+               J := J + 1;
+            end loop;
+
+            Set (Table, State, J, To_State);
+         end Add_Empty_Char;
+
+         -----------------------
+         -- Create_Repetition --
+         -----------------------
+
+         procedure Create_Repetition
+           (Repetition : Character;
+            Start_Prev : State_Index;
+            End_Prev   : State_Index;
+            New_Start  : out State_Index;
+            New_End    : in out State_Index)
+         is
+         begin
+            New_Start := Current_State + 1;
+
+            if New_End /= 0 then
+               Add_Empty_Char (New_End, New_Start);
+            end if;
+
+            Current_State := Current_State + 2;
+            New_End   := Current_State;
+
+            Add_Empty_Char (End_Prev, New_End);
+            Add_Empty_Char (New_Start, Start_Prev);
+
+            if Repetition /= '+' then
+               Add_Empty_Char (New_Start, New_End);
+            end if;
+
+            if Repetition /= '?' then
+               Add_Empty_Char (New_End, New_Start);
+            end if;
+         end Create_Repetition;
+
+         -------------------
+         -- Create_Simple --
+         -------------------
+
+         procedure Create_Simple
+           (Start_Index : Integer;
+            End_Index   : Integer;
+            Start_State : out State_Index;
+            End_State   : out State_Index)
+         is
+            J          : Integer := Start_Index;
+            Last_Start : State_Index := 0;
+
+         begin
+            Start_State := 0;
+            End_State   := 0;
+            while J <= End_Index loop
+               case S (J) is
+                  when Open_Paren =>
+                     declare
+                        J_Start    : constant Integer := J + 1;
+                        Next_Start : State_Index;
+                        Next_End   : State_Index;
+
+                     begin
+                        J := Next_Sub_Expression (J, End_Index);
+                        Create_Simple (J_Start, J - 1, Next_Start, Next_End);
+
+                        if J < End_Index
+                          and then (S (J + 1) = '*' or else
+                                    S (J + 1) = '+' or else
+                                    S (J + 1) = '?')
+                        then
+                           J := J + 1;
+                           Create_Repetition
+                             (S (J),
+                              Next_Start,
+                              Next_End,
+                              Last_Start,
+                              End_State);
+
+                        else
+                           Last_Start := Next_Start;
+
+                           if End_State /= 0 then
+                              Add_Empty_Char (End_State, Last_Start);
+                           end if;
+
+                           End_State := Next_End;
+                        end if;
+                     end;
+
+                  when '|' =>
+                     declare
+                        Start_Prev : constant State_Index := Start_State;
+                        End_Prev   : constant State_Index := End_State;
+                        Start_J    : constant Integer     := J + 1;
+                        Start_Next : State_Index := 0;
+                        End_Next   : State_Index := 0;
+
+                     begin
+                        J := Next_Sub_Expression (J, End_Index);
+
+                        --  Create a new state for the start of the alternative
+
+                        Current_State := Current_State + 1;
+                        Last_Start := Current_State;
+                        Start_State := Last_Start;
+
+                        --  Create the tree for the second part of alternative
+
+                        Create_Simple (Start_J, J, Start_Next, End_Next);
+
+                        --  Create the end state
+
+                        Add_Empty_Char (Last_Start, Start_Next);
+                        Add_Empty_Char (Last_Start, Start_Prev);
+                        Current_State := Current_State + 1;
+                        End_State := Current_State;
+                        Add_Empty_Char (End_Prev, End_State);
+                        Add_Empty_Char (End_Next, End_State);
+                     end;
+
+                  when Open_Bracket =>
+                     Current_State := Current_State + 1;
+
+                     declare
+                        Next_State : State_Index := Current_State + 1;
+
+                     begin
+                        J := J + 1;
+
+                        if S (J) = '^' then
+                           J := J + 1;
+
+                           Next_State := 0;
+
+                           for Column in 0 .. Alphabet_Size loop
+                              Set (Table, Current_State, Column,
+                                   Value => Current_State + 1);
+                           end loop;
+                        end if;
+
+                        --  Automatically add the first character
+
+                        if S (J) = '-' or else S (J) = ']' then
+                           Set (Table, Current_State, Map (S (J)),
+                                Value => Next_State);
+                           J := J + 1;
+                        end if;
+
+                        --  Loop till closing bracket found
+
+                        loop
+                           exit when S (J) = Close_Bracket;
+
+                           if S (J) = '-'
+                             and then S (J + 1) /= ']'
+                           then
+                              declare
+                                 Start : constant Integer := J - 1;
+
+                              begin
+                                 J := J + 1;
+
+                                 if S (J) = '\' then
+                                    J := J + 1;
+                                 end if;
+
+                                 for Char in S (Start) .. S (J) loop
+                                    Set (Table, Current_State, Map (Char),
+                                         Value => Next_State);
+                                 end loop;
+                              end;
+
+                           else
+                              if S (J) = '\' then
+                                 J := J + 1;
+                              end if;
+
+                              Set (Table, Current_State, Map (S (J)),
+                                   Value => Next_State);
+                           end if;
+                           J := J + 1;
+                        end loop;
+                     end;
+
+                     Current_State := Current_State + 1;
+
+                     --  If the next symbol is a special symbol
+
+                     if J < End_Index
+                       and then (S (J + 1) = '*' or else
+                                 S (J + 1) = '+' or else
+                                 S (J + 1) = '?')
+                     then
+                        J := J + 1;
+                        Create_Repetition
+                          (S (J),
+                           Current_State - 1,
+                           Current_State,
+                           Last_Start,
+                           End_State);
+
+                     else
+                        Last_Start := Current_State - 1;
+
+                        if End_State /= 0 then
+                           Add_Empty_Char (End_State, Last_Start);
+                        end if;
+
+                        End_State := Current_State;
+                     end if;
+
+                  when '*' | '+' | '?' | Close_Paren | Close_Bracket =>
+                     Raise_Exception
+                       ("Incorrect character in regular expression :", J);
+
+                  when others =>
+                     Current_State := Current_State + 1;
+
+                     --  Create the state for the symbol S (J)
+
+                     if S (J) = '.' then
+                        for K in 0 .. Alphabet_Size loop
+                           Set (Table, Current_State, K,
+                                Value => Current_State + 1);
+                        end loop;
+
+                     else
+                        if S (J) = '\' then
+                           J := J + 1;
+                        end if;
+
+                        Set (Table, Current_State, Map (S (J)),
+                             Value => Current_State + 1);
+                     end if;
+
+                     Current_State := Current_State + 1;
+
+                     --  If the next symbol is a special symbol
+
+                     if J < End_Index
+                       and then (S (J + 1) = '*' or else
+                                 S (J + 1) = '+' or else
+                                 S (J + 1) = '?')
+                     then
+                        J := J + 1;
+                        Create_Repetition
+                          (S (J),
+                           Current_State - 1,
+                           Current_State,
+                           Last_Start,
+                           End_State);
+
+                     else
+                        Last_Start := Current_State - 1;
+
+                        if End_State /= 0 then
+                           Add_Empty_Char (End_State, Last_Start);
+                        end if;
+
+                        End_State := Current_State;
+                     end if;
+
+               end case;
+
+               if Start_State = 0 then
+                  Start_State := Last_Start;
+               end if;
+
+               J := J + 1;
+            end loop;
+         end Create_Simple;
+
+         -------------------------
+         -- Next_Sub_Expression --
+         -------------------------
+
+         function Next_Sub_Expression
+           (Start_Index : Integer;
+            End_Index   : Integer) return Integer
+         is
+            J              : Integer := Start_Index;
+            Start_On_Alter : Boolean := False;
+
+         begin
+            if S (J) = '|' then
+               Start_On_Alter := True;
+            end if;
+
+            loop
+               exit when J = End_Index;
+               J := J + 1;
+
+               case S (J) is
+                  when '\' =>
+                     J := J + 1;
+
+                  when Open_Bracket =>
+                     loop
+                        J := J + 1;
+                        exit when S (J) = Close_Bracket;
+
+                        if S (J) = '\' then
+                           J := J + 1;
+                        end if;
+                     end loop;
+
+                  when Open_Paren =>
+                     J := Next_Sub_Expression (J, End_Index);
+
+                  when Close_Paren =>
+                     return J;
+
+                  when '|' =>
+                     if Start_On_Alter then
+                        return J - 1;
+                     end if;
+
+                  when others =>
+                     null;
+               end case;
+            end loop;
+
+            return J;
+         end Next_Sub_Expression;
+
+      --  Start of Create_Primary_Table
+
+      begin
+         Table.all := (others => (others => 0));
+         Create_Simple (S'First, S'Last, Start_State, End_State);
+         Num_States := Current_State;
+      end Create_Primary_Table;
+
+      ----------------------------
+      -- Create_Secondary_Table --
+      ----------------------------
+
+      function Create_Secondary_Table
+        (First_Table : Regexp_Array_Access;
+         Start_State : State_Index;
+         End_State   : State_Index) return Regexp
+      is
+         Last_Index : constant State_Index := First_Table'Last (1);
+
+         type Meta_State is array (0 .. Last_Index) of Boolean;
+         pragma Pack (Meta_State);
+         --  Whether a state from first_table belongs to a metastate.
+
+         No_States : constant Meta_State := (others => False);
+
+         type Meta_States_Array is array (State_Index range <>) of Meta_State;
+         type Meta_States_List is access all Meta_States_Array;
+         procedure Unchecked_Free is new Ada.Unchecked_Deallocation
+            (Meta_States_Array, Meta_States_List);
+         Meta_States : Meta_States_List;
+         --  Components of meta-states. A given state might belong to
+         --  several meta-states.
+         --  This array grows dynamically.
+
+         type Char_To_State is array (0 .. Alphabet_Size) of State_Index;
+         type Meta_States_Transition_Arr is
+            array (State_Index range <>) of Char_To_State;
+         type Meta_States_Transition is access all Meta_States_Transition_Arr;
+         procedure Unchecked_Free is new Ada.Unchecked_Deallocation
+           (Meta_States_Transition_Arr, Meta_States_Transition);
+         Table : Meta_States_Transition;
+         --  Documents the transitions between each meta-state. The
+         --  first index is the meta-state, the second column is the
+         --  character seen in the input, the value is the new meta-state.
+
+         Temp_State_Not_Null : Boolean;
+
+         Current_State       : State_Index := 1;
+         --  The current meta-state we are creating
+
+         Nb_State            : State_Index := 1;
+         --  The total number of meta-states created so far.
+
+         procedure Closure
+           (Meta_State : State_Index;
+            State      : State_Index);
+         --  Compute the closure of the state (that is every other state which
+         --  has a empty-character transition) and add it to the state
+
+         procedure Ensure_Meta_State (Meta : State_Index);
+         --  grows the Meta_States array as needed to make sure that there
+         --  is enough space to store the new meta state.
+
+         -----------------------
+         -- Ensure_Meta_State --
+         -----------------------
+
+         procedure Ensure_Meta_State (Meta : State_Index) is
+            Tmp  : Meta_States_List       := Meta_States;
+            Tmp2 : Meta_States_Transition := Table;
+
+         begin
+            if Meta_States = null then
+               Meta_States := new Meta_States_Array
+                  (1 .. State_Index'Max (Last_Index, Meta) + 1);
+               Meta_States (Meta_States'Range) := (others => No_States);
+
+               Table := new Meta_States_Transition_Arr
+                  (1 .. State_Index'Max (Last_Index, Meta) + 1);
+               Table.all := (others => (others => 0));
+
+            elsif Meta > Meta_States'Last then
+               Meta_States := new Meta_States_Array
+                  (1 .. State_Index'Max (2 * Tmp'Last, Meta));
+               Meta_States (Tmp'Range) := Tmp.all;
+               Meta_States (Tmp'Last + 1 .. Meta_States'Last) :=
+                  (others => No_States);
+               Unchecked_Free (Tmp);
+
+               Table := new Meta_States_Transition_Arr
+                  (1 .. State_Index'Max (2 * Tmp2'Last, Meta) + 1);
+               Table (Tmp2'Range) := Tmp2.all;
+               Table (Tmp2'Last + 1 .. Table'Last) :=
+                  (others => (others => 0));
+               Unchecked_Free (Tmp2);
+            end if;
+         end Ensure_Meta_State;
+
+         -------------
+         -- Closure --
+         -------------
+
+         procedure Closure
+           (Meta_State : State_Index;
+            State      : State_Index)
+         is
+         begin
+            if not Meta_States (Meta_State)(State) then
+               Meta_States (Meta_State)(State) := True;
+
+               --  For each transition on empty-character
+
+               for Column in Alphabet_Size + 1 .. First_Table'Last (2) loop
+                  exit when First_Table (State, Column) = 0;
+                  Closure (Meta_State, First_Table (State, Column));
+               end loop;
+            end if;
+         end Closure;
+
+      --  Start of processing for Create_Secondary_Table
+
+      begin
+         --  Create a new state
+
+         Ensure_Meta_State (Current_State);
+         Closure (Current_State, Start_State);
+
+         while Current_State <= Nb_State loop
+
+            --  We will be trying, below, to create the next meta-state
+
+            Ensure_Meta_State (Nb_State + 1);
+
+            --  For every character in the regexp, calculate the possible
+            --  transitions from Current_State.
+
+            for Column in 0 .. Alphabet_Size loop
+               Temp_State_Not_Null := False;
+
+               for K in Meta_States (Current_State)'Range loop
+                  if Meta_States (Current_State)(K)
+                    and then First_Table (K, Column) /= 0
+                  then
+                     Closure (Nb_State + 1, First_Table (K, Column));
+                     Temp_State_Not_Null := True;
+                  end if;
+               end loop;
+
+               --  If at least one transition existed
+
+               if Temp_State_Not_Null then
+
+                  --  Check if this new state corresponds to an old one
+
+                  for K in 1 .. Nb_State loop
+                     if Meta_States (K) = Meta_States (Nb_State + 1) then
+                        Table (Current_State)(Column) := K;
+
+                        --  Reset data, for the next time we try that state
+
+                        Meta_States (Nb_State + 1) := No_States;
+                        exit;
+                     end if;
+                  end loop;
+
+                  --  If not, create a new state
+
+                  if Table (Current_State)(Column) = 0 then
+                     Nb_State := Nb_State + 1;
+                     Ensure_Meta_State (Nb_State + 1);
+                     Table (Current_State)(Column) := Nb_State;
+                  end if;
+               end if;
+            end loop;
+
+            Current_State := Current_State + 1;
+         end loop;
+
+         --  Returns the regexp
+
+         declare
+            R : Regexp_Access;
+
+         begin
+            R := new Regexp_Value (Alphabet_Size => Alphabet_Size,
+                                   Num_States    => Nb_State);
+            R.Map            := Map;
+            R.Case_Sensitive := Case_Sensitive;
+
+            for S in 1 .. Nb_State loop
+               R.Is_Final (S) := Meta_States (S)(End_State);
+            end loop;
+
+            for State in 1 .. Nb_State loop
+               for K in 0 .. Alphabet_Size loop
+                  R.States (State, K) := Table (State)(K);
+               end loop;
+            end loop;
+
+            Unchecked_Free (Meta_States);
+            Unchecked_Free (Table);
+
+            return (Ada.Finalization.Controlled with R => R);
+         end;
+      end Create_Secondary_Table;
+
+      ---------------------
+      -- Raise_Exception --
+      ---------------------
+
+      procedure Raise_Exception (M : String; Index : Integer) is
+      begin
+         raise Error_In_Regexp with M & " at offset" & Index'Img;
+      end Raise_Exception;
+
+   --  Start of processing for Compile
+
+   begin
+      if S = "" then
+         raise Error_In_Regexp with "empty string";
+      end if;
+
+      if not Case_Sensitive then
+         GNAT.Case_Util.To_Lower (S);
+      end if;
+
+      --  Check the pattern is well-formed before any treatment
+
+      Check_Well_Formed_Pattern;
+
+      Create_Mapping;
+
+      --  Creates the primary table
+
+      declare
+         Table       : Regexp_Array_Access;
+         Num_States  : State_Index;
+         Start_State : State_Index;
+         End_State   : State_Index;
+         R           : Regexp;
+
+      begin
+         Table := new Regexp_Array (1 .. Initial_Max_States_In_Primary_Table,
+                                    0 .. Alphabet_Size + 10);
+
+         Create_Primary_Table (Table, Num_States, Start_State, End_State);
+
+         --  Creates the secondary table
+
+         R := Create_Secondary_Table (Table, Start_State, End_State);
+         Free (Table);
+         return R;
+      end;
+   end Compile;
+
+   overriding procedure Finalize (R : in out Regexp) is
+      procedure Free is new
+        Ada.Unchecked_Deallocation (Regexp_Value, Regexp_Access);
+   begin
+      Free (R.R);
+   end Finalize;
+
+   function Get
+     (Table  : Regexp_Array_Access;
+      State  : State_Index;
+      Column : Column_Index) return State_Index
+   is
+   begin
+      if State <= Table'Last (1)
+        and then Column <= Table'Last (2)
+      then
+         return Table (State, Column);
+      else
+         return 0;
+      end if;
+   end Get;
+
+   -----------
+   -- Match --
+   -----------
+
+   function Match (R : in out Regexp; S : String; Next : Integer) return 
Match_State
+   is begin
+      if R.R = null then
+         raise Constraint_Error;
+      end if;
+
+      if R.R.State = 0 then
+         return Error;
+      end if;
+
+      if R.R.Case_Sensitive then
+         R.R.State := R.R.States (R.R.State, R.R.Map (S (Next)));
+      else
+         R.R.State :=
+           R.R.States (R.R.State,
+                       R.R.Map (GNAT.Case_Util.To_Lower (S (Next))));
+      end if;
+
+      if R.R.State = 0 then
+         return Error;
+      elsif R.R.Is_Final (R.R.State) then
+         return Final;
+      else
+         return Matching;
+      end if;
+   end Match;
+
+   procedure Set
+     (Table  : in out Regexp_Array_Access;
+      State  : State_Index;
+      Column : Column_Index;
+      Value  : State_Index)
+   is
+      New_Lines   : State_Index;
+      New_Columns : Column_Index;
+      New_Table   : Regexp_Array_Access;
+
+   begin
+      if State <= Table'Last (1)
+        and then Column <= Table'Last (2)
+      then
+         Table (State, Column) := Value;
+      else
+         --  Doubles the size of the table until it is big enough that
+         --  (State, Column) is a valid index.
+
+         New_Lines := Table'Last (1) * (State / Table'Last (1) + 1);
+         New_Columns := Table'Last (2) * (Column / Table'Last (2) + 1);
+         New_Table := new Regexp_Array (Table'First (1) .. New_Lines,
+                                        Table'First (2) .. New_Columns);
+         New_Table.all := (others => (others => 0));
+
+         for J in Table'Range (1) loop
+            for K in Table'Range (2) loop
+               New_Table (J, K) := Table (J, K);
+            end loop;
+         end loop;
+
+         Free (Table);
+         Table := New_Table;
+         Table (State, Column) := Value;
+      end if;
+   end Set;
+
+   function State (R : in Regexp) return Match_State
+   is begin
+      if R.R.State = 0 then
+         return Error;
+      elsif R.R.Is_Final (R.R.State) then
+         return Final;
+      else
+         return Matching;
+      end if;
+   end State;
+
+end WisiToken.Regexp;
+--  Local Variables:
+--  jit-lock-defer-time: 0.5
+--  End:
diff --git a/wisitoken-regexp.ads b/wisitoken-regexp.ads
new file mode 100644
index 0000000..b7cf9ba
--- /dev/null
+++ b/wisitoken-regexp.ads
@@ -0,0 +1,139 @@
+--  Abstract:
+--
+--  Regular expressions, for WisiToken.Lexer.Regexp. It supports a
+--  subset of the syntax of regular expressions copied from familiar
+--  Unix style utilities.
+--
+--  Copied from GNAT System.Regexp, modified to expose the matcher
+--  state machine to allow matching substrings.
+--
+--  Copyright (C) 2015 Stephen Leake
+--  Copyright (C) 1998-2010, AdaCore
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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.
+--
+--  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.
+--
+--  You should have received a copy of the
+--  GNU General Public License and
+--  a copy of the GCC Runtime Library Exception distributed with the WisiToken 
package;
+--  see files GPL.txt and GPL_runtime.txt. If not, see
+--  <http://www.gnu.org/licenses/>.
+
+pragma License (Modified_GPL);
+
+with Ada.Finalization;
+package WisiToken.Regexp is
+
+   --  The regular expression must first be compiled, using the Compile
+   --  function, which creates a finite state matching table, allowing
+   --  very fast matching.
+
+   --  The following is the form of a regular expression, expressed in Ada
+   --  reference manual style BNF
+
+   --     regexp ::= term
+
+   --     regexp ::= term | term          -- alternation (term or term ...)
+
+   --     term ::= item
+
+   --     term ::= item item ...          -- concatenation (item then item)
+
+   --     item ::= elmt                   -- match elmt
+   --     item ::= elmt *                 -- zero or more elmt's
+   --     item ::= elmt +                 -- one or more elmt's
+   --     item ::= elmt ?                 -- matches elmt or nothing
+
+   --     elmt ::= nchr                   -- matches given character
+   --     elmt ::= [nchr nchr ...]        -- matches any character listed
+   --     elmt ::= [^ nchr nchr ...]      -- matches any character not listed
+   --     elmt ::= [char - char]          -- matches chars in given range
+   --     elmt ::= .                      -- matches any single character
+   --     elmt ::= ( regexp )             -- parens used for grouping
+
+   --     char ::= any character, including special characters
+   --     nchr ::= any character except \()[].*+?^ or \char to match char
+   --     ... is used to indication repetition (one or more terms)
+
+   --  See also regexp(1) man page on Unix systems for further details
+
+   type Regexp is private;
+
+   Error_In_Regexp : exception;
+
+   function Compile
+     (Pattern        : String;
+      Case_Sensitive : Boolean := True)
+     return Regexp;
+   --  Compile a Pattern. If the syntax of the given
+   --  expression is invalid (does not match above grammar), Error_In_Regexp
+   --  is raised.
+   --
+   --  If Pattern is the empty string it will raise Error_In_Regexp
+   --
+   --  Raises Error_In_Regexp when an error is found in the regular
+   --  expression
+
+   procedure Clear (R : in out Regexp);
+   --  Clear R internal state, to prepare for a new Match.
+
+   type Match_State is (Matching, Final, Error);
+
+   function Match (R : in out Regexp; S : String; Next : Integer) return 
Match_State;
+   --  Compute match for S (Next), assuming Next is the next character
+   --  in S after the previous call to Match. If this is the first
+   --  call, Next must be S'First.
+   --
+   --  S'First, S'Last may change between calls to Match.
+   --
+   --  The return values mean:
+   --
+   --  Matching: S (S'First .. Next) matches R, but more characters
+   --  are needed to complete the match.
+   --
+   --  Final: S (S'First .. Next) matches R; more characters may also.
+   --
+   --  Error: S (S'First .. Next) does not match R.
+   --
+   --  Raises Constraint_Error if R is not a compiled regular
+   --  expression.
+
+   function State (R : in Regexp) return Match_State;
+   --  Return the current state of R; if Matching, it is useful to
+   --  call Match again.
+   --
+   --  After Clear (R), State (R) returns Matching.
+
+private
+   type Regexp_Value;
+
+   type Regexp_Access is access Regexp_Value;
+
+   type Regexp is new Ada.Finalization.Controlled with record
+      R : Regexp_Access := null;
+   end record;
+
+   pragma Finalize_Storage_Only (Regexp);
+
+   overriding procedure Finalize (R : in out Regexp);
+   --  Free the memory occupied by R
+
+   overriding procedure Adjust (R : in out Regexp);
+   --  Deep copy R.R.all
+
+end WisiToken.Regexp;
diff --git a/wisitoken-semantic_checks.adb b/wisitoken-semantic_checks.adb
new file mode 100644
index 0000000..af83b7f
--- /dev/null
+++ b/wisitoken-semantic_checks.adb
@@ -0,0 +1,135 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017, 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);
+
+with Ada.Characters.Handling;
+package body WisiToken.Semantic_Checks is
+
+   function Image (Item : in Check_Status; Descriptor : in 
WisiToken.Descriptor) return String
+   is begin
+      case Item.Label is
+      when Ok =>
+         return Check_Status_Label'Image (Item.Label);
+      when Error =>
+         return '(' & Check_Status_Label'Image (Item.Label) & ", " &
+           Image (Item.Begin_Name, Descriptor) & ',' &
+           Image (Item.End_Name, Descriptor) & ')';
+      end case;
+   end Image;
+
+   function Match_Names
+     (Lexer        : access constant WisiToken.Lexer.Instance'Class;
+      Descriptor   : in     WisiToken.Descriptor;
+      Tokens       : in     Recover_Token_Array;
+      Start_Index  : in     Positive_Index_Type;
+      End_Index    : in     Positive_Index_Type;
+      End_Optional : in     Boolean)
+     return Check_Status
+   is
+      Start_Name_Region : constant Buffer_Region :=
+        (if Tokens (Start_Index).Name = Null_Buffer_Region
+         then Tokens (Start_Index).Byte_Region
+         else Tokens (Start_Index).Name);
+      End_Name_Region : constant Buffer_Region :=
+        (if Tokens (End_Index).Name = Null_Buffer_Region
+         then Tokens (End_Index).Byte_Region
+         else Tokens (End_Index).Name);
+
+      function Equal return Boolean
+      is
+         use Ada.Characters.Handling;
+      begin
+         if Descriptor.Case_Insensitive then
+            return To_Lower (Lexer.Buffer_Text (Start_Name_Region)) =
+              To_Lower (Lexer.Buffer_Text (End_Name_Region));
+         else
+            return Lexer.Buffer_Text (Start_Name_Region) = Lexer.Buffer_Text 
(End_Name_Region);
+         end if;
+      end Equal;
+
+   begin
+      if Tokens (Start_Index).Virtual or Tokens (End_Index).Virtual then
+         return (Label => Ok);
+
+      elsif End_Optional then
+         if End_Name_Region = Null_Buffer_Region then
+            return (Label => Ok);
+         elsif Start_Name_Region = Null_Buffer_Region then
+            return (Extra_Name_Error, Tokens (Start_Index), Tokens 
(End_Index));
+         else
+            if Equal then
+               return (Label => Ok);
+            else
+               return (Match_Names_Error, Tokens (Start_Index), Tokens 
(End_Index));
+            end if;
+         end if;
+
+      else
+         if Start_Name_Region = Null_Buffer_Region then
+            if End_Name_Region = Null_Buffer_Region then
+               return (Label => Ok);
+            else
+               return (Extra_Name_Error, Tokens (Start_Index), Tokens 
(End_Index));
+            end if;
+
+         elsif End_Name_Region = Null_Buffer_Region then
+            return (Missing_Name_Error, Tokens (Start_Index), Tokens 
(End_Index));
+
+         else
+            if Equal then
+               return (Label => Ok);
+            else
+               return (Match_Names_Error, Tokens (Start_Index), Tokens 
(End_Index));
+            end if;
+         end if;
+      end if;
+   end Match_Names;
+
+   function Propagate_Name
+     (Nonterm    : in out Recover_Token;
+      Tokens     : in     Recover_Token_Array;
+      Name_Index : in     Positive_Index_Type)
+     return Check_Status
+   is begin
+      if Tokens (Name_Index).Name = Null_Buffer_Region then
+         Nonterm.Name := Tokens (Name_Index).Byte_Region;
+      else
+         Nonterm.Name := Tokens (Name_Index).Name;
+      end if;
+      return (Label => Ok);
+   end Propagate_Name;
+
+   function Merge_Names
+     (Nonterm     : in out Recover_Token;
+      Tokens      : in     Recover_Token_Array;
+      First_Index : in     Positive_Index_Type;
+      Last_Index  : in     Positive_Index_Type)
+     return Check_Status
+   is
+      First_Name : Buffer_Region renames Tokens (First_Index).Name;
+      Last_Name  : Buffer_Region renames Tokens (Last_Index).Name;
+   begin
+      Nonterm.Name :=
+        First_Name and
+          (if Last_Name = Null_Buffer_Region
+           then Tokens (Last_Index).Byte_Region
+           else Last_Name);
+      return (Label => Ok);
+   end Merge_Names;
+
+end WisiToken.Semantic_Checks;
diff --git a/wisitoken-semantic_checks.ads b/wisitoken-semantic_checks.ads
new file mode 100644
index 0000000..49811d6
--- /dev/null
+++ b/wisitoken-semantic_checks.ads
@@ -0,0 +1,89 @@
+--  Abstract :
+--
+--  Grammar semantic check routines.
+--
+--  Copyright (C) 2017, 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);
+
+with WisiToken.Lexer;
+package WisiToken.Semantic_Checks is
+
+   type Check_Status_Label is
+     (Ok,
+      Missing_Name_Error, -- block start has name, required block end name 
missing
+      Extra_Name_Error,   -- block start has no name, end has one
+      Match_Names_Error); -- both names present, but don't match
+
+   subtype Error is Check_Status_Label range Check_Status_Label'Succ (Ok) .. 
Check_Status_Label'Last;
+
+   type Check_Status (Label : Check_Status_Label := Ok) is record
+      case Label is
+      when Ok =>
+         null;
+
+      when Error =>
+         Begin_Name : Recover_Token;
+         End_Name   : Recover_Token;
+      end case;
+   end record;
+
+   subtype Error_Check_Status is Check_Status
+   with Dynamic_Predicate => Error_Check_Status.Label /= Ok;
+
+   function Image (Item : in Check_Status; Descriptor : WisiToken.Descriptor) 
return String;
+
+   type Semantic_Check is access function
+     (Lexer   : access constant WisiToken.Lexer.Instance'Class;
+      Nonterm : in out Recover_Token;
+      Tokens  : in     Recover_Token_Array)
+     return Check_Status;
+   --  Called during parsing and error recovery to implement higher level
+   --  checks, such as block name matching in Ada.
+
+   Null_Check : constant Semantic_Check := null;
+
+   function Match_Names
+     (Lexer        : access constant WisiToken.Lexer.Instance'Class;
+      Descriptor   : in     WisiToken.Descriptor;
+      Tokens       : in     Recover_Token_Array;
+      Start_Index  : in     Positive_Index_Type;
+      End_Index    : in     Positive_Index_Type;
+      End_Optional : in     Boolean)
+     return Check_Status;
+   --  Check that buffer text at Tokens (Start_Index).Name matches buffer
+   --  text at Tokens (End_Index).Name. Comparison is controlled by
+   --  Descriptor.Case_Insensitive.
+
+   function Propagate_Name
+     (Nonterm    : in out Recover_Token;
+      Tokens     : in     Recover_Token_Array;
+      Name_Index : in     Positive_Index_Type)
+     return Check_Status;
+   --  Set Nonterm.Name to Tokens (Name_Index).Name, or .Byte_Region, if
+   --  .Name is Null_Buffer_Region. Return Ok.
+
+   function Merge_Names
+     (Nonterm     : in out Recover_Token;
+      Tokens      : in     Recover_Token_Array;
+      First_Index : in     Positive_Index_Type;
+      Last_Index  : in     Positive_Index_Type)
+     return Check_Status;
+   --  Then set Nonterm.Name to the merger of Tokens (First_Index ..
+   --  Last_Index).Name, return Ok.
+   --
+   --  If Tokens (Last_Index).Name is Null_Buffer_Region, use Tokens
+   --  (Last_Index).Byte_Region instead.
+
+end WisiToken.Semantic_Checks;
diff --git a/wisitoken-syntax_trees.adb b/wisitoken-syntax_trees.adb
new file mode 100644
index 0000000..249065f
--- /dev/null
+++ b/wisitoken-syntax_trees.adb
@@ -0,0 +1,1114 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Containers;
+package body WisiToken.Syntax_Trees is
+
+   --  Body specs, alphabetical, as needed
+
+   function Image
+     (Tree             : in Syntax_Trees.Tree;
+      N                : in Syntax_Trees.Node;
+      Descriptor       : in WisiToken.Descriptor;
+      Include_Children : in Boolean)
+     return String;
+
+   function Min (Item : in Valid_Node_Index_Array) return Valid_Node_Index;
+
+   procedure Move_Branch_Point (Tree : in out Syntax_Trees.Tree; Required_Node 
: in Valid_Node_Index);
+
+   function Process_Tree
+     (Tree         : in Syntax_Trees.Tree;
+      Node         : in Valid_Node_Index;
+      Process_Node : access function
+        (Tree : in Syntax_Trees.Tree;
+         Node : in Valid_Node_Index)
+        return Boolean)
+     return Boolean;
+   --  Call Process_Node on nodes in tree rooted at Node. Return when
+   --  Process_Node returns False (Process_Tree returns False), or when
+   --  all nodes have been processed (Process_Tree returns True).
+
+   procedure Set_Children
+     (Nodes    : in out Node_Arrays.Vector;
+      Parent   : in     Valid_Node_Index;
+      Children : in     Valid_Node_Index_Array);
+
+   ----------
+   --  Public and body operations, alphabetical
+
+   function Action
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return Semantic_Action
+   is begin
+      return
+        (if Node <= Tree.Last_Shared_Node
+         then Tree.Shared_Tree.Nodes (Node).Action
+         else Tree.Branched_Nodes (Node).Action);
+   end Action;
+
+   function Add_Nonterm
+     (Tree            : in out Syntax_Trees.Tree;
+      Production      : in     Production_ID;
+      Children        : in     Valid_Node_Index_Array;
+      Action          : in     Semantic_Action;
+      Default_Virtual : in     Boolean)
+     return Valid_Node_Index
+   is
+      Nonterm_Node : Valid_Node_Index;
+   begin
+      if Tree.Flush then
+         Tree.Shared_Tree.Nodes.Append
+           ((Label      => Syntax_Trees.Nonterm,
+             ID         => Production.LHS,
+             Action     => Action,
+             RHS_Index  => Production.RHS,
+             Virtual    => (if Children'Length = 0 then Default_Virtual else 
False),
+             others     => <>));
+         Tree.Last_Shared_Node := Tree.Shared_Tree.Nodes.Last_Index;
+         Nonterm_Node          := Tree.Last_Shared_Node;
+      else
+         Tree.Branched_Nodes.Append
+           ((Label     => Syntax_Trees.Nonterm,
+             ID        => Production.LHS,
+             Action    => Action,
+             RHS_Index => Production.RHS,
+             Virtual   => (if Children'Length = 0 then Default_Virtual else 
False),
+             others    => <>));
+         Nonterm_Node := Tree.Branched_Nodes.Last_Index;
+      end if;
+
+      if Children'Length = 0 then
+         return Nonterm_Node;
+      end if;
+
+      if Tree.Flush then
+         Set_Children (Tree.Shared_Tree.Nodes, Nonterm_Node, Children);
+
+      else
+         declare
+            Min_Child_Node : constant Valid_Node_Index := Min (Children);
+         begin
+            if Min_Child_Node <= Tree.Last_Shared_Node then
+               Move_Branch_Point (Tree, Min_Child_Node);
+            end if;
+         end;
+
+         Set_Children (Tree.Branched_Nodes, Nonterm_Node, Children);
+      end if;
+
+      return Nonterm_Node;
+   end Add_Nonterm;
+
+   function Add_Terminal
+     (Tree      : in out Syntax_Trees.Tree;
+      Terminal  : in     Token_Index;
+      Terminals : in     Base_Token_Arrays.Vector)
+     return Valid_Node_Index
+   is begin
+      if Tree.Flush then
+         Tree.Shared_Tree.Nodes.Append
+           ((Label       => Shared_Terminal,
+             ID          => Terminals (Terminal).ID,
+             Byte_Region => Terminals (Terminal).Byte_Region,
+             Terminal    => Terminal,
+             others      => <>));
+         Tree.Last_Shared_Node := Tree.Shared_Tree.Nodes.Last_Index;
+         return Tree.Last_Shared_Node;
+      else
+         Tree.Branched_Nodes.Append
+           ((Label       => Shared_Terminal,
+             ID          => Terminals (Terminal).ID,
+             Byte_Region => Terminals (Terminal).Byte_Region,
+             Terminal    => Terminal,
+             others      => <>));
+         return Tree.Branched_Nodes.Last_Index;
+      end if;
+   end Add_Terminal;
+
+   function Add_Terminal
+     (Tree     : in out Syntax_Trees.Tree;
+      Terminal : in     Token_ID)
+     return Valid_Node_Index
+   is begin
+      if Tree.Flush then
+         Tree.Shared_Tree.Nodes.Append
+           ((Label  => Virtual_Terminal,
+             ID     => Terminal,
+             others => <>));
+         Tree.Last_Shared_Node := Tree.Shared_Tree.Nodes.Last_Index;
+         return Tree.Last_Shared_Node;
+      else
+         Tree.Branched_Nodes.Append
+           ((Label  => Virtual_Terminal,
+             ID     => Terminal,
+             others => <>));
+         return Tree.Branched_Nodes.Last_Index;
+      end if;
+   end Add_Terminal;
+
+   overriding procedure Adjust (Tree : in out Base_Tree)
+   is begin
+      if Tree.Augmented_Present then
+         --  Augmented is only set after parsing is complete; trees are never 
copied then.
+         raise SAL.Not_Implemented;
+      end if;
+   end Adjust;
+
+   function Augmented
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return Base_Token_Class_Access
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Augmented;
+      else
+         return Tree.Branched_Nodes (Node).Augmented;
+      end if;
+   end Augmented;
+
+   function Byte_Region
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return WisiToken.Buffer_Region
+   is begin
+      return
+        (if Node <= Tree.Last_Shared_Node
+         then Tree.Shared_Tree.Nodes (Node).Byte_Region
+         else Tree.Branched_Nodes (Node).Byte_Region);
+   end Byte_Region;
+
+   function Children (N : in Syntax_Trees.Node) return Valid_Node_Index_Array
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if N.Children.Length = 0 then
+         return (1 .. 0 => <>);
+      else
+         return Result : Valid_Node_Index_Array (N.Children.First_Index .. 
N.Children.Last_Index) do
+            for I in Result'Range loop
+               Result (I) := N.Children (I);
+            end loop;
+         end return;
+      end if;
+   end Children;
+
+   function Children (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Valid_Node_Index_Array
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Children (Tree.Shared_Tree.Nodes (Node));
+      else
+         return Children (Tree.Branched_Nodes (Node));
+      end if;
+   end Children;
+
+   procedure Clear (Tree : in out Syntax_Trees.Base_Tree)
+   is begin
+      Tree.Finalize;
+   end Clear;
+
+   procedure Clear (Tree : in out Syntax_Trees.Tree)
+   is begin
+      if Tree.Shared_Tree.Augmented_Present then
+         for Node of Tree.Branched_Nodes loop
+            if Node.Label = Nonterm then
+               Free (Node.Augmented);
+            end if;
+         end loop;
+      end if;
+      Tree.Shared_Tree.Finalize;
+      Tree.Last_Shared_Node := Invalid_Node_Index;
+      Tree.Branched_Nodes.Clear;
+   end Clear;
+
+   function Count_Terminals
+     (Tree   : in     Syntax_Trees.Tree;
+      Node   : in     Valid_Node_Index)
+     return Natural
+   is
+      function Compute (N : in Syntax_Trees.Node) return Natural
+      is begin
+         case N.Label is
+         when Shared_Terminal | Virtual_Terminal =>
+            return 1;
+
+         when Nonterm =>
+            return Result : Natural := 0 do
+               for I of N.Children loop
+                  Result := Result + Count_Terminals (Tree, I);
+               end loop;
+            end return;
+         end case;
+      end Compute;
+   begin
+      return Compute
+        ((if Node <= Tree.Last_Shared_Node
+          then Tree.Shared_Tree.Nodes (Node)
+          else Tree.Branched_Nodes (Node)));
+   end Count_Terminals;
+
+   overriding procedure Finalize (Tree : in out Base_Tree)
+   is begin
+      Tree.Traversing := False;
+      if Tree.Augmented_Present then
+         for Node of Tree.Nodes loop
+            if Node.Label = Nonterm then
+               Free (Node.Augmented);
+            end if;
+         end loop;
+         Tree.Augmented_Present := False;
+      end if;
+      Tree.Nodes.Finalize;
+   end Finalize;
+
+   function Find_Ancestor
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index
+   is
+      N : Node_Index := Node;
+   begin
+      loop
+         N :=
+           (if N <= Tree.Last_Shared_Node
+            then Tree.Shared_Tree.Nodes (N).Parent
+            else Tree.Branched_Nodes (N).Parent);
+
+         exit when N = Invalid_Node_Index;
+         exit when ID =
+           (if N <= Tree.Last_Shared_Node
+            then Tree.Shared_Tree.Nodes (N).ID
+            else Tree.Branched_Nodes (N).ID);
+      end loop;
+      return N;
+   end Find_Ancestor;
+
+   function Find_Child
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index
+   is
+      function Compute (N : in Syntax_Trees.Node) return Node_Index
+      is begin
+         case N.Label is
+         when Shared_Terminal | Virtual_Terminal =>
+            return Invalid_Node_Index;
+         when Nonterm =>
+            for C of N.Children loop
+               if ID =
+                 (if C <= Tree.Last_Shared_Node
+                  then Tree.Shared_Tree.Nodes (C).ID
+                  else Tree.Branched_Nodes (C).ID)
+               then
+                  return C;
+               end if;
+            end loop;
+            return Invalid_Node_Index;
+         end case;
+      end Compute;
+   begin
+      return Compute
+        ((if Node <= Tree.Last_Shared_Node
+          then Tree.Shared_Tree.Nodes (Node)
+          else Tree.Branched_Nodes (Node)));
+   end Find_Child;
+
+   function Find_Descendant
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index
+   is
+      Found : Node_Index := Invalid_Node_Index;
+
+      function Process (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean
+      is
+         Node_ID : constant Token_ID :=
+           (if Node <= Tree.Last_Shared_Node
+            then Tree.Shared_Tree.Nodes (Node).ID
+            else Tree.Branched_Nodes (Node).ID);
+      begin
+         if Node_ID = ID then
+            Found := Node;
+            return False;
+         else
+            return True;
+         end if;
+      end Process;
+
+      Junk : constant Boolean := Process_Tree (Tree, Node, Process'Access);
+      pragma Unreferenced (Junk);
+   begin
+      return Found;
+   end Find_Descendant;
+
+   function Find_Sibling
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index
+   is
+      function Compute_2 (N : in Syntax_Trees.Node) return Node_Index
+      is begin
+         case N.Label is
+         when Shared_Terminal | Virtual_Terminal =>
+            return Invalid_Node_Index;
+
+         when Nonterm =>
+            for C of N.Children loop
+               if ID =
+                 (if C <= Tree.Last_Shared_Node
+                  then Tree.Shared_Tree.Nodes (C).ID
+                  else Tree.Branched_Nodes (C).ID)
+               then
+                  return C;
+               end if;
+            end loop;
+            return Invalid_Node_Index;
+         end case;
+      end Compute_2;
+
+      function Compute_1 (Parent : in Node_Index) return Node_Index
+      is begin
+         if Parent = Invalid_Node_Index then
+            return Invalid_Node_Index;
+
+         else
+            return Compute_2
+              ((if Parent <= Tree.Last_Shared_Node
+                then Tree.Shared_Tree.Nodes (Parent)
+                else Tree.Branched_Nodes (Parent)));
+         end if;
+      end Compute_1;
+   begin
+      return Compute_1
+        ((if Node <= Tree.Last_Shared_Node
+          then Tree.Shared_Tree.Nodes (Node).Parent
+          else Tree.Branched_Nodes (Node).Parent));
+   end Find_Sibling;
+
+   procedure Flush (Tree : in out Syntax_Trees.Tree)
+   is begin
+      --  This is the opposite of Move_Branch_Point
+      Tree.Shared_Tree.Nodes.Merge (Tree.Branched_Nodes);
+      Tree.Last_Shared_Node := Tree.Shared_Tree.Nodes.Last_Index;
+      Tree.Flush            := True;
+   end Flush;
+
+   procedure Get_Terminals
+     (Tree   : in     Syntax_Trees.Tree;
+      Node   : in     Valid_Node_Index;
+      Result : in out Valid_Node_Index_Array;
+      Last   : in out SAL.Base_Peek_Type)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      procedure Compute (N : in Syntax_Trees.Node)
+      is begin
+         case N.Label is
+         when Shared_Terminal | Virtual_Terminal =>
+            Last := Last + 1;
+            Result (Last) := Node;
+
+         when Nonterm =>
+            for I of N.Children loop
+               Get_Terminals (Tree, I, Result, Last);
+            end loop;
+         end case;
+      end Compute;
+   begin
+      Compute
+        ((if Node <= Tree.Last_Shared_Node
+          then Tree.Shared_Tree.Nodes (Node)
+          else Tree.Branched_Nodes (Node)));
+   end Get_Terminals;
+
+   procedure Get_Terminal_IDs
+     (Tree   : in     Syntax_Trees.Tree;
+      Node   : in     Valid_Node_Index;
+      Result : in out Token_ID_Array;
+      Last   : in out Natural)
+   is
+      procedure Compute (N : in Syntax_Trees.Node)
+      is begin
+         case N.Label is
+         when Shared_Terminal | Virtual_Terminal =>
+            Last := Last + 1;
+            Result (Last) := N.ID;
+
+         when Nonterm =>
+            for I of N.Children loop
+               Get_Terminal_IDs (Tree, I, Result, Last);
+            end loop;
+         end case;
+      end Compute;
+   begin
+      Compute
+        ((if Node <= Tree.Last_Shared_Node
+          then Tree.Shared_Tree.Nodes (Node)
+          else Tree.Branched_Nodes (Node)));
+   end Get_Terminal_IDs;
+
+   function Get_Terminals (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Valid_Node_Index_Array
+   is
+      Last : SAL.Base_Peek_Type := 0;
+   begin
+      Tree.Shared_Tree.Traversing := True;
+      return Result : Valid_Node_Index_Array (1 .. SAL.Base_Peek_Type 
(Count_Terminals (Tree, Node)))  do
+         Get_Terminals (Tree, Node, Result, Last);
+         Tree.Shared_Tree.Traversing := False;
+      end return;
+   end Get_Terminals;
+
+   function Get_Terminal_IDs (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Token_ID_Array
+   is
+      Last : Natural := 0;
+   begin
+      Tree.Shared_Tree.Traversing := True;
+      return Result : Token_ID_Array (1 .. Count_Terminals (Tree, Node))  do
+         Get_Terminal_IDs (Tree, Node, Result, Last);
+         Tree.Shared_Tree.Traversing := False;
+      end return;
+   end Get_Terminal_IDs;
+
+   function Has_Branched_Nodes (Tree : in Syntax_Trees.Tree) return Boolean
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      return Tree.Branched_Nodes.Length > 0;
+   end Has_Branched_Nodes;
+
+   function Has_Children (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean
+   is
+      use all type Ada.Containers.Count_Type;
+   begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Children.Length > 0;
+      else
+         return Tree.Branched_Nodes (Node).Children.Length > 0;
+      end if;
+   end Has_Children;
+
+   function Has_Parent (Tree : in Syntax_Trees.Tree; Child : in 
Valid_Node_Index) return Boolean
+   is begin
+      return
+        (if Child <= Tree.Last_Shared_Node
+         then Tree.Shared_Tree.Nodes (Child).Parent /= Invalid_Node_Index
+         else Tree.Branched_Nodes (Child).Parent /= Invalid_Node_Index);
+   end Has_Parent;
+
+   function Has_Parent (Tree : in Syntax_Trees.Tree; Children : in 
Valid_Node_Index_Array) return Boolean
+   is begin
+      return
+        (for some Child of Children =>
+           (if Child <= Tree.Last_Shared_Node
+            then Tree.Shared_Tree.Nodes (Child).Parent /= Invalid_Node_Index
+            else Tree.Branched_Nodes (Child).Parent /= Invalid_Node_Index));
+   end Has_Parent;
+
+   function ID
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return Token_ID
+   is begin
+      return
+        (if Node <= Tree.Last_Shared_Node
+         then Tree.Shared_Tree.Nodes (Node).ID
+         else Tree.Branched_Nodes (Node).ID);
+   end ID;
+
+   function Image
+     (Tree       : in Syntax_Trees.Tree;
+      Children   : in Valid_Node_Index_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is
+      use Ada.Strings.Unbounded;
+      Result     : Unbounded_String := +"(";
+      Need_Comma : Boolean := False;
+   begin
+      for I of Children loop
+         Result := Result & (if Need_Comma then ", " else "") &
+           Tree.Image (I, Descriptor, Include_Children => False);
+         Need_Comma := True;
+      end loop;
+      Result := Result & ")";
+      return -Result;
+   end Image;
+
+   function Image
+     (Tree             : in Syntax_Trees.Tree;
+      N                : in Syntax_Trees.Node;
+      Descriptor       : in WisiToken.Descriptor;
+      Include_Children : in Boolean)
+     return String
+   is
+      use Ada.Strings.Unbounded;
+      Result : Unbounded_String;
+   begin
+      if Include_Children and N.Label = Nonterm then
+         Result := +Image (N.ID, Descriptor) & '_' & Trimmed_Image 
(N.RHS_Index) & ": ";
+      end if;
+
+      if N.Label = Shared_Terminal then
+         Result := Result & (+Token_Index'Image (N.Terminal)) & ":";
+      end if;
+
+      Result := Result & "(" & Image (N.ID, Descriptor) &
+        (if N.Byte_Region = Null_Buffer_Region then "" else ", " & Image 
(N.Byte_Region)) & ")";
+
+      if Include_Children and N.Label = Nonterm then
+         Result := Result & " <= " & Image (Tree, N.Children, Descriptor);
+      end if;
+
+      return -Result;
+   end Image;
+
+   function Image
+     (Tree             : in Syntax_Trees.Tree;
+      Node             : in Valid_Node_Index;
+      Descriptor       : in WisiToken.Descriptor;
+      Include_Children : in Boolean := False)
+     return String
+   is begin
+      return Tree.Image
+        ((if Node <= Tree.Last_Shared_Node
+          then Tree.Shared_Tree.Nodes (Node)
+          else Tree.Branched_Nodes (Node)),
+         Descriptor, Include_Children);
+   end Image;
+
+   function Image
+     (Tree       : in Syntax_Trees.Tree;
+      Nodes      : in Valid_Node_Index_Array;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is
+      use Ada.Strings.Unbounded;
+      Result     : Unbounded_String := +"(";
+      Need_Comma : Boolean := False;
+   begin
+      for I in Nodes'Range loop
+         Result := Result & (if Need_Comma then ", " else "") &
+           Tree.Image (Nodes (I), Descriptor, Include_Children => False);
+         Need_Comma := True;
+      end loop;
+      Result := Result & ")";
+      return -Result;
+   end Image;
+
+   procedure Initialize
+     (Branched_Tree : in out Syntax_Trees.Tree;
+      Shared_Tree   : in     Base_Tree_Access;
+      Flush         : in     Boolean)
+   is begin
+      Branched_Tree :=
+        (Shared_Tree      => Shared_Tree,
+         Last_Shared_Node => Shared_Tree.Nodes.Last_Index,
+         Branched_Nodes   => <>,
+         Flush            => Flush,
+         Root             => <>);
+   end Initialize;
+
+   function Is_Empty (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Boolean
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Byte_Region = Null_Buffer_Region;
+      else
+         return Tree.Branched_Nodes (Node).Byte_Region = Null_Buffer_Region;
+      end if;
+   end Is_Empty;
+
+   function Is_Nonterm (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Label = Nonterm;
+      else
+         return Tree.Branched_Nodes (Node).Label = Nonterm;
+      end if;
+   end Is_Nonterm;
+
+   function Is_Terminal (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Label = Shared_Terminal;
+      else
+         return Tree.Branched_Nodes (Node).Label = Shared_Terminal;
+      end if;
+   end Is_Terminal;
+
+   function Is_Virtual (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean
+   is
+      function Compute (N : in Syntax_Trees.Node) return Boolean
+      is begin
+         return N.Label = Virtual_Terminal or (N.Label = Nonterm and then 
N.Virtual);
+      end Compute;
+   begin
+      if Node <= Tree.Last_Shared_Node then
+         return Compute (Tree.Shared_Tree.Nodes (Node));
+      else
+         return Compute (Tree.Branched_Nodes (Node));
+      end if;
+   end Is_Virtual;
+
+   function Label (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Node_Label
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Label;
+      else
+         return Tree.Branched_Nodes (Node).Label;
+      end if;
+   end Label;
+
+   function Min (Item : in Valid_Node_Index_Array) return Valid_Node_Index
+   is
+      Result : Node_Index := Item (Item'First);
+   begin
+      for I in Item'Range loop
+         if Item (I) < Result then
+            Result := Item (I);
+         end if;
+      end loop;
+      return Result;
+   end Min;
+
+   function Min_Descendant (Nodes : in Node_Arrays.Vector; Node : in 
Valid_Node_Index) return Valid_Node_Index
+   is
+      N : Syntax_Trees.Node renames Nodes (Node);
+   begin
+      case N.Label is
+      when Shared_Terminal | Virtual_Terminal =>
+         return Node;
+
+      when Nonterm =>
+         declare
+            Min : Node_Index := Node;
+         begin
+            for C of N.Children loop
+               Min := Node_Index'Min (Min, Min_Descendant (Nodes, C));
+            end loop;
+            return Min;
+         end;
+      end case;
+   end Min_Descendant;
+
+   function Min_Terminal_Index (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Base_Token_Index
+   is
+      function Compute (N : in Syntax_Trees.Node) return Base_Token_Index
+      is begin
+         return
+           (case N.Label is
+            when Shared_Terminal  => N.Terminal,
+            when Virtual_Terminal => Invalid_Token_Index,
+            when Nonterm          => N.Min_Terminal_Index);
+      end Compute;
+
+   begin
+      if Node <= Tree.Last_Shared_Node then
+         return Compute (Tree.Shared_Tree.Nodes (Node));
+      else
+         return Compute (Tree.Branched_Nodes (Node));
+      end if;
+   end Min_Terminal_Index;
+
+   function Max_Terminal_Index (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Base_Token_Index
+   is
+      function Compute (N : in Syntax_Trees.Node) return Base_Token_Index
+      is begin
+         return
+           (case N.Label is
+            when Shared_Terminal  => N.Terminal,
+            when Virtual_Terminal => Invalid_Token_Index,
+            when Nonterm          => N.Max_Terminal_Index);
+      end Compute;
+
+   begin
+      if Node <= Tree.Last_Shared_Node then
+         return Compute (Tree.Shared_Tree.Nodes (Node));
+      else
+         return Compute (Tree.Branched_Nodes (Node));
+      end if;
+   end Max_Terminal_Index;
+
+   procedure Move_Branch_Point (Tree : in out Syntax_Trees.Tree; Required_Node 
: in Valid_Node_Index)
+   is begin
+      --  Note that this preserves all stored indices in Branched_Nodes.
+      Tree.Branched_Nodes.Prepend (Tree.Shared_Tree.Nodes, Required_Node, 
Tree.Last_Shared_Node);
+      Tree.Last_Shared_Node := Required_Node - 1;
+   end Move_Branch_Point;
+
+   function Parent (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Node_Index
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Parent;
+      else
+         return Tree.Branched_Nodes (Node).Parent;
+      end if;
+   end Parent;
+
+   procedure Print_Tree (Tree : in Syntax_Trees.Tree; Descriptor : in 
WisiToken.Descriptor)
+   is
+      use Ada.Text_IO;
+      procedure Print_Node (Node : in Valid_Node_Index; Level : in Integer)
+      is
+         N : Syntax_Trees.Node renames Tree.Shared_Tree.Nodes (Node);
+      begin
+         for I in 1 .. Level loop
+            Put ("| ");
+         end loop;
+         Put_Line (Image (Tree, N, Descriptor, Include_Children => False));
+
+         if N.Label = Nonterm then
+            for Child of N.Children loop
+               Print_Node (Child, Level + 1);
+            end loop;
+         end if;
+      end Print_Node;
+
+   begin
+      Print_Node (Tree.Root, 0);
+   end Print_Tree;
+
+   function Process_Tree
+     (Tree         : in Syntax_Trees.Tree;
+      Node         : in Valid_Node_Index;
+      Process_Node : access function
+        (Tree : in Syntax_Trees.Tree;
+         Node : in Valid_Node_Index)
+        return Boolean)
+     return Boolean
+   is
+      function Compute (N : in Syntax_Trees.Node) return Boolean
+      is begin
+         if N.Label = Nonterm then
+            for Child of N.Children loop
+               if not Process_Tree (Tree, Child, Process_Node) then
+                  return False;
+               end if;
+            end loop;
+         end if;
+
+         return Process_Node (Tree, Node);
+      end Compute;
+   begin
+      if Node <= Tree.Last_Shared_Node then
+         return Compute (Tree.Shared_Tree.Nodes (Node));
+      else
+         return Compute (Tree.Branched_Nodes (Node));
+      end if;
+   end Process_Tree;
+
+   procedure Process_Tree
+     (Tree         : in out Syntax_Trees.Tree;
+      Node         : in     Valid_Node_Index;
+      Process_Node : access procedure
+        (Tree : in out Syntax_Trees.Tree;
+         Node : in     Valid_Node_Index))
+   is
+      procedure Compute (N : in Syntax_Trees.Node)
+      is begin
+         if N.Label = Nonterm then
+            for Child of N.Children loop
+               Process_Tree (Tree, Child, Process_Node);
+            end loop;
+         end if;
+
+         Process_Node (Tree, Node);
+      end Compute;
+   begin
+      if Node <= Tree.Last_Shared_Node then
+         Compute (Tree.Shared_Tree.Nodes (Node));
+      else
+         Compute (Tree.Branched_Nodes (Node));
+      end if;
+   end Process_Tree;
+
+   procedure Process_Tree
+     (Tree         : in out Syntax_Trees.Tree;
+      Process_Node : access procedure
+        (Tree : in out Syntax_Trees.Tree;
+         Node : in     Valid_Node_Index))
+   is begin
+      if Tree.Root = Invalid_Node_Index then
+         raise SAL.Programmer_Error with "Tree.Root not set";
+      end if;
+      Tree.Shared_Tree.Traversing := True;
+      if Tree.Flush then
+         Process_Tree (Tree, Tree.Root, Process_Node);
+      else
+         Process_Tree (Tree, Tree.Root, Process_Node);
+      end if;
+      Tree.Shared_Tree.Traversing := False;
+   exception
+   when others =>
+      Tree.Shared_Tree.Traversing := False;
+      raise;
+   end Process_Tree;
+
+   procedure Set_Root (Tree : in out Syntax_Trees.Tree; Root : in 
Valid_Node_Index)
+   is begin
+      Tree.Root := Root;
+   end Set_Root;
+
+   function Root (Tree : in Syntax_Trees.Tree) return Node_Index
+   is begin
+      if Tree.Root /= Invalid_Node_Index then
+         return Tree.Root;
+      else
+         if Tree.Flush then
+            return Tree.Shared_Tree.Nodes.Last_Index;
+         else
+            return Tree.Branched_Nodes.Last_Index;
+         end if;
+      end if;
+   end Root;
+
+   function Same_Token
+     (Tree_1  : in Syntax_Trees.Tree'Class;
+      Index_1 : in Valid_Node_Index;
+      Tree_2  : in Syntax_Trees.Tree'Class;
+      Index_2 : in Valid_Node_Index)
+     return Boolean
+   is
+      function Compute (N_1, N_2 : in Syntax_Trees.Node) return Boolean
+      is begin
+         return N_1.Label = N_2.Label and
+           N_1.ID = N_2.ID and
+           N_1.Byte_Region = N_2.Byte_Region;
+      end Compute;
+   begin
+      return Compute
+        ((if Index_1 <= Tree_1.Last_Shared_Node
+          then Tree_1.Shared_Tree.Nodes (Index_1)
+          else Tree_1.Branched_Nodes (Index_1)),
+         (if Index_2 <= Tree_2.Last_Shared_Node
+          then Tree_2.Shared_Tree.Nodes (Index_2)
+          else Tree_2.Branched_Nodes (Index_2)));
+   end Same_Token;
+
+   procedure Set_Augmented
+     (Tree  : in out Syntax_Trees.Tree;
+      Node  : in     Valid_Node_Index;
+      Value : in     Base_Token_Class_Access)
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         Tree.Shared_Tree.Nodes (Node).Augmented := Value;
+      else
+         Tree.Branched_Nodes (Node).Augmented := Value;
+      end if;
+   end Set_Augmented;
+
+   procedure Set_Children
+     (Nodes    : in out Node_Arrays.Vector;
+      Parent   : in     Valid_Node_Index;
+      Children : in     Valid_Node_Index_Array)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      N : Nonterm_Node renames Nodes (Parent);
+      J : Positive_Index_Type := Positive_Index_Type'First;
+
+      Min_Terminal_Index_Set : Boolean := False;
+   begin
+      N.Children.Set_Length (Children'Length);
+      for I in Children'Range loop
+         N.Children (J) := Children (I);
+         declare
+            K : Syntax_Trees.Node renames Nodes (Children (I));
+         begin
+            K.Parent := Parent;
+
+            N.Virtual := N.Virtual or
+              (case K.Label is
+               when Shared_Terminal  => False,
+               when Virtual_Terminal => True,
+               when Nonterm          => K.Virtual);
+
+            if N.Byte_Region.First > K.Byte_Region.First then
+               N.Byte_Region.First := K.Byte_Region.First;
+            end if;
+
+            if N.Byte_Region.Last < K.Byte_Region.Last then
+               N.Byte_Region.Last := K.Byte_Region.Last;
+            end if;
+
+            if not Min_Terminal_Index_Set then
+               case K.Label is
+               when Shared_Terminal =>
+                  Min_Terminal_Index_Set := True;
+                  N.Min_Terminal_Index   := K.Terminal;
+
+               when Virtual_Terminal =>
+                  null;
+
+               when Nonterm =>
+                  if K.Min_Terminal_Index /= Invalid_Token_Index then
+                     --  not an empty nonterm
+                     Min_Terminal_Index_Set := True;
+                     N.Min_Terminal_Index   := K.Min_Terminal_Index;
+                  end if;
+               end case;
+            end if;
+
+            case K.Label is
+            when Shared_Terminal =>
+               if N.Max_Terminal_Index < K.Terminal then
+                  N.Max_Terminal_Index := K.Terminal;
+               end if;
+
+            when Virtual_Terminal =>
+               null;
+
+            when Nonterm =>
+               if K.Max_Terminal_Index /= Invalid_Token_Index and then
+                 --  not an empty nonterm
+                 N.Max_Terminal_Index < K.Max_Terminal_Index
+               then
+                  N.Max_Terminal_Index := K.Max_Terminal_Index;
+               end if;
+            end case;
+         end;
+
+         J := J + 1;
+      end loop;
+   end Set_Children;
+
+   procedure Set_State
+     (Tree  : in out Syntax_Trees.Tree;
+      Node  : in     Valid_Node_Index;
+      State : in     State_Index)
+   is begin
+      if Tree.Flush then
+         Tree.Shared_Tree.Nodes (Node).State := State;
+      else
+         if Node <= Tree.Last_Shared_Node then
+            Tree.Shared_Tree.Nodes (Node).State := State;
+         else
+            Tree.Branched_Nodes (Node).State := State;
+         end if;
+      end if;
+   end Set_State;
+
+   procedure Set_Flush_False (Tree : in out Syntax_Trees.Tree)
+   is begin
+      Tree.Flush := False;
+      Tree.Branched_Nodes.Set_First (Tree.Last_Shared_Node + 1);
+   end Set_Flush_False;
+
+   function Flushed (Tree : in Syntax_Trees.Tree) return Boolean
+   is begin
+      return Tree.Flush;
+   end Flushed;
+
+   procedure Set_Name_Region
+     (Tree   : in out Syntax_Trees.Tree;
+      Node   : in     Valid_Node_Index;
+      Region : in     Buffer_Region)
+   is begin
+      if Tree.Flush then
+         Tree.Shared_Tree.Nodes (Node).Name := Region;
+
+      else
+         if Node <= Tree.Last_Shared_Node then
+            Move_Branch_Point (Tree, Node);
+         end if;
+
+         Tree.Branched_Nodes (Node).Name := Region;
+      end if;
+   end Set_Name_Region;
+
+   function Terminal (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Base_Token_Index
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).Terminal;
+      else
+         return Tree.Branched_Nodes (Node).Terminal;
+      end if;
+   end Terminal;
+
+   function Traversing (Tree : in Syntax_Trees.Tree) return Boolean
+   is begin
+      return Tree.Shared_Tree.Traversing;
+   end Traversing;
+
+   function Recover_Token
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return WisiToken.Recover_Token
+   is
+      function Compute (N : Syntax_Trees.Node) return WisiToken.Recover_Token
+      is begin
+         case N.Label is
+         when Shared_Terminal =>
+            return
+              (ID                 => N.ID,
+               Byte_Region        => N.Byte_Region,
+               Min_Terminal_Index => N.Terminal,
+               Name               => Null_Buffer_Region,
+               Virtual            => False);
+
+         when Virtual_Terminal =>
+            return
+              (ID                 => N.ID,
+               Byte_Region        => Null_Buffer_Region,
+               Min_Terminal_Index => Invalid_Token_Index,
+               Name               => Null_Buffer_Region,
+               Virtual            => True);
+
+         when Nonterm =>
+            return
+              (ID                 => N.ID,
+               Byte_Region        => N.Byte_Region,
+               Min_Terminal_Index => N.Min_Terminal_Index,
+               Name               => N.Name,
+               Virtual            => N.Virtual);
+         end case;
+      end Compute;
+   begin
+      return Compute
+        ((if Node <= Tree.Last_Shared_Node
+          then Tree.Shared_Tree.Nodes (Node)
+          else Tree.Branched_Nodes (Node)));
+   end Recover_Token;
+
+   function Recover_Token_Array
+     (Tree  : in Syntax_Trees.Tree;
+      Nodes : in Valid_Node_Index_Array)
+     return WisiToken.Recover_Token_Array
+   is begin
+      return Result : WisiToken.Recover_Token_Array (Nodes'First .. 
Nodes'Last) do
+         for I in Result'Range loop
+            Result (I) := Tree.Recover_Token (Nodes (I));
+         end loop;
+      end return;
+   end Recover_Token_Array;
+
+   function State (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Unknown_State_Index
+   is begin
+      if Node <= Tree.Last_Shared_Node then
+         return Tree.Shared_Tree.Nodes (Node).State;
+      else
+         return Tree.Branched_Nodes (Node).State;
+      end if;
+   end State;
+
+end WisiToken.Syntax_Trees;
diff --git a/wisitoken-syntax_trees.ads b/wisitoken-syntax_trees.ads
new file mode 100644
index 0000000..dc04946
--- /dev/null
+++ b/wisitoken-syntax_trees.ads
@@ -0,0 +1,411 @@
+--  Abstract :
+--
+--  Syntax tree type and operations.
+--
+--  Rationale :
+--
+--  We provide Base_Tree and Tree in one package, because only Tree
+--  needs an API; the only way Base_Tree is accessed is via Tree.
+--
+--  Copyright (C) 2018 Stephen Leake All Rights Reserved.
+
+--  There is one syntax tree for each parser. There is one shared
+--  Terminals array, matching the actual input text.
+--
+--  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);
+
+with Ada.Finalization;
+with SAL.Gen_Unbounded_Definite_Vectors;
+with WisiToken.Lexer;
+package WisiToken.Syntax_Trees is
+
+   type Base_Tree is new Ada.Finalization.Controlled with private;
+
+   type Base_Tree_Access is access all Base_Tree;
+
+   overriding procedure Finalize (Tree : in out Base_Tree);
+   --  Free any allocated storage.
+
+   overriding procedure Adjust (Tree : in out Base_Tree);
+   --  Copy any allocated storage.
+
+   type Tree is tagged private;
+
+   procedure Initialize
+     (Branched_Tree : in out Tree;
+      Shared_Tree   : in     Base_Tree_Access;
+      Flush         : in     Boolean);
+   --  Set Branched_Tree to refer to Shared_Tree.
+
+   type Node_Index is range 0 .. Integer'Last;
+   subtype Valid_Node_Index is Node_Index range 1 .. Node_Index'Last;
+
+   Invalid_Node_Index : constant Node_Index := Node_Index'First;
+
+   type Valid_Node_Index_Array is array (Positive_Index_Type range <>) of 
Valid_Node_Index;
+   --  Index matches Base_Token_Array, Augmented_Token_Array
+
+   package Valid_Node_Index_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Positive_Index_Type, Valid_Node_Index);
+   --  Index matches Valid_Node_Index_Array.
+
+   type Node_Label is (Shared_Terminal, Virtual_Terminal, Nonterm);
+
+   type User_Data_Type is tagged limited null record;
+   --  Many test languages don't need this, so we default the procedures
+   --  to null.
+
+   type User_Data_Access is access all User_Data_Type'Class;
+
+   procedure Set_Lexer_Terminals
+     (User_Data : in out User_Data_Type;
+      Lexer     : in     WisiToken.Lexer.Handle;
+      Terminals : in     Base_Token_Array_Access)
+   is null;
+
+   procedure Reset (User_Data : in out User_Data_Type) is null;
+   --  Reset to start a new parse.
+
+   procedure Lexer_To_Augmented
+     (User_Data  : in out          User_Data_Type;
+      Token      : in              Base_Token;
+      Lexer      : not null access WisiToken.Lexer.Instance'Class)
+     is null;
+   --  Read auxiliary data from Lexer, create an Augmented_Token, store
+   --  it in User_Data. Called before parsing, once for each token in the
+   --  input stream.
+
+   procedure Delete_Token
+     (Data        : in out User_Data_Type;
+      Token_Index : in     WisiToken.Token_Index)
+   is null;
+   --  Token at Token_Index was deleted in error recovery; update
+   --  remaining tokens as needed. Called from Execute_Actions for each
+   --  deleted token, before processing the syntax tree.
+
+   procedure Reduce
+     (User_Data : in out User_Data_Type;
+      Tree      : in out Syntax_Trees.Tree'Class;
+      Nonterm   : in     Valid_Node_Index;
+      Tokens    : in     Valid_Node_Index_Array)
+   is null;
+   --  Reduce Tokens to Nonterm. Nonterm.Byte_Region is computed by
+   --  caller.
+
+   type Semantic_Action is access procedure
+     (User_Data : in out User_Data_Type'Class;
+      Tree      : in out Syntax_Trees.Tree;
+      Nonterm   : in     Valid_Node_Index;
+      Tokens    : in     Valid_Node_Index_Array);
+   --  Routines of this type are called by
+   --  WisiToken.LR.Parser.Execute_Actions when it processes a Nonterm
+   --  node in the syntax tree. Tokens are the children of Nonterm.
+
+   Null_Action : constant Semantic_Action := null;
+
+   procedure Clear (Tree : in out Syntax_Trees.Base_Tree);
+   procedure Clear (Tree : in out Syntax_Trees.Tree);
+   --  Delete all Elements and free associated memory; keep results of
+   --  Initialize.
+
+   procedure Flush (Tree : in out Syntax_Trees.Tree);
+   --  Move all nodes in branched part to shared tree, set Flush mode
+   --  True.
+
+   procedure Set_Flush_False (Tree : in out Syntax_Trees.Tree);
+   --  Set Flush mode False; use Flush to set True.
+
+   function Flushed (Tree : in Syntax_Trees.Tree) return Boolean;
+
+   function Add_Nonterm
+     (Tree            : in out Syntax_Trees.Tree;
+      Production      : in     Production_ID;
+      Children        : in     Valid_Node_Index_Array;
+      Action          : in     Semantic_Action;
+      Default_Virtual : in     Boolean)
+     return Valid_Node_Index
+   with
+     Pre  => not Tree.Traversing,
+     Post => Tree.Is_Empty (Add_Nonterm'Result) or
+             Tree.Min_Terminal_Index (Add_Nonterm'Result) /= 
Invalid_Token_Index;
+   --  Add a new Nonterm node, which can be empty. Result points to the
+   --  added node. If Children'Length = 0, set Nonterm.Virtual :=
+   --  Default_Virtual.
+
+   function Add_Terminal
+     (Tree      : in out Syntax_Trees.Tree;
+      Terminal  : in     Token_Index;
+      Terminals : in     Base_Token_Arrays.Vector)
+     return Valid_Node_Index
+   with Pre => not Tree.Traversing;
+   --  Add a new Terminal node. Terminal must be an index into Terminals.
+   --  Result points to the added node.
+
+   function Add_Terminal
+     (Tree     : in out Syntax_Trees.Tree;
+      Terminal : in     Token_ID)
+     return Valid_Node_Index
+   with Pre => not Tree.Traversing;
+   --  Add a new virtual terminal node with no parent. Result points to
+   --  the added node.
+
+   procedure Set_State
+     (Tree  : in out Syntax_Trees.Tree;
+      Node  : in     Valid_Node_Index;
+      State : in     State_Index);
+
+   function State (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Unknown_State_Index;
+
+   function Label (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Node_Label;
+
+   function Children (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Valid_Node_Index_Array
+   with Pre => Tree.Is_Nonterm (Node);
+
+   function Has_Branched_Nodes (Tree : in Syntax_Trees.Tree) return Boolean;
+   function Has_Children (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean;
+   function Has_Parent (Tree : in Syntax_Trees.Tree; Child : in 
Valid_Node_Index) return Boolean;
+   function Has_Parent (Tree : in Syntax_Trees.Tree; Children : in 
Valid_Node_Index_Array) return Boolean;
+   function Is_Empty (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Boolean;
+   function Is_Nonterm (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean;
+   function Is_Terminal (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean;
+   function Is_Virtual (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Boolean;
+   function Traversing (Tree : in Syntax_Trees.Tree) return Boolean;
+
+   function Parent (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Node_Index;
+
+   procedure Set_Name_Region
+     (Tree   : in out Syntax_Trees.Tree;
+      Node   : in     Valid_Node_Index;
+      Region : in     Buffer_Region)
+   with Pre => Tree.Is_Nonterm (Node);
+
+   function ID
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return WisiToken.Token_ID;
+
+   function Byte_Region
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return WisiToken.Buffer_Region;
+
+   function Same_Token
+     (Tree_1  : in Syntax_Trees.Tree'Class;
+      Index_1 : in Valid_Node_Index;
+      Tree_2  : in Syntax_Trees.Tree'Class;
+      Index_2 : in Valid_Node_Index)
+     return Boolean;
+   --  True if the two tokens have the same ID and Byte_Region.
+
+   function Recover_Token
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return WisiToken.Recover_Token;
+
+   function Recover_Token_Array
+     (Tree  : in Syntax_Trees.Tree;
+      Nodes : in Valid_Node_Index_Array)
+     return WisiToken.Recover_Token_Array;
+   --  For non-virtual terminals, copied from Tree.Terminals. For others,
+   --  constructed from Tree data.
+
+   procedure Set_Augmented
+     (Tree  : in out Syntax_Trees.Tree;
+      Node  : in     Valid_Node_Index;
+      Value : in     Base_Token_Class_Access)
+   with Pre => Tree.Is_Nonterm (Node);
+   --  Value will be deallocated when Tree is finalized.
+
+   function Augmented
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return Base_Token_Class_Access
+   with Pre => Tree.Is_Nonterm (Node);
+   --  Returns result of Set_Augmented.
+
+   function Action
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index)
+     return Semantic_Action
+   with Pre => Tree.Is_Nonterm (Node);
+
+   function Find_Ancestor
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index;
+   --  Return the ancestor of Node that contains ID, or Invalid_Node_Index if
+   --  none match.
+
+   function Find_Sibling
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index
+   with Pre => Tree.Has_Parent (Node);
+   --  Return the sibling of Node that contains ID, or Invalid_Node_Index if
+   --  none match.
+
+   function Find_Child
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index
+   with Pre => Tree.Is_Nonterm (Node);
+   --  Return the child of Node that contains ID, or Invalid_Node_Index if
+   --  none match.
+
+   function Find_Descendant
+     (Tree : in Syntax_Trees.Tree;
+      Node : in Valid_Node_Index;
+      ID   : in Token_ID)
+     return Node_Index;
+   --  Return the child of Node that contains ID (may be Node), or
+   --  Invalid_Node_Index if none match.
+
+   procedure Set_Root (Tree : in out Syntax_Trees.Tree; Root : in 
Valid_Node_Index);
+
+   function Root (Tree : in Syntax_Trees.Tree) return Node_Index;
+   --  Return value set by Set_Root; defaults to the last node added.
+   --  returns Invalid_Node_Index if Tree is empty.
+
+   procedure Process_Tree
+     (Tree         : in out Syntax_Trees.Tree;
+      Process_Node : access procedure
+        (Tree : in out Syntax_Trees.Tree;
+         Node : in     Valid_Node_Index));
+   --  Traverse Tree in depth-first order, calling Process_Node on each
+   --  node, starting at Tree.Root.
+
+   function Terminal (Tree : in Syntax_Trees.Tree; Node : in Valid_Node_Index) 
return Base_Token_Index
+   with Pre => Tree.Is_Terminal (Node);
+
+   function Min_Terminal_Index (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Base_Token_Index;
+   function Max_Terminal_Index (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Base_Token_Index;
+   --  Returns lowest/highest index of shared terminal in subtree under
+   --  Node. If result is Invalid_Token_Index, all terminals are virtual,
+   --  or a nonterm is empty.
+
+   function Get_Terminals (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Valid_Node_Index_Array;
+
+   function Get_Terminal_IDs (Tree : in Syntax_Trees.Tree; Node : in 
Valid_Node_Index) return Token_ID_Array;
+
+   function Image
+     (Tree             : in Syntax_Trees.Tree;
+      Node             : in Valid_Node_Index;
+      Descriptor       : in WisiToken.Descriptor;
+      Include_Children : in Boolean := False)
+     return String;
+   function Image
+     (Tree       : in Syntax_Trees.Tree;
+      Nodes      : in Valid_Node_Index_Array;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+   --  For debug and error messages.
+
+   procedure Print_Tree (Tree : in Syntax_Trees.Tree; Descriptor : in 
WisiToken.Descriptor)
+   with Pre => Tree.Flushed;
+   --  To Text_IO.Current_Output, for debugging.
+
+private
+
+   type Node (Label : Node_Label := Virtual_Terminal) is
+   --  Label has a default to allow use with Ada.Containers.Vectors; all
+   --  entries are the same size.
+   record
+      ID : WisiToken.Token_ID := Invalid_Token_ID;
+
+      Byte_Region : Buffer_Region := Null_Buffer_Region;
+      --  Computed by Set_Children, used in Semantic_Check actions and debug
+      --  messages.
+
+      Parent : Node_Index := Invalid_Node_Index;
+
+      State : Unknown_State_Index := Unknown_State;
+      --  Parse state that was on stack with this token, to allow undoing a
+      --  reduce.
+
+      case Label is
+      when Shared_Terminal =>
+         Terminal : Token_Index;
+
+      when Virtual_Terminal =>
+         null;
+
+      when Nonterm =>
+         Virtual : Boolean := False;
+         --  True if any child node is Virtual_Terminal or Nonterm with Virtual
+         --  set. Used by Semantic_Check actions.
+
+         RHS_Index : Natural;
+         --  With ID, index into Productions.
+         --  Used for debug output, keep for future use.
+
+         Action : Semantic_Action := null;
+
+         Name : Buffer_Region := Null_Buffer_Region;
+         --  Name is set and checked by Semantic_Check actions.
+
+         Children : Valid_Node_Index_Arrays.Vector;
+
+         Min_Terminal_Index : Base_Token_Index := Invalid_Token_Index;
+         --  Cached for push_back of nonterminals during recovery
+
+         Max_Terminal_Index : Base_Token_Index := Invalid_Token_Index;
+         --  Cached for building a WisiToken tree from a libadalang tree.
+
+         Augmented : Base_Token_Class_Access := null;
+      end case;
+   end record;
+
+   subtype Nonterm_Node is Node (Nonterm);
+
+   package Node_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Valid_Node_Index, Node);
+
+   type Base_Tree is new Ada.Finalization.Controlled with record
+      Nodes : Node_Arrays.Vector;
+      --  During normal parsing, tokens are added to Nodes by "parallel"
+      --  LALR parsers, but they are all run from one Ada task, so there's
+      --  no need for Nodes to be Protected. Packrat parsing also has a
+      --  single Ada task.
+      --
+      --  During McKenzie_Recover, the syntax tree is not modified.
+
+      Augmented_Present : Boolean := False;
+      --  True if Set_Augmented has been called on any node.
+      --  Declared in Base_Tree because used by Base_Tree.Adjust.
+
+      Traversing : Boolean := False;
+      --  True while traversing tree in Process_Tree.
+      --  Declared in Base_Tree so it is cleared by Finalize.
+
+   end record;
+
+   type Tree is tagged record
+      Shared_Tree : Base_Tree_Access;
+      --  If we need to set anything (ie parent) in Shared_Tree, we move the
+      --  branch point instead, unless Flush = True.
+
+      Last_Shared_Node : Node_Index := Invalid_Node_Index;
+      Branched_Nodes   : Node_Arrays.Vector;
+      Flush            : Boolean    := False;
+      --  We maintain Last_Shared_Node when Flush is True, so subprograms
+      --  that have no reason to check Flush can rely on Last_Shared_Node.
+
+      Root : Node_Index := Invalid_Node_Index;
+   end record with
+     Type_Invariant => (if Tree.Flush then not Tree.Has_Branched_Nodes);
+
+end WisiToken.Syntax_Trees;
diff --git a/wisitoken-text_io_trace.adb b/wisitoken-text_io_trace.adb
new file mode 100644
index 0000000..1329cff
--- /dev/null
+++ b/wisitoken-text_io_trace.adb
@@ -0,0 +1,70 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  Copyright (C) 2017 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);
+
+package body WisiToken.Text_IO_Trace is
+
+   overriding
+   procedure Put (Trace : in out Text_IO_Trace.Trace; Item : in String)
+   is
+      use Ada.Text_IO;
+   begin
+      if Trace.File /= null and then Is_Open (Trace.File.all) then
+         Ada.Text_IO.Put (Trace.File.all, Item);
+      else
+         Ada.Text_IO.Put (Item);
+      end if;
+   end Put;
+
+   overriding
+   procedure Put_Line (Trace : in out Text_IO_Trace.Trace; Item : in String)
+   is
+      use Ada.Text_IO;
+   begin
+      if Trace.File /= null and then Is_Open (Trace.File.all) then
+         Ada.Text_IO.Put_Line (Trace.File.all, Item);
+         Ada.Text_IO.Flush (Trace.File.all);
+      else
+         Ada.Text_IO.Put_Line (Item);
+         Ada.Text_IO.Flush;
+      end if;
+   end Put_Line;
+
+   overriding
+   procedure New_Line (Trace : in out Text_IO_Trace.Trace)
+   is
+      use Ada.Text_IO;
+   begin
+      if Trace.File /= null and then Is_Open (Trace.File.all) then
+         Ada.Text_IO.New_Line (Trace.File.all);
+      else
+         Ada.Text_IO.New_Line;
+      end if;
+   end New_Line;
+
+   procedure Set_File (Trace : in out Text_IO_Trace.Trace; File : in 
Ada.Text_IO.File_Access)
+   is begin
+      Trace.File := File;
+   end Set_File;
+
+   procedure Clear_File (Trace : in out Text_IO_Trace.Trace)
+   is begin
+      Trace.File := null;
+   end Clear_File;
+
+end WisiToken.Text_IO_Trace;
diff --git a/wisitoken-text_io_trace.ads b/wisitoken-text_io_trace.ads
new file mode 100644
index 0000000..6deaf51
--- /dev/null
+++ b/wisitoken-text_io_trace.ads
@@ -0,0 +1,45 @@
+--  Abstract :
+--
+--  Trace output to Ada.Text_IO
+--
+--  Copyright (C) 2017 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);
+
+with Ada.Text_IO;
+package WisiToken.Text_IO_Trace is
+
+   type Trace is limited new WisiToken.Trace with private;
+   --  Defaults to Ada.Text_IO.Standard_Output
+
+   overriding
+   procedure Put (Trace : in out Text_IO_Trace.Trace; Item : in String);
+
+   overriding
+   procedure Put_Line (Trace : in out Text_IO_Trace.Trace; Item : in String);
+
+   overriding
+   procedure New_Line (Trace : in out Text_IO_Trace.Trace);
+
+   procedure Set_File (Trace : in out Text_IO_Trace.Trace; File : in 
Ada.Text_IO.File_Access);
+   --  Set file for trace output. Default is Text_IO.Current_Output.
+
+   procedure Clear_File (Trace : in out Text_IO_Trace.Trace);
+   --  Clear internal file; output to Text_IO.Current_Output.
+
+private
+   type Trace is limited new WisiToken.Trace with record
+      File : Ada.Text_IO.File_Access;
+   end record;
+end WisiToken.Text_IO_Trace;
diff --git a/wisitoken-wisi_ada.adb b/wisitoken-wisi_ada.adb
new file mode 100644
index 0000000..ba0285d
--- /dev/null
+++ b/wisitoken-wisi_ada.adb
@@ -0,0 +1,163 @@
+--  Abstract :
+--
+--  see spec
+--
+--  Copyright (C) 2013, 2014, 2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+package body WisiToken.Wisi_Ada is
+   use WisiToken.Productions;
+
+   function Only (Item : in Token_ID) return WisiToken.Token_ID_Arrays.Vector
+   is begin
+      return Result : WisiToken.Token_ID_Arrays.Vector do
+         Result.Append (Item);
+      end return;
+   end Only;
+
+   function "&" (Left : in Token_ID; Right : in Token_ID) return 
WisiToken.Token_ID_Arrays.Vector
+   is begin
+      return Result : WisiToken.Token_ID_Arrays.Vector do
+         Result.Append (Left);
+         Result.Append (Right);
+      end return;
+   end "&";
+
+   function "+" (Tokens : in Token_ID_Arrays.Vector; Action : in 
Syntax_Trees.Semantic_Action) return Right_Hand_Side
+   is begin
+      return (Tokens, Action, null);
+   end "+";
+
+   function "+" (Tokens : in Token_ID; Action : in 
Syntax_Trees.Semantic_Action) return Right_Hand_Side
+   is begin
+      return (Only (Tokens), Action, null);
+   end "+";
+
+   function "+" (Action : in Syntax_Trees.Semantic_Action) return 
Right_Hand_Side
+   is begin
+      return (Token_ID_Arrays.Empty_Vector, Action, null);
+   end "+";
+
+   function Only (Item : in WisiToken.Productions.Right_Hand_Side) return 
WisiToken.Productions.RHS_Arrays.Vector
+   is begin
+      return Result : WisiToken.Productions.RHS_Arrays.Vector do
+         Result.Append (Item);
+      end return;
+   end Only;
+
+   function "or"
+     (Left  : in WisiToken.Productions.Instance;
+      Right : in WisiToken.Productions.Right_Hand_Side)
+     return WisiToken.Productions.Instance
+   is begin
+      return Result : WisiToken.Productions.Instance := Left do
+         Result.RHSs.Append (Right);
+      end return;
+   end "or";
+
+   function "<=" (LHS : in Token_ID; RHSs : in 
WisiToken.Productions.RHS_Arrays.Vector) return Instance
+   is begin
+      return (LHS, RHSs);
+   end "<=";
+
+   function Only (Subject : in Instance) return Prod_Arrays.Vector
+   is begin
+      return Result : Prod_Arrays.Vector do
+         Result.Set_First (Subject.LHS);
+         Result.Set_Last (Subject.LHS);
+         Result (Subject.LHS) := Subject;
+      end return;
+   end Only;
+
+   function Merge (Left, Right : in Instance) return Instance
+   is
+      Index : Integer := Left.RHSs.Last_Index + 1;
+   begin
+      return Result : Instance := Left do
+         Result.RHSs.Set_Last (Left.RHSs.Last_Index + Integer 
(Right.RHSs.Length));
+         for RHS of Right.RHSs loop
+            Result.RHSs (Index) := RHS;
+            Index := Index + 1;
+         end loop;
+      end return;
+   end Merge;
+
+   function "and" (Left : in Instance; Right : in Instance) return 
Prod_Arrays.Vector
+   is begin
+      return Result : Prod_Arrays.Vector do
+         Result.Set_First (Token_ID'Min (Left.LHS, Right.LHS));
+         Result.Set_Last (Token_ID'Max (Left.LHS, Right.LHS));
+         if Left.LHS = Right.LHS then
+            Result (Left.LHS) := Merge (Left, Right);
+         else
+            Result (Left.LHS) := Left;
+            Result (Right.LHS) := Right;
+         end if;
+      end return;
+   end "and";
+
+   function "and" (Left : in Prod_Arrays.Vector; Right : in Instance) return 
Prod_Arrays.Vector
+   is begin
+      return Result : Prod_Arrays.Vector := Left do
+         if Right.LHS < Result.First_Index then
+            Result.Set_First (Right.LHS);
+         elsif Right.LHS > Result.Last_Index then
+            Result.Set_Last (Right.LHS);
+         end if;
+
+         if Result (Right.LHS).LHS = Invalid_Token_ID then
+            Result (Right.LHS) := Right;
+         else
+            Result (Right.LHS) := Merge (Result (Right.LHS), Right);
+         end if;
+      end return;
+   end "and";
+
+   function "and" (Left : in Prod_Arrays.Vector; Right : in 
Prod_Arrays.Vector) return Prod_Arrays.Vector
+   is begin
+      return Result : Prod_Arrays.Vector := Left do
+         if Right.First_Index < Result.First_Index then
+            Result.Set_First (Right.First_Index);
+         elsif Right.First_Index > Result.Last_Index then
+            Result.Set_Last (Right.First_Index);
+         end if;
+         if Right.Last_Index < Result.First_Index then
+            Result.Set_First (Right.Last_Index);
+         elsif Right.Last_Index > Result.Last_Index then
+            Result.Set_Last (Right.Last_Index);
+         end if;
+
+         for P of Right loop
+            if Result (P.LHS).LHS = Invalid_Token_ID then
+               Result (P.LHS) := P;
+            else
+               Result (P.LHS) := Merge (Result (P.LHS), P);
+            end if;
+         end loop;
+      end return;
+   end "and";
+
+end WisiToken.Wisi_Ada;
diff --git a/wisitoken-wisi_ada.ads b/wisitoken-wisi_ada.ads
new file mode 100644
index 0000000..0678d06
--- /dev/null
+++ b/wisitoken-wisi_ada.ads
@@ -0,0 +1,82 @@
+--  Abstract :
+--
+--  Type and operations for building a grammar directly in Ada source.
+--
+--  Copyright (C) 2003, 2013 - 2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 Ted Dennison
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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);
+
+with WisiToken.Productions;
+with WisiToken.Syntax_Trees;
+package WisiToken.Wisi_Ada is
+
+   function Only (Item : in Token_ID) return WisiToken.Token_ID_Arrays.Vector;
+   function "&" (Left : in Token_ID; Right : in Token_ID) return 
WisiToken.Token_ID_Arrays.Vector;
+
+   function "+"
+     (Tokens : in WisiToken.Token_ID_Arrays.Vector;
+      Action : in WisiToken.Syntax_Trees.Semantic_Action)
+     return WisiToken.Productions.Right_Hand_Side;
+   function "+"
+     (Tokens : in Token_ID;
+      Action : in WisiToken.Syntax_Trees.Semantic_Action)
+     return WisiToken.Productions.Right_Hand_Side;
+   function "+" (Action : in WisiToken.Syntax_Trees.Semantic_Action) return 
WisiToken.Productions.Right_Hand_Side;
+   --  Create the right hand side of a production.
+
+   function Only (Item : in WisiToken.Productions.Right_Hand_Side) return 
WisiToken.Productions.RHS_Arrays.Vector;
+   function "+" (Item : in WisiToken.Productions.Right_Hand_Side) return 
WisiToken.Productions.RHS_Arrays.Vector
+     renames Only;
+
+   function "or"
+     (Left  : in WisiToken.Productions.Instance;
+      Right : in WisiToken.Productions.Right_Hand_Side)
+     return WisiToken.Productions.Instance;
+
+   function "<="
+     (LHS  : in Token_ID;
+      RHSs : in WisiToken.Productions.RHS_Arrays.Vector)
+     return WisiToken.Productions.Instance;
+
+   function Only (Subject : in WisiToken.Productions.Instance) return 
WisiToken.Productions.Prod_Arrays.Vector;
+   function "+" (Subject : in WisiToken.Productions.Instance) return 
WisiToken.Productions.Prod_Arrays.Vector
+     renames Only;
+   --  First production in a grammar.
+
+   function "and"
+     (Left  : in WisiToken.Productions.Instance;
+      Right : in WisiToken.Productions.Instance)
+     return WisiToken.Productions.Prod_Arrays.Vector;
+   function "and"
+     (Left  : in WisiToken.Productions.Prod_Arrays.Vector;
+      Right : in WisiToken.Productions.Instance)
+     return WisiToken.Productions.Prod_Arrays.Vector;
+   function "and"
+     (Left : in WisiToken.Productions.Prod_Arrays.Vector;
+      Right : in WisiToken.Productions.Prod_Arrays.Vector)
+     return WisiToken.Productions.Prod_Arrays.Vector;
+   --  Create a grammar
+
+end WisiToken.Wisi_Ada;
diff --git a/wisitoken.adb b/wisitoken.adb
new file mode 100644
index 0000000..3553e4a
--- /dev/null
+++ b/wisitoken.adb
@@ -0,0 +1,351 @@
+-------------------------------------------------------------------------------
+--
+--  Copyright (C) 2009, 2014-2015, 2017, 2018 Stephe Leake
+--
+--  This file is part of the WisiToken package.
+--
+--  The WisiToken package is free software; you can redistribute it
+--  and/or modify it under the terms of the GNU General Public License
+--  as published by the Free Software Foundation; either version 3, or
+--  (at your option) any later version. The WisiToken package 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 the WisiToken package;
+--  see file GPL.txt. 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.
+-------------------------------------------------------------------------------
+
+with Ada.Strings.Fixed;
+package body WisiToken is
+
+   function Padded_Image (Item : in Token_ID; Desc : in Descriptor) return 
String
+   is begin
+      return Ada.Strings.Fixed.Head
+        (Desc.Image (Item).all,
+         (if Item in Desc.First_Terminal .. Desc.Last_Terminal
+          then Desc.Terminal_Image_Width
+          else Desc.Image_Width));
+   end Padded_Image;
+
+   function Image (Item : in Token_ID; Desc : in Descriptor) return String
+   is begin
+      return (if Item = Invalid_Token_ID then "" else Desc.Image (Item).all);
+   end Image;
+
+   procedure Put_Tokens (Descriptor : in WisiToken.Descriptor)
+   is
+      use Ada.Text_IO;
+   begin
+      for I in Token_ID'First .. Descriptor.Last_Nonterminal loop
+         Put_Line (Token_ID'Image (I) & " => " & Descriptor.Image (I).all);
+      end loop;
+   end Put_Tokens;
+
+   function Find_ID (Descriptor : in WisiToken.Descriptor; Name : in String) 
return Token_ID
+   is begin
+      for I in Descriptor.Image'Range loop
+         if Descriptor.Image (I).all = Name then
+            return I;
+         end if;
+      end loop;
+      raise SAL.Programmer_Error with "token name '" & Name & "' not found in 
descriptor.image";
+   end Find_ID;
+
+   procedure To_Vector (Item : in Token_ID_Array; Vector : in out 
Token_ID_Arrays.Vector)
+   is
+      J : Integer := Vector.First_Index;
+   begin
+      for ID of Item loop
+         Vector.Replace_Element (J, ID);
+         J := J + 1;
+      end loop;
+   end To_Vector;
+
+   function Shared_Prefix (A, B : in Token_ID_Arrays.Vector) return Natural
+   is
+      use all type Ada.Containers.Count_Type;
+      I : Natural := A.First_Index;
+      J : Natural := B.First_Index;
+   begin
+      if A.Length = 0 or B.Length = 0 then
+         return 0;
+      end if;
+
+      loop
+         exit when A (I) /= B (I) or I = A.Last_Index or J = B.Last_Index;
+         I := I + 1;
+         J := J + 1;
+      end loop;
+      return I - 1;
+   end Shared_Prefix;
+
+   function "&" (Left : in Token_ID_Set; Right : in Token_ID) return 
Token_ID_Set
+   is begin
+      return Result : Token_ID_Set := Left do
+         Result (Right) := True;
+      end return;
+   end "&";
+
+   function To_Token_ID_Set (First, Last : in Token_ID; Item : in 
Token_ID_Array) return Token_ID_Set
+   is begin
+      return Result : Token_ID_Set := (First .. Last => False)
+      do
+         for ID of Item loop
+            Result (ID) := True;
+         end loop;
+      end return;
+   end To_Token_ID_Set;
+
+   procedure To_Set (Item : in Token_ID_Arrays.Vector; Set : out Token_ID_Set)
+   is begin
+      for ID of Item loop
+         Set (ID) := True;
+      end loop;
+   end To_Set;
+
+   function To_Array (Item : in Token_ID_Set) return Token_ID_Arrays.Vector
+   is begin
+      return Result : Token_ID_Arrays.Vector do
+         for ID in Item'Range loop
+            if Item (ID) then
+               Result.Append (ID);
+            end if;
+         end loop;
+      end return;
+   end To_Array;
+
+   function Any (Item : in Token_ID_Set) return Boolean
+   is begin
+      for I in Item'Range loop
+         if Item (I) then
+            return True;
+         end if;
+      end loop;
+      return False;
+   end Any;
+
+   function Count (Item : in Token_ID_Set) return Integer
+   is
+      Result : Integer := 0;
+   begin
+      for I in Item'Range loop
+         if Item (I) then
+            Result := Result + 1;
+         end if;
+      end loop;
+      return Result;
+   end Count;
+
+   function Image
+     (Item      : in Token_ID_Set;
+      Desc      : in Descriptor;
+      Max_Count : in Integer := Integer'Last;
+      Inverted  : in Boolean := False)
+     return String
+   is
+      use Ada.Strings.Unbounded;
+      Result     : Unbounded_String;
+      Need_Comma : Boolean := False;
+      Count      : Integer := 0;
+
+      function Include (Item : in Boolean) return Boolean
+      is begin
+         if not Inverted then
+            return Item;
+
+         else
+            return not Item;
+         end if;
+      end Include;
+
+   begin
+      for I in Item'Range loop
+         if Include (Item (I)) then
+            if Need_Comma then
+               Result := Result & ", ";
+            end if;
+            Result     := Result & Image (I, Desc);
+            Need_Comma := True;
+            Count := Count + 1;
+            if Count = Max_Count then
+               return To_String (Result);
+            end if;
+         end if;
+      end loop;
+      return To_String (Result);
+   end Image;
+
+   function Image (Item : in Production_ID) return String
+   is begin
+      return '(' & Trimmed_Image (Item.LHS) & ',' & Natural'Image (Item.RHS) & 
')';
+   end Image;
+
+   function Trimmed_Image (Item : in Production_ID) return String
+   is begin
+      return Trimmed_Image (Item.LHS) & '.' & Trimmed_Image (Item.RHS);
+   end Trimmed_Image;
+
+   function Padded_Image (Item : in Production_ID; Width : in Integer) return 
String
+   is
+      use Ada.Strings.Fixed;
+   begin
+      return Result : String (1 .. Width) do
+         Move (Trimmed_Image (Item), Result, Justify => Ada.Strings.Right);
+      end return;
+   end Padded_Image;
+
+   function To_Vector (Item : in Production_ID_Array) return 
Production_ID_Arrays.Vector
+   is begin
+      return Result : Production_ID_Arrays.Vector do
+         for I of Item loop
+            Result.Append (I);
+         end loop;
+      end return;
+   end To_Vector;
+
+   function Slice (Item : in Token_Array_Token_Set; I : in Token_ID) return 
Token_ID_Set
+   is
+      Result : Token_ID_Set := (Item'First (2) .. Item'Last (2) => False);
+   begin
+      for J in Result'Range loop
+         Result (J) := Item (I, J);
+      end loop;
+      return Result;
+   end Slice;
+
+   function Any (Item : in Token_Array_Token_Set; I : in Token_ID) return 
Boolean
+   is begin
+      for J in Item'Range (2) loop
+         if Item (I, J) then
+            return True;
+         end if;
+      end loop;
+      return False;
+   end Any;
+
+   function Any (Item : in Token_Array_Token_Set) return Boolean
+   is begin
+      for I in Item'Range (1) loop
+         for J in Item'Range (2) loop
+            if Item (I, J) then
+               return True;
+            end if;
+         end loop;
+      end loop;
+      return False;
+   end Any;
+
+   procedure Or_Slice (Item : in out Token_Array_Token_Set; I : in Token_ID; 
Value : in Token_ID_Set)
+   is begin
+      for J in Item'Range (2) loop
+         Item (I, J) := Item (I, J) or Value (J);
+      end loop;
+   end Or_Slice;
+
+   procedure Put (Descriptor : in WisiToken.Descriptor; Item : in 
Token_Array_Token_Set)
+   is
+      use Ada.Text_IO;
+      Paren_Done : Boolean := False;
+   begin
+      if not Any (Item) then
+         Put_Line ("(others => (others => False))");
+      else
+         Put ("(");
+         for I in Item'Range (1) loop
+            if Any (Item, I) then
+               Put_Line (" " & Image (I, Descriptor) & " =>");
+               Put ("  (");
+               Paren_Done := False;
+               for J in Item'Range (2) loop
+                  if Item (I, J) then
+                     if Paren_Done then
+                        Put_Line (" |");
+                        Put ("   " & Image (J, Descriptor));
+                     else
+                        Paren_Done := True;
+                        Put (Image (J, Descriptor));
+                     end if;
+                  end if;
+               end loop;
+               if Paren_Done then
+                  Put_Line (" => True,");
+                  Put_Line ("   others => False)");
+               else
+                  Put_Line (" others => False),");
+               end if;
+            end if;
+         end loop;
+         Put_Line ((if Paren_Done then " " else "") & "others => (others => 
False))");
+      end if;
+   end Put;
+
+   function Error_Message
+     (File_Name : in String;
+      Line      : in Line_Number_Type;
+      Column    : in Ada.Text_IO.Count;
+      Message   : in String)
+     return String
+   is begin
+      return File_Name & ":" &
+        Trimmed_Image (if Line = Invalid_Line_Number then Integer'(0) else 
Integer (Line)) & ":" &
+        Trimmed_Image (Integer (Column)) & ": " &
+        Message;
+   end Error_Message;
+
+   function Image (Item : in Buffer_Region) return String
+   is begin
+      return "(" & Trimmed_Image (Integer (Item.First)) & " ." & 
Buffer_Pos'Image (Item.Last) & ")";
+   end Image;
+
+   function "and" (Left, Right : in Buffer_Region) return Buffer_Region
+   is begin
+      return (Buffer_Pos'Min (Left.First, Right.First), Buffer_Pos'Max 
(Left.Last, Right.Last));
+   end "and";
+
+   function Image
+     (Item       : in Base_Token;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is
+      ID_Image : constant String := WisiToken.Image (Item.ID, Descriptor);
+   begin
+      if Item.Char_Region = Null_Buffer_Region then
+         return "(" & ID_Image & ")";
+
+      else
+         return "(" & ID_Image & ", " & Image (Item.Char_Region) & ")";
+      end if;
+   end Image;
+
+   function Image
+     (Token      : in Base_Token_Index;
+      Terminals  : in Base_Token_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is begin
+      if Token = Invalid_Token_Index then
+         return "<invalid_token_index>";
+      else
+         return Token_Index'Image (Token) & ":" & Image (Terminals (Token), 
Descriptor);
+      end if;
+   end Image;
+
+   function Image
+     (Item       : in Recover_Token;
+      Descriptor : in WisiToken.Descriptor)
+     return String
+   is begin
+      return "(" & Image (Item.ID, Descriptor) &
+        (if Item.Byte_Region = Null_Buffer_Region then "" else ", " & Image 
(Item.Byte_Region)) & ")";
+   end Image;
+
+end WisiToken;
diff --git a/wisitoken.ads b/wisitoken.ads
new file mode 100644
index 0000000..734ff4a
--- /dev/null
+++ b/wisitoken.ads
@@ -0,0 +1,432 @@
+--  Abstract:
+--
+--  Root of WisiToken lexer/parser generator and exector.
+--
+--  The token type is an integer subtype, not an enumeration type, to
+--  avoid making this package generic, which would make all other
+--  packages generic.
+--
+--  Additional information about a token can be stored in the
+--  'augmented' field of the syntax tree; see
+--  wisitoken-syntax_trees.ads.
+--
+--  References:
+--
+--  [dragon] "Compilers Principles, Techniques, and Tools" by Aho,
+--  Sethi, and Ullman (aka: "The [Red] Dragon Book" due to the dragon
+--  on the cover).
+--
+--  Copyright (C) 2009, 2010, 2013 - 2015, 2017, 2018 Stephe Leake
+--  Copyright (C) 1999 FlightSafety International and Ted Dennison
+--
+--  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 MERCHANTABILITY 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.
+--
+--  This software was originally developed with the name OpenToken by
+--  the following company, and was released as open-source software as
+--  a service to the community:
+--
+--           FlightSafety International Simulation Systems Division
+--                    Broken Arrow, OK  USA  918-259-4000
+
+pragma License (Modified_GPL);
+
+with Ada.Containers;
+with Ada.Strings.Unbounded;
+with Ada.Text_IO;
+with Ada.Unchecked_Deallocation;
+with SAL.Gen_Trimmed_Image;
+with SAL.Gen_Unbounded_Definite_Queues;
+with SAL.Gen_Unbounded_Definite_Vectors.Gen_Image;
+with SAL.Gen_Unbounded_Definite_Vectors.Gen_Image_Aux;
+package WisiToken is
+
+   Syntax_Error : exception; -- no recovery for a syntax error was found
+
+   Parse_Error : exception; -- a non-recoverable non-fatal error was 
encountered; editing the input can fix the error.
+
+   Fatal_Error : exception; -- Error in code or grammar; editing input cannot 
fix error.
+
+   Grammar_Error : exception;
+   --  Grammar file has bad syntax, or grammar is not consistent (ie
+   --  unused tokens, missing productions, invalid actions)
+
+   User_Error : exception; -- other user error (ie command line parameter)
+
+   --  SAL.Programmer_Error : exception; -- a programming convention has been 
violated
+
+   subtype Positive_Index_Type is SAL.Peek_Type;
+   function Trimmed_Image is new SAL.Gen_Trimmed_Image (SAL.Base_Peek_Type);
+
+   type Unknown_State_Index is new Integer range -1 .. Integer'Last;
+   subtype State_Index is Unknown_State_Index range 0 .. 
Unknown_State_Index'Last;
+   Unknown_State : constant Unknown_State_Index := -1;
+
+   function Trimmed_Image is new SAL.Gen_Trimmed_Image (Unknown_State_Index);
+
+   package State_Index_Queues is new SAL.Gen_Unbounded_Definite_Queues 
(State_Index);
+   package State_Index_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Positive, State_Index);
+   function Image is new State_Index_Arrays.Gen_Image (Trimmed_Image);
+
+   ----------
+   --  Token IDs
+
+   type Token_ID is range 0 .. Integer'Last; -- 0 origin to match elisp array
+
+   Invalid_Token_ID : constant Token_ID := Token_ID'Last;
+
+   type Token_ID_Array_String is array (Token_ID range <>) of access constant 
String;
+   type Token_ID_Array_Natural is array (Token_ID range <>) of Natural;
+
+   type Descriptor
+     (First_Terminal    : Token_ID;
+      Last_Terminal     : Token_ID;
+      First_Nonterminal : Token_ID;
+      Last_Nonterminal  : Token_ID;
+      EOF_ID            : Token_ID;
+      Accept_ID         : Token_ID)
+   is record
+      --  Tokens in the range Token_ID'First .. First_Terminal - 1 are
+      --  non-reporting (comments, whitespace), and thus are not used in
+      --  generating parse tables.
+      --
+      --  Tokens in the range Last_Terminal + 1 .. Last_Nonterminal are
+      --  the nonterminals of a grammar.
+      --
+      --  Components are discriminants if they can be specified statically.
+
+      Case_Insensitive : Boolean; -- keywords and names
+      New_Line_ID      : Token_ID;
+      Comment_ID       : Token_ID;
+      Left_Paren_ID    : Token_ID;
+      Right_Paren_ID   : Token_ID;
+      --  If the language does not define these tokens, set them to
+      --  Invalid_Token_ID.
+
+      String_1_ID  : Token_ID; -- delimited by ', error if New_Line_ID
+      String_2_ID  : Token_ID; -- delimited by ", error if New_Line_ID
+      --
+      --  Support for missing quote error recovery. If the language does not
+      --  have two kinds of string literals, set one or both of these to
+      --  Invalid_Token_ID.
+
+      Embedded_Quote_Escape_Doubled : Boolean;
+      --  True if quote characters embedded in strings are escaped by
+      --  doubling (as in Ada); false if by preceding with backslash (as in
+      --  C).
+
+      Image : Token_ID_Array_String (Token_ID'First .. Last_Nonterminal);
+      --  User names for tokens.
+
+      Terminal_Image_Width : Integer;
+      Image_Width          : Integer; --  max width of Image
+
+      --  LALR generate needs a 'Propagate_ID' lookahead that is distinct
+      --  from all terminals. Since lookaheads are Token_ID_Set, we need to
+      --  allocate First_Terminal .. Last_Terminal for LR1 generate, and
+      --  First_Terminal .. Propagate_ID for LALR generate, so we define
+      --  Last_Lookahead. After the LR table is generated, Last_Lookahead is
+      --  no longer used.
+      Last_Lookahead : Token_ID;
+   end record;
+
+   function Padded_Image (Item : in Token_ID; Desc : in Descriptor) return 
String;
+   --  Return Desc.Image (Item), padded to Terminal_Image_Width (if Item
+   --  is a terminal) or to Image_Width.
+
+   function Image (Item : in Token_ID; Desc : in Descriptor) return String;
+   --  Return Desc.Image (Item), or empty string for Invalid_Token_ID.
+
+   function Trimmed_Image is new SAL.Gen_Trimmed_Image (Token_ID);
+
+   procedure Put_Tokens (Descriptor : in WisiToken.Descriptor);
+   --  Put user readable token list (token_id'first ..
+   --  descriptor.last_nonterminal) to Ada.Text_IO.Current_Output
+
+   function Find_ID (Descriptor : in WisiToken.Descriptor; Name : in String) 
return Token_ID;
+   --  Return index of Name in Descriptor.Image. If not found, raise 
Programmer_Error.
+
+   type Token_ID_Array is array (Positive range <>) of Token_ID;
+
+   package Token_ID_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Positive, Token_ID);
+
+   function Image is new Token_ID_Arrays.Gen_Image_Aux (WisiToken.Descriptor, 
Image);
+   function Trimmed_Image is new Token_ID_Arrays.Gen_Image (Trimmed_Image);
+
+   procedure To_Vector (Item : in Token_ID_Array; Vector : in out 
Token_ID_Arrays.Vector);
+
+   function Shared_Prefix (A, B : in Token_ID_Arrays.Vector) return Natural;
+   --  Return last index in A of a prefix shared between A, B; 0 if none.
+
+   type Token_ID_Set is array (Token_ID range <>) of Boolean;
+
+   function "&" (Left : in Token_ID_Set; Right : in Token_ID) return 
Token_ID_Set;
+   --  Include Left and Right in result.
+
+   function To_Token_ID_Set (First, Last : in Token_ID; Item : in 
Token_ID_Array) return Token_ID_Set;
+   --  First, Last determine size of result.
+   --  For each element in Item, set result (element) True.
+
+   procedure To_Set (Item : in Token_ID_Arrays.Vector; Set : out Token_ID_Set);
+   --  For each element of Item, set Set (element) True.
+
+   function To_Array (Item : in Token_ID_Set) return Token_ID_Arrays.Vector;
+
+   function Any (Item : in Token_ID_Set) return Boolean;
+
+   function Count (Item : in Token_ID_Set) return Integer;
+   --  Count of True elements.
+
+   function Image
+     (Item      : in Token_ID_Set;
+      Desc      : in Descriptor;
+      Max_Count : in Integer := Integer'Last;
+      Inverted  : in Boolean := False)
+     return String;
+   --  For diagnostics; not Ada syntax.
+
+   type Token_Array_Token_Set is array (Token_ID range <>, Token_ID range <>) 
of Boolean;
+
+   function Slice (Item : in Token_Array_Token_Set; I : in Token_ID) return 
Token_ID_Set;
+   function Any (Item : in Token_Array_Token_Set; I : in Token_ID) return 
Boolean;
+   function Any (Item : in Token_Array_Token_Set) return Boolean;
+   procedure Or_Slice (Item : in out Token_Array_Token_Set; I : in Token_ID; 
Value : in Token_ID_Set);
+
+   procedure Put (Descriptor : in WisiToken.Descriptor; Item : in 
Token_Array_Token_Set);
+   --  Put Item to Ada.Text_IO.Current_Output, using valid Ada aggregate
+   --  syntax.
+
+   type Token_Array_Token_ID is array (Token_ID range <>) of Token_ID;
+
+   package Token_Sequence_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Token_ID, Token_ID_Arrays.Vector);
+
+   ----------
+   --  Production IDs; see wisitoken-productions.ads for more
+
+   type Production_ID is record
+      LHS : Token_ID := Invalid_Token_ID;
+      RHS : Natural  := 0;
+      --  Index into the production table.
+   end record;
+
+   Invalid_Production_ID : constant Production_ID := (others => <>);
+
+   function Image (Item : in Production_ID) return String;
+   --  Ada positional aggregate syntax, for code generation.
+
+   function Trimmed_Image (Item : in Production_ID) return String;
+   --  Nonterm.rhs_index, both integers, no leading or trailing space;
+   --  for parse table output and diagnostics.
+
+   Prod_ID_Image_Width : constant Integer := 7;
+   --  Max width of Trimmed_Image
+
+   function Padded_Image (Item : in Production_ID; Width : in Integer) return 
String;
+   --  Trimmed_Image padded with leading spaces to Width
+
+   package Production_ID_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Positive, Production_ID);
+   function Image is new Production_ID_Arrays.Gen_Image (Image);
+   function Trimmed_Image is new Production_ID_Arrays.Gen_Image 
(Trimmed_Image);
+
+   type Production_ID_Array is array (Natural range <>) of Production_ID;
+
+   function To_Vector (Item : in Production_ID_Array) return 
Production_ID_Arrays.Vector;
+   function "+" (Item : in Production_ID_Array) return 
Production_ID_Arrays.Vector renames To_Vector;
+   function "+" (Item : in Production_ID) return Production_ID_Arrays.Vector 
is (To_Vector ((1 => Item)));
+
+   ----------
+   --  Tokens
+
+   type Base_Buffer_Pos is range 0 .. Integer'Last;
+   subtype Buffer_Pos is Base_Buffer_Pos range 1 .. Base_Buffer_Pos'Last; -- 
match Emacs buffer origin.
+   type Buffer_Region is record
+      First : Buffer_Pos;
+      Last  : Base_Buffer_Pos; --  allow representing null range.
+   end record;
+
+   Invalid_Buffer_Pos : constant Buffer_Pos    := Buffer_Pos'Last;
+   Null_Buffer_Region : constant Buffer_Region := (Buffer_Pos'Last, 
Buffer_Pos'First);
+
+   function Length (Region : in Buffer_Region) return Natural is (Natural 
(Region.Last - Region.First + 1));
+
+   function Inside (Pos : in Buffer_Pos; Region : in Buffer_Region) return 
Boolean
+     is (Region.First <= Pos and Pos <= Region.Last);
+
+   function Image (Item : in Buffer_Region) return String;
+
+   function "and" (Left, Right : in Buffer_Region) return Buffer_Region;
+   --  Return region enclosing both Left and Right.
+
+   type Line_Number_Type is range 1 .. Natural'Last; -- Match Emacs buffer 
line numbers.
+
+   Invalid_Line_Number : constant Line_Number_Type := Line_Number_Type'Last;
+
+   type Base_Token is tagged record
+      --  Base_Token is used in the core parser. The parser only needs ID;
+      --  semantic checks need Byte_Region to compare names. Line, Col, and
+      --  Char_Region are included for error messages.
+      ID : Token_ID := Invalid_Token_ID;
+
+      Byte_Region : Buffer_Region := Null_Buffer_Region;
+      --  Index into the Lexer buffer for the token text.
+
+      Line   : Line_Number_Type  := Invalid_Line_Number;
+      Column : Ada.Text_IO.Count := 0;
+      --  At start of token.
+
+      Char_Region : Buffer_Region := Null_Buffer_Region;
+      --  Character position, useful for finding the token location in Emacs
+      --  buffers.
+   end record;
+
+   type Base_Token_Class_Access is access all Base_Token'Class;
+   type Base_Token_Class_Access_Array is array (Positive_Index_Type range <>) 
of Base_Token_Class_Access;
+
+   function Image
+     (Item       : in Base_Token;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+   --  For debug/test messages.
+
+   procedure Free is new Ada.Unchecked_Deallocation (Base_Token'Class, 
Base_Token_Class_Access);
+
+   Invalid_Token : constant Base_Token := (others => <>);
+
+   type Base_Token_Index is range 0 .. Integer'Last;
+   subtype Token_Index is Base_Token_Index range 1 .. Base_Token_Index'Last;
+
+   type Token_Index_Array is array (Natural range <>) of Token_Index;
+
+   type Base_Token_Array is array (Positive_Index_Type range <>) of Base_Token;
+
+   package Base_Token_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Token_Index, Base_Token);
+   type Base_Token_Array_Access is access all Base_Token_Arrays.Vector;
+
+   Invalid_Token_Index : constant Base_Token_Index := 
Base_Token_Arrays.No_Index;
+
+   package Line_Begin_Token_Vectors is new SAL.Gen_Unbounded_Definite_Vectors 
(Line_Number_Type, Base_Token_Index);
+
+   function Image is new Base_Token_Arrays.Gen_Image_Aux 
(WisiToken.Descriptor, Image);
+
+   function Image
+     (Token      : in Base_Token_Index;
+      Terminals  : in Base_Token_Arrays.Vector;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+
+   type Recover_Token is record
+      --  Maintaining a syntax tree during recover is too slow, so we store
+      --  enough information in the recover stack to perform semantic_checks
+      --  and to apply the solution to the main parser state. We make
+      --  thousands of copies of the parse stack during recover, so
+      --  minimizing size is critical.
+      ID : Token_ID := Invalid_Token_ID;
+
+      Byte_Region : Buffer_Region := Null_Buffer_Region;
+      --  Byte_Region is used to detect empty tokens, for cost and other 
issues.
+
+      Min_Terminal_Index : Base_Token_Index := Invalid_Token_Index;
+      --  For terminals, index of this token in Shared_Parser.Terminals. For
+      --  nonterminals, minimum of contained tokens. For virtuals,
+      --  Invalid_Token_Index. Used for push_back of nonterminals.
+
+      Name : Buffer_Region := Null_Buffer_Region;
+      --  Set and used by semantic_checks.
+
+      Virtual : Boolean := False;
+      --  For terminals, True if inserted by recover. For nonterminals, True
+      --  if any contained token has Virtual = True. Used by Semantic_Checks
+      --  and push_back.
+   end record;
+
+   function Image
+     (Item       : in Recover_Token;
+      Descriptor : in WisiToken.Descriptor)
+     return String;
+
+   type Recover_Token_Array is array (Positive_Index_Type range <>) of 
Recover_Token;
+
+   package Recover_Token_Arrays is new SAL.Gen_Unbounded_Definite_Vectors 
(Token_Index, Recover_Token);
+
+   function Image is new Recover_Token_Arrays.Gen_Image_Aux 
(WisiToken.Descriptor, Image);
+
+   ----------
+   --  Trace, debug
+
+   Trace_Parse : Integer  := 0;
+   --  If Trace_Parse > 0, Parse prints messages helpful for debugging
+   --  the grammar and/or the parser; higher value prints more.
+   --
+   --  Trace_Parse levels; output info if Trace_Parse > than:
+   --
+   Outline     : constant := 0; -- spawn/terminate parallel parsers, error 
recovery enter/exit
+   Detail      : constant := 1; -- add each parser cycle
+   Extra       : constant := 2; -- add pending semantic state operations
+   Lexer_Debug : constant := 3; -- add lexer debug
+
+   Trace_McKenzie : Integer  := 0;
+   --  If Trace_McKenzie > 0, Parse prints messages helpful for debugging 
error recovery.
+   --
+   --  Outline - error recovery enter/exit
+   --  Detail  - add each error recovery configuration
+   --  Extra   - add error recovery parse actions
+
+   Trace_Action : Integer := 0;
+   --  Output during Execute_Action, and unit tests.
+
+   Trace_Generate : Integer := 0;
+   --  Output during grammar generation.
+
+   Debug_Mode : Boolean := False;
+   --  If True, Output stack traces, propagate exceptions to top level.
+   --  Otherwise, be robust to errors, so user does not notice them.
+
+   type Trace (Descriptor : not null access constant WisiToken.Descriptor) is 
abstract tagged limited null record;
+   --  Output for tests/debugging. Descriptor included here because many
+   --  uses of Trace will use Image (Item, Descriptor);
+
+   procedure Put (Trace : in out WisiToken.Trace; Item : in String) is 
abstract;
+   --  Put Item to the Trace display.
+
+   procedure Put_Line (Trace : in out WisiToken.Trace; Item : in String) is 
abstract;
+   --  Put Item to the Trace display, followed by a newline.
+
+   procedure New_Line (Trace : in out WisiToken.Trace) is abstract;
+   --  Put a newline to the Trace display.
+
+   ----------
+   --  Misc
+
+   function "+" (Item : in String) return 
Ada.Strings.Unbounded.Unbounded_String
+     renames Ada.Strings.Unbounded.To_Unbounded_String;
+
+   function "-" (Item : in Ada.Strings.Unbounded.Unbounded_String) return 
String
+     renames Ada.Strings.Unbounded.To_String;
+
+   function Trimmed_Image is new SAL.Gen_Trimmed_Image (Integer);
+   function Trimmed_Image is new SAL.Gen_Trimmed_Image 
(Ada.Containers.Count_Type);
+
+   function Error_Message
+     (File_Name : in String;
+      Line      : in Line_Number_Type;
+      Column    : in Ada.Text_IO.Count;
+      Message   : in String)
+     return String;
+   --  Return Gnu-formatted error message.
+
+   type Names_Array is array (Integer range <>) of access constant String;
+   type Names_Array_Access is access Names_Array;
+   type Names_Array_Array is array (WisiToken.Token_ID range <>) of 
Names_Array_Access;
+
+end WisiToken;
diff --git a/wisitoken.gpr b/wisitoken.gpr
new file mode 100644
index 0000000..82b23de
--- /dev/null
+++ b/wisitoken.gpr
@@ -0,0 +1,56 @@
+--  For the wisi ELPA package; provide WisiToken to other ELPA packages
+--  SAL sources are also in the wisi package.
+with "gnatcoll";
+with "standard_common";
+project WisiToken is
+
+   for Source_Dirs use (".");
+
+   for Languages use ("Ada", "C");
+
+   case Standard_Common.Profile is
+   when "On" =>
+      for Object_Dir use "obj_pro";
+      for Exec_Dir use "exec_pro";
+
+   when "Off" =>
+      for Object_Dir use "obj";
+      for Exec_Dir use ".";
+   end case;
+
+   package Compiler is
+      case Standard_Common.Build is
+      when "Debug" =>
+         for Default_Switches ("Ada") use
+           Standard_Common.Compiler.Debug_Switches &
+           Standard_Common.Compiler.Style_Checks;
+
+      when "Normal" =>
+         for Default_Switches ("Ada") use
+           Standard_Common.Compiler.Release_Switches &
+           Standard_Common.Compiler.Style_Checks;
+      end case;
+
+   end Compiler;
+
+   package Builder is
+      for Switches ("Ada") use Standard_Common.Builder'Default_Switches 
("Ada");
+
+      --  We use ".exe" extension even on non-Windows, to simplify the 
makefiles.
+      for Executable_Suffix use ".exe";
+
+      case Standard_Common.Profile is
+      when "On" =>
+         for Global_Compilation_Switches ("Ada") use ("-pg");
+
+      when "Off" =>
+         null;
+      end case;
+
+   end Builder;
+
+   package Binder is
+      for Default_Switches ("Ada") use Standard_Common.Binder'Default_Switches 
("Ada");
+   end Binder;
+
+end WisiToken;
diff --git a/wisitoken_grammar_actions.adb b/wisitoken_grammar_actions.adb
new file mode 100644
index 0000000..c9970d9
--- /dev/null
+++ b/wisitoken_grammar_actions.adb
@@ -0,0 +1,105 @@
+--  generated parser support file.
+--  command line: wisitoken-bnf-generate.exe  --generate LALR Ada re2c 
wisitoken_grammar.wy
+--
+
+--  Copyright (C) 2017, 2018 Free Software Foundation, Inc.
+--
+--  Author: Stephen Leake <stephe-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/>.
+
+with WisiToken_Grammar_Runtime; use WisiToken_Grammar_Runtime;
+
+package body Wisitoken_Grammar_Actions is
+
+   procedure declaration_0
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      pragma Unreferenced (Nonterm);
+   begin
+      Add_Declaration (User_Data, Tree, Tokens);
+   end declaration_0;
+
+   procedure declaration_1
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      pragma Unreferenced (Nonterm);
+   begin
+      Add_Declaration (User_Data, Tree, Tokens);
+   end declaration_1;
+
+   procedure declaration_2
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      pragma Unreferenced (Nonterm);
+   begin
+      Add_Declaration (User_Data, Tree, Tokens);
+   end declaration_2;
+
+   procedure declaration_3
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      pragma Unreferenced (Nonterm);
+   begin
+      Add_Declaration (User_Data, Tree, Tokens);
+   end declaration_3;
+
+   procedure declaration_4
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      pragma Unreferenced (Nonterm);
+   begin
+      Start_If (User_Data, Tree, Tokens);
+   end declaration_4;
+
+   procedure declaration_5
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      pragma Unreferenced (Tree, Nonterm, Tokens);
+   begin
+      End_If (User_Data);
+   end declaration_5;
+
+   procedure nonterminal_0
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      pragma Unreferenced (Nonterm);
+   begin
+      Add_Nonterminal (User_Data, Tree, Tokens);
+   end nonterminal_0;
+
+end Wisitoken_Grammar_Actions;
diff --git a/wisitoken_grammar_actions.ads b/wisitoken_grammar_actions.ads
new file mode 100644
index 0000000..25b68e8
--- /dev/null
+++ b/wisitoken_grammar_actions.ads
@@ -0,0 +1,168 @@
+--  generated parser support file.
+--  command line: wisitoken-bnf-generate.exe  --generate LALR Ada re2c PROCESS 
wisitoken_grammar.wy
+--
+
+--  Copyright (C) 2017, 2018 Free Software Foundation, Inc.
+--
+--  Author: Stephen Leake <stephe-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/>.
+
+with WisiToken.Syntax_Trees;
+package Wisitoken_Grammar_Actions is
+
+   Descriptor : aliased WisiToken.Descriptor :=
+     (First_Terminal                => 3,
+      Last_Terminal                 => 25,
+      First_Nonterminal             => 26,
+      Last_Nonterminal              => 37,
+      EOF_ID                        => 25,
+      Accept_ID                     => 26,
+      Case_Insensitive              => False,
+      New_Line_ID                   => 1,
+      Comment_ID                    => 2,
+      Left_Paren_ID                 => 2147483647,
+      Right_Paren_ID                => 2147483647,
+      String_1_ID                   => 24,
+      String_2_ID                   => 23,
+      Embedded_Quote_Escape_Doubled => False,
+      Image                         =>
+        (new String'("WHITESPACE"),
+         new String'("NEW_LINE"),
+         new String'("COMMENT"),
+         new String'("CODE"),
+         new String'("END"),
+         new String'("IF"),
+         new String'("KEYWORD"),
+         new String'("NON_GRAMMAR"),
+         new String'("TOKEN"),
+         new String'("RAW_CODE"),
+         new String'("REGEXP"),
+         new String'("ACTION"),
+         new String'("BAR"),
+         new String'("COLON"),
+         new String'("COMMA"),
+         new String'("EQUAL"),
+         new String'("GREATER"),
+         new String'("LESS"),
+         new String'("PERCENT"),
+         new String'("SEMICOLON"),
+         new String'("SLASH"),
+         new String'("NUMERIC_LITERAL"),
+         new String'("IDENTIFIER"),
+         new String'("STRING_LITERAL"),
+         new String'("STRING_LITERAL_CASE_INS"),
+         new String'("Wisi_EOI"),
+         new String'("wisitoken_accept"),
+         new String'("declaration"),
+         new String'("token_keyword_non_grammar"),
+         new String'("identifier_list"),
+         new String'("declaration_item_list"),
+         new String'("declaration_item"),
+         new String'("nonterminal"),
+         new String'("rhs_list"),
+         new String'("rhs"),
+         new String'("token_list"),
+         new String'("compilation_unit"),
+         new String'("compilation_unit_list")),
+      Terminal_Image_Width => 23,
+      Image_Width          => 25,
+      Last_Lookahead       => 26);
+
+   type Token_Enum_ID is
+     (WHITESPACE_ID,
+      NEW_LINE_ID,
+      COMMENT_ID,
+      CODE_ID,
+      END_ID,
+      IF_ID,
+      KEYWORD_ID,
+      NON_GRAMMAR_ID,
+      TOKEN_ID,
+      RAW_CODE_ID,
+      REGEXP_ID,
+      ACTION_ID,
+      BAR_ID,
+      COLON_ID,
+      COMMA_ID,
+      EQUAL_ID,
+      GREATER_ID,
+      LESS_ID,
+      PERCENT_ID,
+      SEMICOLON_ID,
+      SLASH_ID,
+      NUMERIC_LITERAL_ID,
+      IDENTIFIER_ID,
+      STRING_LITERAL_ID,
+      STRING_LITERAL_CASE_INS_ID,
+      Wisi_EOI_ID,
+      wisitoken_accept_ID,
+      declaration_ID,
+      token_keyword_non_grammar_ID,
+      identifier_list_ID,
+      declaration_item_list_ID,
+      declaration_item_ID,
+      nonterminal_ID,
+      rhs_list_ID,
+      rhs_ID,
+      token_list_ID,
+      compilation_unit_ID,
+      compilation_unit_list_ID);
+
+   type Token_Enum_ID_Array is array (Positive range <>) of Token_Enum_ID;
+   use all type WisiToken.Token_ID;
+   function "+" (Item : in Token_Enum_ID) return WisiToken.Token_ID
+     is (WisiToken.Token_ID'First + Token_Enum_ID'Pos (Item));
+   function To_Token_Enum (Item : in WisiToken.Token_ID) return Token_Enum_ID
+     is (Token_Enum_ID'Val (Item - WisiToken.Token_ID'First));
+   function "-" (Item : in WisiToken.Token_ID) return Token_Enum_ID renames 
To_Token_Enum;
+
+   procedure declaration_0
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+   procedure declaration_1
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+   procedure declaration_2
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+   procedure declaration_3
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+   procedure declaration_4
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+   procedure declaration_5
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+   procedure nonterminal_0
+    (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+     Tree      : in out WisiToken.Syntax_Trees.Tree;
+     Nonterm   : in     WisiToken.Syntax_Trees.Valid_Node_Index;
+     Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+end Wisitoken_Grammar_Actions;
diff --git a/wisitoken_grammar_main.adb b/wisitoken_grammar_main.adb
new file mode 100644
index 0000000..5470e30
--- /dev/null
+++ b/wisitoken_grammar_main.adb
@@ -0,0 +1,305 @@
+--  generated parser support file.
+--  command line: wisitoken-bnf-generate.exe  --generate LALR Ada re2c 
wisitoken_grammar.wy
+--
+
+--  Copyright (C) 2017, 2018 Free Software Foundation, Inc.
+--
+--  Author: Stephen Leake <stephe-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/>.
+
+with Wisitoken_Grammar_Actions; use Wisitoken_Grammar_Actions;
+with WisiToken.Lexer.re2c;
+with wisitoken_grammar_re2c_c;
+package body Wisitoken_Grammar_Main is
+
+   package Lexer is new WisiToken.Lexer.re2c
+     (wisitoken_grammar_re2c_c.New_Lexer,
+      wisitoken_grammar_re2c_c.Free_Lexer,
+      wisitoken_grammar_re2c_c.Reset_Lexer,
+      wisitoken_grammar_re2c_c.Next_Token);
+
+   procedure Create_Parser
+     (Parser                       :    out 
WisiToken.Parse.LR.Parser_No_Recover.Parser;
+      Trace                        : not null access WisiToken.Trace'Class;
+      User_Data                    : in     
WisiToken.Syntax_Trees.User_Data_Access)
+   is
+      use WisiToken.Parse.LR;
+      Table : constant Parse_Table_Ptr := new Parse_Table
+        (State_First       => 0,
+         State_Last        => 61,
+         First_Terminal    => 3,
+         Last_Terminal     => 25,
+         First_Nonterminal => 26,
+         Last_Nonterminal  => 37);
+   begin
+      declare
+         procedure Subr_1
+         is begin
+            Add_Action (Table.States (0), 18, 1);
+            Add_Action (Table.States (0), 22, 2);
+            Add_Error (Table.States (0));
+            Add_Goto (Table.States (0), 27, 3);
+            Add_Goto (Table.States (0), 32, 4);
+            Add_Goto (Table.States (0), 36, 5);
+            Add_Goto (Table.States (0), 37, 6);
+            Set_Minimal_Action (Table.States (0).Minimal_Complete_Actions, (1 
=> (Shift, 18, 1)));
+            Add_Action (Table.States (1), 3, 7);
+            Add_Action (Table.States (1), 4, 8);
+            Add_Action (Table.States (1), 5, 9);
+            Add_Action (Table.States (1), 6, 10);
+            Add_Action (Table.States (1), 7, 11);
+            Add_Action (Table.States (1), 8, 12);
+            Add_Action (Table.States (1), 22, 13);
+            Add_Error (Table.States (1));
+            Add_Goto (Table.States (1), 28, 14);
+            Set_Minimal_Action (Table.States (1).Minimal_Complete_Actions, 
((Shift, 3, 7), (Shift, 4, 8), (Shift, 5,
+            9), (Shift, 6, 10), (Shift, 22, 13)));
+            Add_Action (Table.States (2), 13, 15);
+            Add_Error (Table.States (2));
+            Set_Minimal_Action (Table.States (2).Minimal_Complete_Actions, (1 
=> (Shift, 13, 15)));
+            Add_Action (Table.States (3), (18, 22, 25), (36, 0), 1, null, 
null);
+            Set_Minimal_Action (Table.States (3).Minimal_Complete_Actions, (1 
=> (Reduce, 36, 1)));
+            Add_Action (Table.States (4), (18, 22, 25), (36, 1), 1, null, 
null);
+            Set_Minimal_Action (Table.States (4).Minimal_Complete_Actions, (1 
=> (Reduce, 36, 1)));
+            Add_Action (Table.States (5), (18, 22, 25), (37, 0), 1, null, 
null);
+            Set_Minimal_Action (Table.States (5).Minimal_Complete_Actions, (1 
=> (Reduce, 37, 1)));
+            Add_Action (Table.States (6), 18, 1);
+            Add_Action (Table.States (6), 22, 2);
+            Add_Action (Table.States (6), 25, Accept_It, (26, 0), 1, null, 
null);
+            Add_Error (Table.States (6));
+            Add_Goto (Table.States (6), 27, 3);
+            Add_Goto (Table.States (6), 32, 4);
+            Add_Goto (Table.States (6), 36, 16);
+            Add_Action (Table.States (7), 22, 17);
+            Add_Error (Table.States (7));
+            Add_Goto (Table.States (7), 29, 18);
+            Set_Minimal_Action (Table.States (7).Minimal_Complete_Actions, (1 
=> (Shift, 22, 17)));
+            Add_Action (Table.States (8), 5, 19);
+            Add_Error (Table.States (8));
+            Set_Minimal_Action (Table.States (8).Minimal_Complete_Actions, (1 
=> (Shift, 5, 19)));
+            Add_Action (Table.States (9), 22, 20);
+            Add_Error (Table.States (9));
+            Set_Minimal_Action (Table.States (9).Minimal_Complete_Actions, (1 
=> (Shift, 22, 20)));
+            Add_Action (Table.States (10), (1 =>  22), (28, 0), 1, null, null);
+            Set_Minimal_Action (Table.States (10).Minimal_Complete_Actions, (1 
=> (Reduce, 28, 1)));
+            Add_Action (Table.States (11), 17, 21);
+            Add_Error (Table.States (11));
+            Set_Minimal_Action (Table.States (11).Minimal_Complete_Actions, (1 
=> (Shift, 17, 21)));
+            Add_Action (Table.States (12), 17, 22);
+            Add_Error (Table.States (12));
+            Set_Minimal_Action (Table.States (12).Minimal_Complete_Actions, (1 
=> (Shift, 17, 22)));
+            Add_Action (Table.States (13), 8, 23);
+            Add_Action (Table.States (13), 10, 24);
+            Add_Action (Table.States (13), 14, 25);
+            Add_Action (Table.States (13), 15, 26);
+            Add_Action (Table.States (13), 18, Reduce, (27, 3), 2, 
declaration_3'Access, null);
+            Add_Action (Table.States (13), 20, 27);
+            Add_Action (Table.States (13), 21, 28);
+            Add_Action (Table.States (13), 22, 29, (27, 3), 2, 
declaration_3'Access, null);
+            Add_Action (Table.States (13), 23, 30);
+            Add_Action (Table.States (13), 24, 31);
+            Add_Action (Table.States (13), 25, Reduce, (27, 3), 2, 
declaration_3'Access, null);
+            Add_Error (Table.States (13));
+            Add_Goto (Table.States (13), 30, 32);
+            Add_Goto (Table.States (13), 31, 33);
+            Set_Minimal_Action (Table.States (13).Minimal_Complete_Actions, (1 
=> (Reduce, 27, 2)));
+            Add_Action (Table.States (14), 22, 34);
+            Add_Error (Table.States (14));
+            Set_Minimal_Action (Table.States (14).Minimal_Complete_Actions, (1 
=> (Shift, 22, 34)));
+            Add_Action (Table.States (15), 12, Reduce, (34, 0), 0, null, null);
+            Add_Action (Table.States (15), 18, Reduce, (34, 0), 0, null, null);
+            Add_Action (Table.States (15), 19, Reduce, (34, 0), 0, null, null);
+            Add_Action (Table.States (15), 22, 35);
+            Add_Error (Table.States (15));
+            Add_Goto (Table.States (15), 33, 36);
+            Add_Goto (Table.States (15), 34, 37);
+            Add_Goto (Table.States (15), 35, 38);
+            Set_Minimal_Action (Table.States (15).Minimal_Complete_Actions, (1 
=> (Reduce, 33, 0)));
+            Add_Action (Table.States (16), (18, 22, 25), (37, 1), 2, null, 
null);
+            Set_Minimal_Action (Table.States (16).Minimal_Complete_Actions, (1 
=> (Reduce, 37, 2)));
+            Add_Action (Table.States (17), (9, 22), (29, 0), 1, null, null);
+            Set_Minimal_Action (Table.States (17).Minimal_Complete_Actions, (1 
=> (Reduce, 29, 1)));
+            Add_Action (Table.States (18), 9, 39);
+            Add_Action (Table.States (18), 22, 40);
+            Add_Error (Table.States (18));
+            Set_Minimal_Action (Table.States (18).Minimal_Complete_Actions, (1 
=> (Shift, 9, 39)));
+            Add_Action (Table.States (19), (18, 22, 25), (27, 5), 3, 
declaration_5'Access, null);
+            Set_Minimal_Action (Table.States (19).Minimal_Complete_Actions, (1 
=> (Reduce, 27, 3)));
+            Add_Action (Table.States (20), 15, 41);
+            Add_Error (Table.States (20));
+            Set_Minimal_Action (Table.States (20).Minimal_Complete_Actions, (1 
=> (Shift, 15, 41)));
+            Add_Action (Table.States (21), 22, 42);
+            Add_Error (Table.States (21));
+            Set_Minimal_Action (Table.States (21).Minimal_Complete_Actions, (1 
=> (Shift, 22, 42)));
+            Add_Action (Table.States (22), 22, 43);
+            Add_Error (Table.States (22));
+            Set_Minimal_Action (Table.States (22).Minimal_Complete_Actions, (1 
=> (Shift, 22, 43)));
+            Add_Action (Table.States (23), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 8), 1, null, null);
+            Set_Minimal_Action (Table.States (23).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (24), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 4), 1, null, null);
+            Set_Minimal_Action (Table.States (24).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (25), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 0), 1, null, null);
+            Set_Minimal_Action (Table.States (25).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (26), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 2), 1, null, null);
+            Set_Minimal_Action (Table.States (26).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (27), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 5), 1, null, null);
+            Set_Minimal_Action (Table.States (27).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (28), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 3), 1, null, null);
+            Set_Minimal_Action (Table.States (28).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (29), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 1), 1, null, null);
+            Set_Minimal_Action (Table.States (29).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (30), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 6), 1, null, null);
+            Set_Minimal_Action (Table.States (30).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (31), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (31, 7), 1, null, null);
+            Set_Minimal_Action (Table.States (31).Minimal_Complete_Actions, (1 
=> (Reduce, 31, 1)));
+            Add_Action (Table.States (32), 8, 23);
+            Add_Action (Table.States (32), 10, 24);
+            Add_Action (Table.States (32), 14, 25);
+            Add_Action (Table.States (32), 15, 26);
+            Add_Action (Table.States (32), 18, Reduce, (27, 2), 3, 
declaration_2'Access, null);
+            Add_Action (Table.States (32), 20, 27);
+            Add_Action (Table.States (32), 21, 28);
+            Add_Action (Table.States (32), 22, 29, (27, 2), 3, 
declaration_2'Access, null);
+            Add_Action (Table.States (32), 23, 30);
+            Add_Action (Table.States (32), 24, 31);
+            Add_Action (Table.States (32), 25, Reduce, (27, 2), 3, 
declaration_2'Access, null);
+            Add_Error (Table.States (32));
+            Add_Goto (Table.States (32), 31, 44);
+            Set_Minimal_Action (Table.States (32).Minimal_Complete_Actions, (1 
=> (Reduce, 27, 3)));
+            Add_Action (Table.States (33), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (30, 0), 1, null, null);
+            Set_Minimal_Action (Table.States (33).Minimal_Complete_Actions, (1 
=> (Reduce, 30, 1)));
+            Add_Action (Table.States (34), 8, 23);
+            Add_Action (Table.States (34), 10, 24);
+            Add_Action (Table.States (34), 14, 25);
+            Add_Action (Table.States (34), 15, 26);
+            Add_Action (Table.States (34), 20, 27);
+            Add_Action (Table.States (34), 21, 28);
+            Add_Action (Table.States (34), 22, 29);
+            Add_Action (Table.States (34), 23, 30);
+            Add_Action (Table.States (34), 24, 31);
+            Add_Error (Table.States (34));
+            Add_Goto (Table.States (34), 30, 45);
+            Add_Goto (Table.States (34), 31, 33);
+            Set_Minimal_Action (Table.States (34).Minimal_Complete_Actions, (1 
=> (Shift, 8, 23)));
+            Add_Action (Table.States (35), (11, 12, 18, 19, 22), (35, 0), 1, 
null, null);
+            Set_Minimal_Action (Table.States (35).Minimal_Complete_Actions, (1 
=> (Reduce, 35, 1)));
+            Add_Action (Table.States (36), 12, 46);
+            Add_Action (Table.States (36), 18, 47);
+            Add_Action (Table.States (36), 19, 48);
+            Add_Error (Table.States (36));
+            Set_Minimal_Action (Table.States (36).Minimal_Complete_Actions, (1 
=> (Shift, 19, 48)));
+            Add_Action (Table.States (37), (12, 18, 19), (33, 0), 1, null, 
null);
+            Set_Minimal_Action (Table.States (37).Minimal_Complete_Actions, (1 
=> (Reduce, 33, 1)));
+            Add_Action (Table.States (38), 11, 49);
+            Add_Action (Table.States (38), 12, Reduce, (34, 1), 1, null, null);
+            Add_Action (Table.States (38), 18, Reduce, (34, 1), 1, null, null);
+            Add_Action (Table.States (38), 19, Reduce, (34, 1), 1, null, null);
+            Add_Action (Table.States (38), 22, 50);
+            Add_Error (Table.States (38));
+            Set_Minimal_Action (Table.States (38).Minimal_Complete_Actions, (1 
=> (Reduce, 34, 1)));
+            Add_Action (Table.States (39), (18, 22, 25), (27, 1), 4, 
declaration_1'Access, null);
+            Set_Minimal_Action (Table.States (39).Minimal_Complete_Actions, (1 
=> (Reduce, 27, 4)));
+            Add_Action (Table.States (40), (9, 22), (29, 1), 2, null, null);
+            Set_Minimal_Action (Table.States (40).Minimal_Complete_Actions, (1 
=> (Reduce, 29, 2)));
+            Add_Action (Table.States (41), 22, 51);
+            Add_Error (Table.States (41));
+            Set_Minimal_Action (Table.States (41).Minimal_Complete_Actions, (1 
=> (Shift, 22, 51)));
+            Add_Action (Table.States (42), 16, 52);
+            Add_Error (Table.States (42));
+            Set_Minimal_Action (Table.States (42).Minimal_Complete_Actions, (1 
=> (Shift, 16, 52)));
+            Add_Action (Table.States (43), 16, 53);
+            Add_Error (Table.States (43));
+            Set_Minimal_Action (Table.States (43).Minimal_Complete_Actions, (1 
=> (Shift, 16, 53)));
+            Add_Action (Table.States (44), (8, 10, 14, 15, 18, 20, 21, 22, 23, 
24, 25), (30, 1), 2, null, null);
+            Set_Minimal_Action (Table.States (44).Minimal_Complete_Actions, (1 
=> (Reduce, 30, 2)));
+            Add_Action (Table.States (45), 8, 23);
+            Add_Action (Table.States (45), 10, 24);
+            Add_Action (Table.States (45), 14, 25);
+            Add_Action (Table.States (45), 15, 26);
+            Add_Action (Table.States (45), 18, Reduce, (27, 0), 4, 
declaration_0'Access, null);
+            Add_Action (Table.States (45), 20, 27);
+            Add_Action (Table.States (45), 21, 28);
+            Add_Action (Table.States (45), 22, 29, (27, 0), 4, 
declaration_0'Access, null);
+            Add_Action (Table.States (45), 23, 30);
+            Add_Action (Table.States (45), 24, 31);
+            Add_Action (Table.States (45), 25, Reduce, (27, 0), 4, 
declaration_0'Access, null);
+            Add_Error (Table.States (45));
+            Add_Goto (Table.States (45), 31, 44);
+            Set_Minimal_Action (Table.States (45).Minimal_Complete_Actions, (1 
=> (Reduce, 27, 4)));
+            Add_Action (Table.States (46), 12, Reduce, (34, 0), 0, null, null);
+            Add_Action (Table.States (46), 18, Reduce, (34, 0), 0, null, null);
+            Add_Action (Table.States (46), 19, Reduce, (34, 0), 0, null, null);
+            Add_Action (Table.States (46), 22, 35);
+            Add_Error (Table.States (46));
+            Add_Goto (Table.States (46), 34, 54);
+            Add_Goto (Table.States (46), 35, 38);
+            Set_Minimal_Action (Table.States (46).Minimal_Complete_Actions, (1 
=> (Reduce, 34, 0)));
+            Add_Action (Table.States (47), 4, 55);
+            Add_Action (Table.States (47), 5, 56);
+            Add_Error (Table.States (47));
+            Set_Minimal_Action (Table.States (47).Minimal_Complete_Actions, 
((Shift, 4, 55), (Shift, 5, 56)));
+            Add_Action (Table.States (48), (18, 22, 25), (32, 0), 4, 
nonterminal_0'Access, null);
+            Set_Minimal_Action (Table.States (48).Minimal_Complete_Actions, (1 
=> (Reduce, 32, 4)));
+            Add_Action (Table.States (49), 11, 57);
+            Add_Action (Table.States (49), 12, Reduce, (34, 2), 2, null, null);
+            Add_Action (Table.States (49), 18, Reduce, (34, 2), 2, null, null);
+            Add_Action (Table.States (49), 19, Reduce, (34, 2), 2, null, null);
+            Add_Error (Table.States (49));
+            Set_Minimal_Action (Table.States (49).Minimal_Complete_Actions, (1 
=> (Reduce, 34, 2)));
+            Add_Action (Table.States (50), (11, 12, 18, 19, 22), (35, 1), 2, 
null, null);
+            Set_Minimal_Action (Table.States (50).Minimal_Complete_Actions, (1 
=> (Reduce, 35, 2)));
+            Add_Action (Table.States (51), (18, 22, 25), (27, 4), 5, 
declaration_4'Access, null);
+            Set_Minimal_Action (Table.States (51).Minimal_Complete_Actions, (1 
=> (Reduce, 27, 5)));
+            Add_Action (Table.States (52), (1 =>  22), (28, 1), 4, null, null);
+            Set_Minimal_Action (Table.States (52).Minimal_Complete_Actions, (1 
=> (Reduce, 28, 4)));
+            Add_Action (Table.States (53), (1 =>  22), (28, 2), 4, null, null);
+            Set_Minimal_Action (Table.States (53).Minimal_Complete_Actions, (1 
=> (Reduce, 28, 4)));
+            Add_Action (Table.States (54), (12, 18, 19), (33, 1), 3, null, 
null);
+            Set_Minimal_Action (Table.States (54).Minimal_Complete_Actions, (1 
=> (Reduce, 33, 3)));
+            Add_Action (Table.States (55), 5, 58);
+            Add_Error (Table.States (55));
+            Set_Minimal_Action (Table.States (55).Minimal_Complete_Actions, (1 
=> (Shift, 5, 58)));
+            Add_Action (Table.States (56), 22, 59);
+            Add_Error (Table.States (56));
+            Set_Minimal_Action (Table.States (56).Minimal_Complete_Actions, (1 
=> (Shift, 22, 59)));
+            Add_Action (Table.States (57), (12, 18, 19), (34, 3), 3, null, 
null);
+            Set_Minimal_Action (Table.States (57).Minimal_Complete_Actions, (1 
=> (Reduce, 34, 3)));
+            Add_Action (Table.States (58), (12, 18, 19), (33, 3), 4, null, 
null);
+            Set_Minimal_Action (Table.States (58).Minimal_Complete_Actions, (1 
=> (Reduce, 33, 4)));
+            Add_Action (Table.States (59), 15, 60);
+            Add_Error (Table.States (59));
+            Set_Minimal_Action (Table.States (59).Minimal_Complete_Actions, (1 
=> (Shift, 15, 60)));
+            Add_Action (Table.States (60), 22, 61);
+            Add_Error (Table.States (60));
+            Set_Minimal_Action (Table.States (60).Minimal_Complete_Actions, (1 
=> (Shift, 22, 61)));
+            Add_Action (Table.States (61), (12, 18, 19), (33, 2), 6, null, 
null);
+            Set_Minimal_Action (Table.States (61).Minimal_Complete_Actions, (1 
=> (Reduce, 33, 6)));
+         end Subr_1;
+      begin
+         Subr_1;
+      end;
+
+      WisiToken.Parse.LR.Parser_No_Recover.New_Parser
+        (Parser,
+         Trace,
+         Lexer.New_Lexer (Trace),
+         Table,
+         User_Data,
+         Max_Parallel         => 15,
+         Terminate_Same_State => True);
+   end Create_Parser;
+end Wisitoken_Grammar_Main;
diff --git a/wisitoken_grammar_main.ads b/wisitoken_grammar_main.ads
new file mode 100644
index 0000000..7f28437
--- /dev/null
+++ b/wisitoken_grammar_main.ads
@@ -0,0 +1,33 @@
+--  generated parser support file.
+--  command line: wisitoken-bnf-generate.exe  --generate LALR Ada re2c PROCESS 
wisitoken_grammar.wy
+--
+
+--  Copyright (C) 2017, 2018 Free Software Foundation, Inc.
+--
+--  Author: Stephen Leake <stephe-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/>.
+
+with WisiToken.Syntax_Trees;
+with WisiToken.Parse.LR.Parser_No_Recover;
+package Wisitoken_Grammar_Main is
+
+   procedure Create_Parser
+     (Parser                       :    out 
WisiToken.Parse.LR.Parser_No_Recover.Parser;
+      Trace                        : not null access WisiToken.Trace'Class;
+      User_Data                    : in     
WisiToken.Syntax_Trees.User_Data_Access);
+
+end Wisitoken_Grammar_Main;
diff --git a/wisitoken_grammar_re2c.c b/wisitoken_grammar_re2c.c
new file mode 100644
index 0000000..b45ab3a
--- /dev/null
+++ b/wisitoken_grammar_re2c.c
@@ -0,0 +1,3025 @@
+/* Generated by re2c 1.0.3 */
+#line 1 "../wisitoken_grammar.re2c"
+//  generated parser support file. -*- mode: C -*-
+//  command line: wisitoken-bnf-generate.exe  --generate LALR Ada re2c 
wisitoken_grammar.wy
+//
+
+//  Copyright (C) 2017, 2018 Free Software Foundation, Inc.
+//
+//  Author: Stephen Leake <stephe-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/>.
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+typedef struct wisi_lexer
+{
+   unsigned char* buffer;           // input text, in utf-8 encoding
+   unsigned char* buffer_last;      // last byte in buffer
+   unsigned char* cursor;           // current byte
+   unsigned char* byte_token_start; // byte position at start of current token
+   size_t         char_pos;         // character position of current character
+   size_t         char_token_start; // character position at start of current 
token
+   int            line;             // 1 indexed
+   int            line_token_start; // line at start of current token
+   unsigned char* marker;           // saved cursor
+   size_t         marker_pos;       // saved character position
+   size_t         marker_line;      // saved line 
+   unsigned char* context;          // saved cursor
+   size_t         context_pos;      // saved character position
+   int            context_line;     // saved line
+   int            verbosity;
+
+} wisi_lexer;
+
+#define YYCTYPE unsigned char
+
+#define NO_ERROR 0
+#define ERROR_unrecognized_character 1
+wisi_lexer* wisitoken_grammar_new_lexer
+   (unsigned char* input, size_t length, int verbosity)
+{
+   wisi_lexer* result  = malloc (sizeof (wisi_lexer));
+   result->buffer      = input;
+   result->buffer_last = input + length - 1;
+   result->cursor      = input;
+   result->char_pos    = 1;
+   result->line        = (*result->cursor == 0x0A) ? 2 : 1;
+   result->verbosity   = verbosity;
+   return result;
+}
+
+void
+wisitoken_grammar_free_lexer(wisi_lexer** lexer)
+{
+   free(*lexer);
+   *lexer = 0;
+}
+
+void
+wisitoken_grammar_reset_lexer(wisi_lexer* lexer)
+{
+   lexer->cursor   = lexer->buffer;
+   lexer->char_pos = 1;
+   lexer->line     = (*lexer->cursor == 0x0A) ? 2 : 1;
+}
+
+static void debug(wisi_lexer* lexer, int state, unsigned char ch)
+{
+   if (lexer->verbosity > 0)
+      {
+      if (ch < ' ')
+         printf ("lexer: %d, 0x%x\n", state, ch);
+      else
+         printf ("lexer: %d, '%c' 0x%x\n", state, ch, ch);
+      }
+}
+#define YYDEBUG(state, ch) debug(lexer, state, ch)
+#define YYCURSOR lexer->cursor
+
+#define YYPEEK() (lexer->cursor <= lexer->buffer_last) ? *lexer->cursor : 4
+
+#define DO_COUNT ((*lexer->cursor & 0xC0) != 0xC0) && (*lexer->cursor != 0x0D)
+
+static void skip(wisi_lexer* lexer)
+{
+   if (lexer->cursor <= lexer->buffer_last) ++lexer->cursor;
+   if (lexer->cursor <= lexer->buffer_last)
+      if (DO_COUNT) ++lexer->char_pos;
+   if (*lexer->cursor == 0x0A) ++lexer->line;
+}
+#define YYSKIP() skip(lexer)
+#define YYBACKUP() lexer->marker = lexer->cursor; lexer->marker_pos = 
lexer->char_pos;lexer->marker_line = lexer->line
+#define YYRESTORE() lexer->cursor = lexer->marker; lexer->char_pos = 
lexer->marker_pos;lexer->line = lexer->marker_line
+#define YYBACKUPCTX() lexer->context = lexer->cursor; lexer->context_pos = 
lexer->char_pos;lexer->context_line = lexer->line
+#define YYRESTORECTX() lexer->cursor = lexer->context; lexer->char_pos = 
lexer->context_pos;lexer->line = lexer->context_line
+
+static void skip_to(wisi_lexer* lexer, char* target)
+{
+  int i;
+
+  while (lexer->cursor <= lexer->buffer_last)
+    {
+      if (*lexer->cursor == target[0])
+      {
+        i = 0;
+        do
+          i++;
+        while (0 != target[i] &&
+               lexer->cursor + i <= lexer->buffer_last &&
+               *(lexer->cursor + i) == target[i]);
+
+        if (0 == target[i])
+          {
+            for (i = 0; 0 != target[i]; i++)
+               skip(lexer);
+            break;
+          }
+      }
+      skip(lexer);
+    };
+}
+
+int wisitoken_grammar_next_token
+  (wisi_lexer* lexer,
+   int* id,
+   size_t* byte_position,
+   size_t* byte_length,
+   size_t* char_position,
+   size_t* char_length,
+   int*    line_start)
+{
+   int status = NO_ERROR;
+   *id = -1;
+   if (lexer->cursor > lexer->buffer_last)
+   {
+      *id            = 25;
+      *byte_position = lexer->buffer_last - lexer->buffer + 1;
+      *byte_length   = 0;
+      *char_position = lexer->char_token_start;
+      *char_length   = 0;
+      *line_start    = lexer->line;
+      return status;
+   }
+
+   lexer->byte_token_start = lexer->cursor;
+   if (DO_COUNT)
+      lexer->char_token_start = lexer->char_pos;
+   else
+      lexer->char_token_start = lexer->char_pos + 1;
+   if (*lexer->cursor == 0x0A)
+      lexer->line_token_start = lexer->line-1;
+   else
+      lexer->line_token_start = lexer->line;
+
+   while (*id == -1 && status == 0)
+   {
+
+#line 174 "../wisitoken_grammar_re2c.c"
+{
+       YYCTYPE yych;
+       unsigned int yyaccept = 0;
+       YYDEBUG(0, *YYCURSOR);
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x04:      goto yy4;
+       case '\t':
+       case ' ':       goto yy6;
+       case '\n':      goto yy8;
+       case '\r':      goto yy10;
+       case '"':       goto yy11;
+       case '%':       goto yy12;
+       case '\'':      goto yy14;
+       case ',':       goto yy15;
+       case '/':       goto yy17;
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':       goto yy19;
+       case ':':       goto yy22;
+       case ';':       goto yy24;
+       case '<':       goto yy26;
+       case '=':       goto yy28;
+       case '>':       goto yy30;
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case 'a':
+       case 'b':
+       case 'd':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'j':
+       case 'l':
+       case 'm':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       case 'c':       goto yy35;
+       case 'e':       goto yy36;
+       case 'i':       goto yy37;
+       case 'k':       goto yy38;
+       case 'n':       goto yy39;
+       case 't':       goto yy40;
+       case '|':       goto yy41;
+       default:        goto yy2;
+       }
+yy2:
+       YYDEBUG(2, YYPEEK ());
+       YYSKIP ();
+yy3:
+       YYDEBUG(3, YYPEEK ());
+#line 230 "../wisitoken_grammar.re2c"
+       {status = ERROR_unrecognized_character; continue;}
+#line 268 "../wisitoken_grammar_re2c.c"
+yy4:
+       YYDEBUG(4, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(5, YYPEEK ());
+#line 228 "../wisitoken_grammar.re2c"
+       {*id =  25; continue;}
+#line 275 "../wisitoken_grammar_re2c.c"
+yy6:
+       YYDEBUG(6, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(7, YYPEEK ());
+#line 197 "../wisitoken_grammar.re2c"
+       { lexer->byte_token_start = lexer->cursor;
+          lexer->char_token_start = lexer->char_pos;
+          if (*lexer->cursor == 0x0A)
+             lexer->line_token_start = lexer->line-1;
+          else
+             lexer->line_token_start = lexer->line;
+          continue; }
+#line 288 "../wisitoken_grammar_re2c.c"
+yy8:
+       YYDEBUG(8, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(9, YYPEEK ());
+#line 204 "../wisitoken_grammar.re2c"
+       {*id =  1; continue;}
+#line 295 "../wisitoken_grammar_re2c.c"
+yy10:
+       YYDEBUG(10, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '\n':      goto yy8;
+       default:        goto yy3;
+       }
+yy11:
+       YYDEBUG(11, YYPEEK ());
+       yyaccept = 0;
+       YYSKIP ();
+       YYBACKUP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case ' ':
+       case '!':
+       case '"':
+       case '#':
+       case '$':
+       case '%':
+       case '&':
+       case '\'':
+       case '(':
+       case ')':
+       case '*':
+       case '+':
+       case ',':
+       case '-':
+       case '.':
+       case '/':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case ':':
+       case ';':
+       case '<':
+       case '=':
+       case '>':
+       case '?':
+       case '@':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '[':
+       case '\\':
+       case ']':
+       case '^':
+       case '_':
+       case '`':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':
+       case '{':
+       case '|':
+       case '}':
+       case '~':
+       case 0x7F:
+       case 0xC2:
+       case 0xC3:
+       case 0xC4:
+       case 0xC5:
+       case 0xC6:
+       case 0xC7:
+       case 0xC8:
+       case 0xC9:
+       case 0xCA:
+       case 0xCB:
+       case 0xCC:
+       case 0xCD:
+       case 0xCE:
+       case 0xCF:
+       case 0xD0:
+       case 0xD1:
+       case 0xD2:
+       case 0xD3:
+       case 0xD4:
+       case 0xD5:
+       case 0xD6:
+       case 0xD7:
+       case 0xD8:
+       case 0xD9:
+       case 0xDA:
+       case 0xDB:
+       case 0xDC:
+       case 0xDD:
+       case 0xDE:
+       case 0xDF:
+       case 0xE0:
+       case 0xE1:
+       case 0xE2:
+       case 0xE3:
+       case 0xE4:
+       case 0xE5:
+       case 0xE6:
+       case 0xE7:
+       case 0xE8:
+       case 0xE9:
+       case 0xEA:
+       case 0xEB:
+       case 0xEC:
+       case 0xED:
+       case 0xEE:
+       case 0xEF:
+       case 0xF0:
+       case 0xF1:
+       case 0xF2:
+       case 0xF3:
+       case 0xF4:      goto yy44;
+       default:        goto yy3;
+       }
+yy12:
+       YYDEBUG(12, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '(':       goto yy54;
+       case '[':       goto yy56;
+       case '{':       goto yy58;
+       default:        goto yy13;
+       }
+yy13:
+       YYDEBUG(13, YYPEEK ());
+#line 221 "../wisitoken_grammar.re2c"
+       {*id =  18; continue;}
+#line 474 "../wisitoken_grammar_re2c.c"
+yy14:
+       YYDEBUG(14, YYPEEK ());
+       yyaccept = 0;
+       YYSKIP ();
+       YYBACKUP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case ' ':
+       case '!':
+       case '"':
+       case '#':
+       case '$':
+       case '%':
+       case '&':
+       case '\'':
+       case '(':
+       case ')':
+       case '*':
+       case '+':
+       case ',':
+       case '-':
+       case '.':
+       case '/':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case ':':
+       case ';':
+       case '<':
+       case '=':
+       case '>':
+       case '?':
+       case '@':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '[':
+       case '\\':
+       case ']':
+       case '^':
+       case '_':
+       case '`':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':
+       case '{':
+       case '|':
+       case '}':
+       case '~':
+       case 0x7F:
+       case 0xC2:
+       case 0xC3:
+       case 0xC4:
+       case 0xC5:
+       case 0xC6:
+       case 0xC7:
+       case 0xC8:
+       case 0xC9:
+       case 0xCA:
+       case 0xCB:
+       case 0xCC:
+       case 0xCD:
+       case 0xCE:
+       case 0xCF:
+       case 0xD0:
+       case 0xD1:
+       case 0xD2:
+       case 0xD3:
+       case 0xD4:
+       case 0xD5:
+       case 0xD6:
+       case 0xD7:
+       case 0xD8:
+       case 0xD9:
+       case 0xDA:
+       case 0xDB:
+       case 0xDC:
+       case 0xDD:
+       case 0xDE:
+       case 0xDF:
+       case 0xE0:
+       case 0xE1:
+       case 0xE2:
+       case 0xE3:
+       case 0xE4:
+       case 0xE5:
+       case 0xE6:
+       case 0xE7:
+       case 0xE8:
+       case 0xE9:
+       case 0xEA:
+       case 0xEB:
+       case 0xEC:
+       case 0xED:
+       case 0xEE:
+       case 0xEF:
+       case 0xF0:
+       case 0xF1:
+       case 0xF2:
+       case 0xF3:
+       case 0xF4:      goto yy61;
+       default:        goto yy3;
+       }
+yy15:
+       YYDEBUG(15, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(16, YYPEEK ());
+#line 217 "../wisitoken_grammar.re2c"
+       {*id =  14; continue;}
+#line 637 "../wisitoken_grammar_re2c.c"
+yy17:
+       YYDEBUG(17, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(18, YYPEEK ());
+#line 223 "../wisitoken_grammar.re2c"
+       {*id =  20; continue;}
+#line 644 "../wisitoken_grammar_re2c.c"
+yy19:
+       YYDEBUG(19, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       YYDEBUG(20, YYPEEK ());
+       switch (yych) {
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case '_':       goto yy19;
+       default:        goto yy21;
+       }
+yy21:
+       YYDEBUG(21, YYPEEK ());
+#line 224 "../wisitoken_grammar.re2c"
+       {*id =  21; continue;}
+#line 668 "../wisitoken_grammar_re2c.c"
+yy22:
+       YYDEBUG(22, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(23, YYPEEK ());
+#line 216 "../wisitoken_grammar.re2c"
+       {*id =  13; continue;}
+#line 675 "../wisitoken_grammar_re2c.c"
+yy24:
+       YYDEBUG(24, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case ';':       goto yy70;
+       default:        goto yy25;
+       }
+yy25:
+       YYDEBUG(25, YYPEEK ());
+#line 222 "../wisitoken_grammar.re2c"
+       {*id =  19; continue;}
+#line 688 "../wisitoken_grammar_re2c.c"
+yy26:
+       YYDEBUG(26, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(27, YYPEEK ());
+#line 220 "../wisitoken_grammar.re2c"
+       {*id =  17; continue;}
+#line 695 "../wisitoken_grammar_re2c.c"
+yy28:
+       YYDEBUG(28, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(29, YYPEEK ());
+#line 218 "../wisitoken_grammar.re2c"
+       {*id =  15; continue;}
+#line 702 "../wisitoken_grammar_re2c.c"
+yy30:
+       YYDEBUG(30, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(31, YYPEEK ());
+#line 219 "../wisitoken_grammar.re2c"
+       {*id =  16; continue;}
+#line 709 "../wisitoken_grammar_re2c.c"
+yy32:
+       YYDEBUG(32, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+yy33:
+       YYDEBUG(33, YYPEEK ());
+       switch (yych) {
+       case '-':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '_':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       default:        goto yy34;
+       }
+yy34:
+       YYDEBUG(34, YYPEEK ());
+#line 225 "../wisitoken_grammar.re2c"
+       {*id =  22; continue;}
+#line 787 "../wisitoken_grammar_re2c.c"
+yy35:
+       YYDEBUG(35, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'o':       goto yy73;
+       default:        goto yy33;
+       }
+yy36:
+       YYDEBUG(36, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'n':       goto yy74;
+       default:        goto yy33;
+       }
+yy37:
+       YYDEBUG(37, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'f':       goto yy75;
+       default:        goto yy33;
+       }
+yy38:
+       YYDEBUG(38, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'e':       goto yy77;
+       default:        goto yy33;
+       }
+yy39:
+       YYDEBUG(39, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'o':       goto yy78;
+       default:        goto yy33;
+       }
+yy40:
+       YYDEBUG(40, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'o':       goto yy79;
+       default:        goto yy33;
+       }
+yy41:
+       YYDEBUG(41, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(42, YYPEEK ());
+#line 215 "../wisitoken_grammar.re2c"
+       {*id =  12; continue;}
+#line 842 "../wisitoken_grammar_re2c.c"
+yy43:
+       YYDEBUG(43, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+yy44:
+       YYDEBUG(44, YYPEEK ());
+       switch (yych) {
+       case ' ':
+       case '!':
+       case '#':
+       case '$':
+       case '%':
+       case '&':
+       case '\'':
+       case '(':
+       case ')':
+       case '*':
+       case '+':
+       case ',':
+       case '-':
+       case '.':
+       case '/':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case ':':
+       case ';':
+       case '<':
+       case '=':
+       case '>':
+       case '?':
+       case '@':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '[':
+       case '\\':
+       case ']':
+       case '^':
+       case '_':
+       case '`':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':
+       case '{':
+       case '|':
+       case '}':
+       case '~':
+       case 0x7F:      goto yy43;
+       case '"':       goto yy46;
+       case 0xC2:
+       case 0xC3:
+       case 0xC4:
+       case 0xC5:
+       case 0xC6:
+       case 0xC7:
+       case 0xC8:
+       case 0xC9:
+       case 0xCA:
+       case 0xCB:
+       case 0xCC:
+       case 0xCD:
+       case 0xCE:
+       case 0xCF:
+       case 0xD0:
+       case 0xD1:
+       case 0xD2:
+       case 0xD3:
+       case 0xD4:
+       case 0xD5:
+       case 0xD6:
+       case 0xD7:
+       case 0xD8:
+       case 0xD9:
+       case 0xDA:
+       case 0xDB:
+       case 0xDC:
+       case 0xDD:
+       case 0xDE:
+       case 0xDF:      goto yy48;
+       case 0xE0:      goto yy49;
+       case 0xE1:
+       case 0xE2:
+       case 0xE3:
+       case 0xE4:
+       case 0xE5:
+       case 0xE6:
+       case 0xE7:
+       case 0xE8:
+       case 0xE9:
+       case 0xEA:
+       case 0xEB:
+       case 0xEC:
+       case 0xED:
+       case 0xEE:
+       case 0xEF:      goto yy50;
+       case 0xF0:      goto yy51;
+       case 0xF1:
+       case 0xF2:
+       case 0xF3:      goto yy52;
+       case 0xF4:      goto yy53;
+       default:        goto yy45;
+       }
+yy45:
+       YYDEBUG(45, YYPEEK ());
+       YYRESTORE ();
+       switch (yyaccept) {
+       case 0:         goto yy3;
+       case 1:         goto yy47;
+       case 2:         goto yy63;
+       default:        goto yy72;
+       }
+yy46:
+       YYDEBUG(46, YYPEEK ());
+       yyaccept = 1;
+       YYSKIP ();
+       YYBACKUP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '"':       goto yy43;
+       default:        goto yy47;
+       }
+yy47:
+       YYDEBUG(47, YYPEEK ());
+#line 226 "../wisitoken_grammar.re2c"
+       {*id =  23; continue;}
+#line 1022 "../wisitoken_grammar_re2c.c"
+yy48:
+       YYDEBUG(48, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy43;
+       default:        goto yy45;
+       }
+yy49:
+       YYDEBUG(49, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy48;
+       default:        goto yy45;
+       }
+yy50:
+       YYDEBUG(50, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy48;
+       default:        goto yy45;
+       }
+yy51:
+       YYDEBUG(51, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy50;
+       default:        goto yy45;
+       }
+yy52:
+       YYDEBUG(52, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy50;
+       default:        goto yy45;
+       }
+yy53:
+       YYDEBUG(53, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:      goto yy50;
+       default:        goto yy45;
+       }
+yy54:
+       YYDEBUG(54, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(55, YYPEEK ());
+#line 214 "../wisitoken_grammar.re2c"
+       {*id =  11; skip_to(lexer, ")%"); continue;}
+#line 1359 "../wisitoken_grammar_re2c.c"
+yy56:
+       YYDEBUG(56, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(57, YYPEEK ());
+#line 213 "../wisitoken_grammar.re2c"
+       {*id =  10; skip_to(lexer, "]%"); continue;}
+#line 1366 "../wisitoken_grammar_re2c.c"
+yy58:
+       YYDEBUG(58, YYPEEK ());
+       YYSKIP ();
+       YYDEBUG(59, YYPEEK ());
+#line 212 "../wisitoken_grammar.re2c"
+       {*id =  9; skip_to(lexer, "}%"); continue;}
+#line 1373 "../wisitoken_grammar_re2c.c"
+yy60:
+       YYDEBUG(60, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+yy61:
+       YYDEBUG(61, YYPEEK ());
+       switch (yych) {
+       case ' ':
+       case '!':
+       case '"':
+       case '#':
+       case '$':
+       case '%':
+       case '&':
+       case '(':
+       case ')':
+       case '*':
+       case '+':
+       case ',':
+       case '-':
+       case '.':
+       case '/':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case ':':
+       case ';':
+       case '<':
+       case '=':
+       case '>':
+       case '?':
+       case '@':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '[':
+       case '\\':
+       case ']':
+       case '^':
+       case '_':
+       case '`':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':
+       case '{':
+       case '|':
+       case '}':
+       case '~':
+       case 0x7F:      goto yy60;
+       case '\'':      goto yy62;
+       case 0xC2:
+       case 0xC3:
+       case 0xC4:
+       case 0xC5:
+       case 0xC6:
+       case 0xC7:
+       case 0xC8:
+       case 0xC9:
+       case 0xCA:
+       case 0xCB:
+       case 0xCC:
+       case 0xCD:
+       case 0xCE:
+       case 0xCF:
+       case 0xD0:
+       case 0xD1:
+       case 0xD2:
+       case 0xD3:
+       case 0xD4:
+       case 0xD5:
+       case 0xD6:
+       case 0xD7:
+       case 0xD8:
+       case 0xD9:
+       case 0xDA:
+       case 0xDB:
+       case 0xDC:
+       case 0xDD:
+       case 0xDE:
+       case 0xDF:      goto yy64;
+       case 0xE0:      goto yy65;
+       case 0xE1:
+       case 0xE2:
+       case 0xE3:
+       case 0xE4:
+       case 0xE5:
+       case 0xE6:
+       case 0xE7:
+       case 0xE8:
+       case 0xE9:
+       case 0xEA:
+       case 0xEB:
+       case 0xEC:
+       case 0xED:
+       case 0xEE:
+       case 0xEF:      goto yy66;
+       case 0xF0:      goto yy67;
+       case 0xF1:
+       case 0xF2:
+       case 0xF3:      goto yy68;
+       case 0xF4:      goto yy69;
+       default:        goto yy45;
+       }
+yy62:
+       YYDEBUG(62, YYPEEK ());
+       yyaccept = 2;
+       YYSKIP ();
+       YYBACKUP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '\'':      goto yy60;
+       default:        goto yy63;
+       }
+yy63:
+       YYDEBUG(63, YYPEEK ());
+#line 227 "../wisitoken_grammar.re2c"
+       {*id =  24; continue;}
+#line 1544 "../wisitoken_grammar_re2c.c"
+yy64:
+       YYDEBUG(64, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy60;
+       default:        goto yy45;
+       }
+yy65:
+       YYDEBUG(65, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy64;
+       default:        goto yy45;
+       }
+yy66:
+       YYDEBUG(66, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy64;
+       default:        goto yy45;
+       }
+yy67:
+       YYDEBUG(67, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy66;
+       default:        goto yy45;
+       }
+yy68:
+       YYDEBUG(68, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy66;
+       default:        goto yy45;
+       }
+yy69:
+       YYDEBUG(69, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:      goto yy66;
+       default:        goto yy45;
+       }
+yy70:
+       YYDEBUG(70, YYPEEK ());
+       yyaccept = 3;
+       YYSKIP ();
+       YYBACKUP ();
+       yych = YYPEEK ();
+       YYDEBUG(71, YYPEEK ());
+       switch (yych) {
+       case 0x00:
+       case 0x01:
+       case 0x02:
+       case 0x03:
+       case 0x05:
+       case 0x06:
+       case 0x07:
+       case 0x08:
+       case '\t':
+       case '\v':
+       case '\f':
+       case '\r':
+       case 0x0E:
+       case 0x0F:
+       case 0x10:
+       case 0x11:
+       case 0x12:
+       case 0x13:
+       case 0x14:
+       case 0x15:
+       case 0x16:
+       case 0x17:
+       case 0x18:
+       case 0x19:
+       case 0x1A:
+       case 0x1B:
+       case 0x1C:
+       case 0x1D:
+       case 0x1E:
+       case 0x1F:
+       case ' ':
+       case '!':
+       case '"':
+       case '#':
+       case '$':
+       case '%':
+       case '&':
+       case '\'':
+       case '(':
+       case ')':
+       case '*':
+       case '+':
+       case ',':
+       case '-':
+       case '.':
+       case '/':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case ':':
+       case ';':
+       case '<':
+       case '=':
+       case '>':
+       case '?':
+       case '@':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '[':
+       case '\\':
+       case ']':
+       case '^':
+       case '_':
+       case '`':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':
+       case '{':
+       case '|':
+       case '}':
+       case '~':
+       case 0x7F:      goto yy70;
+       case 0xC2:
+       case 0xC3:
+       case 0xC4:
+       case 0xC5:
+       case 0xC6:
+       case 0xC7:
+       case 0xC8:
+       case 0xC9:
+       case 0xCA:
+       case 0xCB:
+       case 0xCC:
+       case 0xCD:
+       case 0xCE:
+       case 0xCF:
+       case 0xD0:
+       case 0xD1:
+       case 0xD2:
+       case 0xD3:
+       case 0xD4:
+       case 0xD5:
+       case 0xD6:
+       case 0xD7:
+       case 0xD8:
+       case 0xD9:
+       case 0xDA:
+       case 0xDB:
+       case 0xDC:
+       case 0xDD:
+       case 0xDE:
+       case 0xDF:      goto yy80;
+       case 0xE0:      goto yy81;
+       case 0xE1:
+       case 0xE2:
+       case 0xE3:
+       case 0xE4:
+       case 0xE5:
+       case 0xE6:
+       case 0xE7:
+       case 0xE8:
+       case 0xE9:
+       case 0xEA:
+       case 0xEB:
+       case 0xEC:
+       case 0xED:
+       case 0xEE:
+       case 0xEF:      goto yy82;
+       case 0xF0:      goto yy83;
+       case 0xF1:
+       case 0xF2:
+       case 0xF3:      goto yy84;
+       case 0xF4:      goto yy85;
+       default:        goto yy72;
+       }
+yy72:
+       YYDEBUG(72, YYPEEK ());
+#line 205 "../wisitoken_grammar.re2c"
+       {*id =  2; continue;}
+#line 2066 "../wisitoken_grammar_re2c.c"
+yy73:
+       YYDEBUG(73, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'd':       goto yy86;
+       default:        goto yy33;
+       }
+yy74:
+       YYDEBUG(74, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'd':       goto yy87;
+       default:        goto yy33;
+       }
+yy75:
+       YYDEBUG(75, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '-':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '_':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       default:        goto yy76;
+       }
+yy76:
+       YYDEBUG(76, YYPEEK ());
+#line 208 "../wisitoken_grammar.re2c"
+       {*id =  5; continue;}
+#line 2158 "../wisitoken_grammar_re2c.c"
+yy77:
+       YYDEBUG(77, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'y':       goto yy89;
+       default:        goto yy33;
+       }
+yy78:
+       YYDEBUG(78, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'n':       goto yy90;
+       default:        goto yy33;
+       }
+yy79:
+       YYDEBUG(79, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'k':       goto yy91;
+       default:        goto yy33;
+       }
+yy80:
+       YYDEBUG(80, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy70;
+       default:        goto yy45;
+       }
+yy81:
+       YYDEBUG(81, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy80;
+       default:        goto yy45;
+       }
+yy82:
+       YYDEBUG(82, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy80;
+       default:        goto yy45;
+       }
+yy83:
+       YYDEBUG(83, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy82;
+       default:        goto yy45;
+       }
+yy84:
+       YYDEBUG(84, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:
+       case 0x90:
+       case 0x91:
+       case 0x92:
+       case 0x93:
+       case 0x94:
+       case 0x95:
+       case 0x96:
+       case 0x97:
+       case 0x98:
+       case 0x99:
+       case 0x9A:
+       case 0x9B:
+       case 0x9C:
+       case 0x9D:
+       case 0x9E:
+       case 0x9F:
+       case 0xA0:
+       case 0xA1:
+       case 0xA2:
+       case 0xA3:
+       case 0xA4:
+       case 0xA5:
+       case 0xA6:
+       case 0xA7:
+       case 0xA8:
+       case 0xA9:
+       case 0xAA:
+       case 0xAB:
+       case 0xAC:
+       case 0xAD:
+       case 0xAE:
+       case 0xAF:
+       case 0xB0:
+       case 0xB1:
+       case 0xB2:
+       case 0xB3:
+       case 0xB4:
+       case 0xB5:
+       case 0xB6:
+       case 0xB7:
+       case 0xB8:
+       case 0xB9:
+       case 0xBA:
+       case 0xBB:
+       case 0xBC:
+       case 0xBD:
+       case 0xBE:
+       case 0xBF:      goto yy82;
+       default:        goto yy45;
+       }
+yy85:
+       YYDEBUG(85, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 0x80:
+       case 0x81:
+       case 0x82:
+       case 0x83:
+       case 0x84:
+       case 0x85:
+       case 0x86:
+       case 0x87:
+       case 0x88:
+       case 0x89:
+       case 0x8A:
+       case 0x8B:
+       case 0x8C:
+       case 0x8D:
+       case 0x8E:
+       case 0x8F:      goto yy82;
+       default:        goto yy45;
+       }
+yy86:
+       YYDEBUG(86, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'e':       goto yy92;
+       default:        goto yy33;
+       }
+yy87:
+       YYDEBUG(87, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '-':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '_':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       default:        goto yy88;
+       }
+yy88:
+       YYDEBUG(88, YYPEEK ());
+#line 207 "../wisitoken_grammar.re2c"
+       {*id =  4; continue;}
+#line 2596 "../wisitoken_grammar_re2c.c"
+yy89:
+       YYDEBUG(89, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'w':       goto yy94;
+       default:        goto yy33;
+       }
+yy90:
+       YYDEBUG(90, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '_':       goto yy95;
+       default:        goto yy33;
+       }
+yy91:
+       YYDEBUG(91, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'e':       goto yy96;
+       default:        goto yy33;
+       }
+yy92:
+       YYDEBUG(92, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '-':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '_':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       default:        goto yy93;
+       }
+yy93:
+       YYDEBUG(93, YYPEEK ());
+#line 206 "../wisitoken_grammar.re2c"
+       {*id =  3; continue;}
+#line 2696 "../wisitoken_grammar_re2c.c"
+yy94:
+       YYDEBUG(94, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'o':       goto yy97;
+       default:        goto yy33;
+       }
+yy95:
+       YYDEBUG(95, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'g':       goto yy98;
+       default:        goto yy33;
+       }
+yy96:
+       YYDEBUG(96, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'n':       goto yy99;
+       default:        goto yy33;
+       }
+yy97:
+       YYDEBUG(97, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'r':       goto yy101;
+       default:        goto yy33;
+       }
+yy98:
+       YYDEBUG(98, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'r':       goto yy102;
+       default:        goto yy33;
+       }
+yy99:
+       YYDEBUG(99, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '-':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '_':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       default:        goto yy100;
+       }
+yy100:
+       YYDEBUG(100, YYPEEK ());
+#line 211 "../wisitoken_grammar.re2c"
+       {*id =  8; continue;}
+#line 2812 "../wisitoken_grammar_re2c.c"
+yy101:
+       YYDEBUG(101, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'd':       goto yy103;
+       default:        goto yy33;
+       }
+yy102:
+       YYDEBUG(102, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'a':       goto yy105;
+       default:        goto yy33;
+       }
+yy103:
+       YYDEBUG(103, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '-':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '_':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       default:        goto yy104;
+       }
+yy104:
+       YYDEBUG(104, YYPEEK ());
+#line 209 "../wisitoken_grammar.re2c"
+       {*id =  6; continue;}
+#line 2904 "../wisitoken_grammar_re2c.c"
+yy105:
+       YYDEBUG(105, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'm':       goto yy106;
+       default:        goto yy33;
+       }
+yy106:
+       YYDEBUG(106, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'm':       goto yy107;
+       default:        goto yy33;
+       }
+yy107:
+       YYDEBUG(107, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'a':       goto yy108;
+       default:        goto yy33;
+       }
+yy108:
+       YYDEBUG(108, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case 'r':       goto yy109;
+       default:        goto yy33;
+       }
+yy109:
+       YYDEBUG(109, YYPEEK ());
+       YYSKIP ();
+       yych = YYPEEK ();
+       switch (yych) {
+       case '-':
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+       case 'A':
+       case 'B':
+       case 'C':
+       case 'D':
+       case 'E':
+       case 'F':
+       case 'G':
+       case 'H':
+       case 'I':
+       case 'J':
+       case 'K':
+       case 'L':
+       case 'M':
+       case 'N':
+       case 'O':
+       case 'P':
+       case 'Q':
+       case 'R':
+       case 'S':
+       case 'T':
+       case 'U':
+       case 'V':
+       case 'W':
+       case 'X':
+       case 'Y':
+       case 'Z':
+       case '_':
+       case 'a':
+       case 'b':
+       case 'c':
+       case 'd':
+       case 'e':
+       case 'f':
+       case 'g':
+       case 'h':
+       case 'i':
+       case 'j':
+       case 'k':
+       case 'l':
+       case 'm':
+       case 'n':
+       case 'o':
+       case 'p':
+       case 'q':
+       case 'r':
+       case 's':
+       case 't':
+       case 'u':
+       case 'v':
+       case 'w':
+       case 'x':
+       case 'y':
+       case 'z':       goto yy32;
+       default:        goto yy110;
+       }
+yy110:
+       YYDEBUG(110, YYPEEK ());
+#line 210 "../wisitoken_grammar.re2c"
+       {*id =  7; continue;}
+#line 3012 "../wisitoken_grammar_re2c.c"
+}
+#line 231 "../wisitoken_grammar.re2c"
+
+      }
+   *byte_position = lexer->byte_token_start - lexer->buffer + 1;
+   *byte_length   = lexer->cursor - lexer->byte_token_start;
+   *char_position = lexer->char_token_start;
+   if (DO_COUNT)
+      *char_length = lexer->char_pos - lexer->char_token_start;
+   else
+      *char_length = lexer->char_pos - lexer->char_token_start + 1;
+   *line_start     = lexer->line_token_start;
+   return status;
+   }
diff --git a/wisitoken_grammar_re2c_c.ads b/wisitoken_grammar_re2c_c.ads
new file mode 100644
index 0000000..0f96b59
--- /dev/null
+++ b/wisitoken_grammar_re2c_c.ads
@@ -0,0 +1,63 @@
+--  generated parser support file.
+--  command line: wisitoken-bnf-generate.exe  --generate LALR Ada re2c 
wisitoken_grammar.wy
+--
+
+--  Copyright (C) 2017, 2018 Free Software Foundation, Inc.
+--
+--  Author: Stephen Leake <stephe-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/>.
+
+with Interfaces.C;
+with WisiToken;
+with System;
+package wisitoken_grammar_re2c_c is
+
+   function New_Lexer
+     (Buffer    : in System.Address;
+      Length    : in Interfaces.C.size_t;
+      Verbosity : in Interfaces.C.int)
+     return System.Address
+   with Import        => True,
+        Convention    => C,
+        External_Name => "wisitoken_grammar_new_lexer";
+   --  Create the lexer object, passing it the full text to process.
+
+   procedure Free_Lexer (Lexer : in out System.Address)
+   with Import        => True,
+        Convention    => C,
+        External_Name => "wisitoken_grammar_free_lexer";
+   --  Free the lexer object
+
+   procedure Reset_Lexer (Lexer : in System.Address)
+   with Import        => True,
+        Convention    => C,
+        External_Name => "wisitoken_grammar_reset_lexer";
+
+   function Next_Token
+     (Lexer         : in     System.Address;
+      ID            :    out WisiToken.Token_ID;
+      Byte_Position :    out Interfaces.C.size_t;
+      Byte_Length   :    out Interfaces.C.size_t;
+      Char_Position :    out Interfaces.C.size_t;
+      Char_Length   :    out Interfaces.C.size_t;
+      Line_Start    :    out Interfaces.C.int)
+     return Interfaces.C.int
+   with Import        => True,
+        Convention    => C,
+        External_Name => "wisitoken_grammar_next_token";
+
+end wisitoken_grammar_re2c_c;
diff --git a/wisitoken_grammar_runtime.adb b/wisitoken_grammar_runtime.adb
new file mode 100644
index 0000000..905b7a5
--- /dev/null
+++ b/wisitoken_grammar_runtime.adb
@@ -0,0 +1,610 @@
+--  Abstract :
+--
+--  See spec.
+--
+--  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);
+
+with Ada.Strings.Unbounded;
+with SAL;
+with WisiToken.Generate;   use WisiToken.Generate;
+with Wisitoken_Grammar_Actions; use Wisitoken_Grammar_Actions;
+package body WisiToken_Grammar_Runtime is
+
+   use WisiToken;
+
+   function Get_Text
+     (Data         : in User_Data_Type;
+      Tree         : in Syntax_Trees.Tree;
+      Tree_Index   : in Syntax_Trees.Valid_Node_Index;
+      Strip_Quotes : in Boolean := False)
+     return String
+   is
+      use all type Syntax_Trees.Node_Label;
+
+      function Strip_Delimiters (Tree_Index : in 
Syntax_Trees.Valid_Node_Index) return String
+      is
+         Region : Buffer_Region renames Data.Terminals.all (Tree.Terminal 
(Tree_Index)).Byte_Region;
+      begin
+         if -Tree.ID (Tree_Index) in RAW_CODE_ID | REGEXP_ID | ACTION_ID then
+            --  strip delimiters.
+            return Data.Grammar_Lexer.Buffer_Text ((Region.First + 2, 
Region.Last - 2));
+
+         elsif -Tree.ID (Tree_Index) in STRING_LITERAL_ID | 
STRING_LITERAL_CASE_INS_ID and Strip_Quotes then
+            return Data.Grammar_Lexer.Buffer_Text ((Region.First + 1, 
Region.Last - 1));
+         else
+            return Data.Grammar_Lexer.Buffer_Text (Region);
+         end if;
+      end Strip_Delimiters;
+
+   begin
+      case Tree.Label (Tree_Index) is
+      when Shared_Terminal =>
+         return Strip_Delimiters (Tree_Index);
+
+      when Virtual_Terminal =>
+         raise SAL.Programmer_Error;
+
+      when Nonterm =>
+         declare
+            use all type Ada.Strings.Unbounded.Unbounded_String;
+            Result       : Ada.Strings.Unbounded.Unbounded_String;
+            Tree_Indices : constant Syntax_Trees.Valid_Node_Index_Array := 
Tree.Get_Terminals (Tree_Index);
+            Need_Space   : Boolean                                      := 
False;
+         begin
+            for Tree_Index of Tree_Indices loop
+               Result := Result & (if Need_Space then " " else "") & 
Strip_Delimiters (Tree_Index);
+               Need_Space := True;
+            end loop;
+            return -Result;
+         end;
+      end case;
+   end Get_Text;
+
+   function Get_Child_Text
+     (Data         : in User_Data_Type;
+      Tree         : in Syntax_Trees.Tree;
+      Parent       : in Syntax_Trees.Valid_Node_Index;
+      Child        : in SAL.Peek_Type;
+      Strip_Quotes : in Boolean := False)
+     return String
+   is
+      Tree_Indices : constant Syntax_Trees.Valid_Node_Index_Array := 
Tree.Get_Terminals (Parent);
+   begin
+      return Get_Text (Data, Tree, Tree_Indices (Child), Strip_Quotes);
+   end Get_Child_Text;
+
+   procedure Start_If_1
+     (Data    : in out User_Data_Type;
+      Tree    : in     Syntax_Trees.Tree;
+      A_Index : in     Syntax_Trees.Valid_Node_Index;
+      B_Index : in     Syntax_Trees.Valid_Node_Index)
+   is
+      use all type WisiToken.BNF.Generate_Algorithm;
+      use all type WisiToken.BNF.Lexer_Type;
+   begin
+      if "lexer" = Get_Text (Data, Tree, A_Index) then
+         Data.If_Lexer_Present := True;
+         Data.Ignore_Lines     := Data.User_Lexer /= WisiToken.BNF.To_Lexer 
(Get_Text (Data, Tree, B_Index));
+
+      elsif "parser" = Get_Text (Data, Tree, A_Index) then
+         Data.If_Parser_Present := True;
+         Data.Ignore_Lines := Data.User_Parser /= 
WisiToken.BNF.Generate_Algorithm'Value
+           (Get_Text (Data, Tree, B_Index));
+
+      else
+         raise Grammar_Error with
+           Error_Message
+             (Data.Grammar_Lexer.File_Name, Data.Terminals.all 
(Tree.Min_Terminal_Index (A_Index)).Line,
+              "invalid '%if'; must be one of {lexer | parser}");
+      end if;
+   end Start_If_1;
+
+   function Get_RHS
+     (Data  : in out User_Data_Type;
+      Tree  : in     Syntax_Trees.Tree;
+      Token : in     Syntax_Trees.Valid_Node_Index)
+     return WisiToken.BNF.RHS_Type
+   is
+      use all type SAL.Base_Peek_Type;
+      Tokens : constant Syntax_Trees.Valid_Node_Index_Array := Tree.Children 
(Token);
+   begin
+      pragma Assert (-Tree.ID (Token) = rhs_ID);
+
+      return RHS : WisiToken.BNF.RHS_Type do
+         if Tokens'Length = 0 then
+            --  Token is an empty rhs; parent is a possibly empty rhs_list; 
grandparent is
+            --  a non-empty rhs_list or nonterminal.
+            RHS.Source_Line := Data.Terminals.all (Tree.Min_Terminal_Index 
(Tree.Parent (Tree.Parent (Token)))).Line;
+
+         else
+            RHS.Source_Line := Data.Terminals.all (Tree.Min_Terminal_Index 
(Token)).Line;
+
+            for I of Tree.Get_Terminals (Tokens (1)) loop
+               RHS.Tokens.Append (Get_Text (Data, Tree, I));
+            end loop;
+
+            if Tokens'Last >= 2 then
+               declare
+                  Text : constant String := Get_Text (Data, Tree, Tokens (2));
+               begin
+                  if Text'Length > 0 then
+                     RHS.Action := +Text;
+                     Data.Action_Count := Data.Action_Count + 1;
+                  end if;
+               end;
+            end if;
+
+            if Tokens'Last >= 3 then
+               RHS.Check := +Get_Text (Data, Tree, Tokens (3));
+               Data.Check_Count := Data.Check_Count + 1;
+            end if;
+         end if;
+      end return;
+   end Get_RHS;
+
+   procedure Get_Right_Hand_Sides
+     (Data             : in out User_Data_Type;
+      Tree             : in     WisiToken.Syntax_Trees.Tree;
+      Right_Hand_Sides : in out WisiToken.BNF.RHS_Lists.List;
+      Token            : in     WisiToken.Syntax_Trees.Valid_Node_Index)
+   is
+      use all type SAL.Base_Peek_Type;
+
+      Tokens : constant Syntax_Trees.Valid_Node_Index_Array := Tree.Children 
(Token);
+   begin
+      pragma Assert (-Tree.ID (Token) = rhs_list_ID);
+
+      if Tokens'Last = 1 then
+         --  | rhs
+         if not Data.Ignore_Lines then
+            Right_Hand_Sides.Append (Get_RHS (Data, Tree, Tokens (1)));
+         end if;
+      else
+         --  | rhs_list BAR rhs
+         --  | rhs_list PERCENT IF IDENTIFIER EQUAL IDENTIFIER
+         --  | rhs_list PERCENT END IF
+         Get_Right_Hand_Sides (Data, Tree, Right_Hand_Sides, Tokens (1));
+
+         case Token_Enum_ID'(-Tree.ID (Tokens (3))) is
+         when rhs_ID =>
+            if not Data.Ignore_Lines then
+               Right_Hand_Sides.Append (Get_RHS (Data, Tree, Tokens (3)));
+            end if;
+
+         when IF_ID =>
+            Start_If_1 (Data, Tree, Tokens (4), Tokens (6));
+
+         when END_ID =>
+            Data.Ignore_Lines := False;
+
+         when others =>
+            raise SAL.Programmer_Error;
+         end case;
+      end if;
+   end Get_Right_Hand_Sides;
+
+   ----------
+   --  Public subprograms, declaration order
+
+   overriding
+   procedure Set_Lexer_Terminals
+     (User_Data : in out User_Data_Type;
+      Lexer     : in     WisiToken.Lexer.Handle;
+      Terminals : in     Base_Token_Array_Access)
+   is begin
+      User_Data.Grammar_Lexer := Lexer;
+      User_Data.Terminals     := Terminals;
+   end Set_Lexer_Terminals;
+
+   overriding procedure Reset (Data : in out User_Data_Type)
+   is begin
+      --  Preserve Grammar_Lexer
+      --  Preserve User_Lexer
+      --  Preserve User_Parser
+      --  Perserve Generate_Set
+      --  Preserve Terminals
+      Data.Raw_Code          := (others => <>);
+      Data.Language_Params   := (others => <>);
+      WisiToken.BNF.Free (Data.Generate_Set);
+      Data.Tokens            := (others => <>);
+      Data.User_Names        := (others => <>);
+      Data.Conflicts.Clear;
+      Data.McKenzie_Recover  := (others => <>);
+      Data.Rule_Count        := 0;
+      Data.Action_Count      := 0;
+      Data.Check_Count       := 0;
+      Data.If_Lexer_Present  := False;
+      Data.If_Parser_Present := False;
+      Data.Ignore_Lines      := False;
+   end Reset;
+
+   procedure Start_If
+     (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+      Tree      : in     WisiToken.Syntax_Trees.Tree;
+      Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is begin
+      Start_If_1 (User_Data_Type (User_Data), Tree, Tokens (3), Tokens (5));
+   end Start_If;
+
+   procedure End_If (User_Data : in out 
WisiToken.Syntax_Trees.User_Data_Type'Class)
+   is
+      Data : User_Data_Type renames User_Data_Type (User_Data);
+   begin
+      Data.Ignore_Lines := False;
+   end End_If;
+
+   procedure Add_Declaration
+     (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+      Tree      : in     WisiToken.Syntax_Trees.Tree;
+      Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      use all type Ada.Strings.Unbounded.Unbounded_String;
+
+      Data : User_Data_Type renames User_Data_Type (User_Data);
+
+      function Token (Index : in SAL.Peek_Type) return Base_Token
+      is
+         use all type WisiToken.Syntax_Trees.Node_Label;
+         use all type SAL.Base_Peek_Type;
+      begin
+         if Tokens'Last < Index then
+            raise SAL.Programmer_Error;
+         elsif Tree.Label (Tokens (Index)) /= 
WisiToken.Syntax_Trees.Shared_Terminal then
+            raise SAL.Programmer_Error;
+         else
+            return Data.Terminals.all (Tree.Terminal (Tokens (Index)));
+         end if;
+      end Token;
+
+      function Enum_ID (Index : in SAL.Peek_Type) return Token_Enum_ID
+        is (Token_Enum_ID'(-Token (Index).ID));
+
+   begin
+      --  Add declaration to User_Data.Generate_Set, Language_Params,
+      --  Tokens, Conflicts, or McKenzie_Recover.
+
+      if Data.Ignore_Lines then
+         return;
+      end if;
+
+      case Tree.Label (Tokens (2)) is
+      when Syntax_Trees.Nonterm =>
+         --  must be token_keyword_non_grammar
+         declare
+            Children : Syntax_Trees.Valid_Node_Index_Array renames 
Tree.Children (Tokens (2));
+            Child_1  : Base_Token renames Data.Terminals.all (Tree.Terminal 
(Children (1)));
+         begin
+            case Token_Enum_ID'(-Child_1.ID) is
+            when Wisitoken_Grammar_Actions.TOKEN_ID =>
+
+               WisiToken.BNF.Add_Token
+                 (Data.Tokens.Tokens,
+                  Kind  => Get_Text (Data, Tree, Children (3)),
+                  Name  => Get_Text (Data, Tree, Tokens (3)),
+                  Value => Get_Text (Data, Tree, Tokens (4)));
+
+            when KEYWORD_ID =>
+
+               Data.Tokens.Keywords.Append
+                 ((Name  => +Get_Text (Data, Tree, Tokens (3)),
+                   Value => +Get_Text (Data, Tree, Tokens (4))));
+
+            when NON_GRAMMAR_ID =>
+
+               WisiToken.BNF.Add_Token
+                 (Data.Tokens.Non_Grammar,
+                  Kind  => Get_Text (Data, Tree, Children (3)),
+                  Name  => Get_Text (Data, Tree, Tokens (3)),
+                  Value => Get_Text (Data, Tree, Tokens (4)));
+
+            when others =>
+               raise SAL.Programmer_Error;
+            end case;
+         end;
+
+      when Syntax_Trees.Shared_Terminal =>
+         case Enum_ID (2) is
+         when CODE_ID =>
+            declare
+               Location : WisiToken.BNF.Raw_Code_Location;
+
+               --  % code identifier_list raw_code
+               --  1 2    3               4
+               --
+               --  identifier_list = "action spec context"
+               --  identifier_list children = identifier_list IDENTIFIER_ID
+               --  children = identifier_list IDENTIFIER_ID
+               --  children = IDENTIFIER_ID
+               function Get_Loc_List return Base_Token_Array
+               is
+                  use all type SAL.Base_Peek_Type;
+                  use WisiToken.Syntax_Trees;
+                  Node   : Valid_Node_Index := Tokens (3);
+                  Result : Base_Token_Array (1 .. 3);
+                  First  : SAL.Peek_Type    := Result'Last + 1;
+               begin
+                  loop
+                     pragma Assert (-Tree.ID (Node) = identifier_list_ID);
+                     exit when not Tree.Has_Children (Node);
+                     declare
+                        Children : constant Valid_Node_Index_Array := 
Tree.Children (Node);
+                     begin
+                        if Children'Length = 1 then
+                           --  identifier_list : IDENTIFIER
+                           First := First - 1;
+                           Result (First) := Data.Terminals.all (Tree.Terminal 
(Children (1)));
+                           exit;
+
+                        elsif Children'Length = 2 then
+                           --  identifier_list : identifier_list IDENTIFIER
+                           First := First - 1;
+                           Result (First) := Data.Terminals.all (Tree.Terminal 
(Children (2)));
+
+                           Node := Children (1);
+                        else
+                           raise SAL.Programmer_Error;
+                        end if;
+                     end;
+                  end loop;
+                  return Result (First .. Result'Last);
+               end Get_Loc_List;
+
+               Loc_List : constant Base_Token_Array := Get_Loc_List;
+
+               function Get_Loc (Index : in SAL.Peek_Type) return String
+               is (Data.Grammar_Lexer.Buffer_Text (Loc_List 
(Index).Byte_Region));
+
+            begin
+               if Get_Loc (Loc_List'First) = "actions" then
+                  Location :=
+                    (if Get_Loc (2) = "spec" then
+                       (if Get_Loc (3) = "context" then 
WisiToken.BNF.Actions_Spec_Context
+                        elsif Get_Loc (3) = "pre" then 
WisiToken.BNF.Actions_Spec_Pre
+                        elsif Get_Loc (3) = "post" then 
WisiToken.BNF.Actions_Spec_Post
+                        else raise Grammar_Error with
+                          Error_Message
+                            (Data.Grammar_Lexer.File_Name, Loc_List (2).Line,
+                            "expecting {context | pre | post}"))
+
+                     elsif Get_Loc (2) = "body" then
+                       (if Get_Loc (3) = "context" then 
WisiToken.BNF.Actions_Body_Context
+                        elsif Get_Loc (3) = "pre" then 
WisiToken.BNF.Actions_Body_Pre
+                        elsif Get_Loc (3) = "post" then 
WisiToken.BNF.Actions_Body_Post
+                        else raise Grammar_Error with
+                          Error_Message
+                            (Data.Grammar_Lexer.File_Name, Loc_List (2).Line,
+                            "expecting {context | pre | post}"))
+
+                     else raise Grammar_Error);
+
+               elsif Get_Loc (Loc_List'First) = "copyright_license" then
+                  Location := WisiToken.BNF.Copyright_License;
+
+               else
+                  raise Grammar_Error with
+                    Error_Message
+                      (Data.Grammar_Lexer.File_Name, Loc_List 
(Loc_List'First).Line,
+                       "expecting {actions | copyright_license}");
+               end if;
+
+               Data.Raw_Code (Location) := WisiToken.BNF.Split_Lines (Get_Text 
(Data, Tree, Tokens (4)));
+            exception
+            when Grammar_Error =>
+               Put_Error
+                 (Error_Message
+                    (Data.Grammar_Lexer.File_Name, Token (2).Line, Token 
(2).Column,
+                     "invalid raw code location; actions {spec | body} 
{context | pre | post}"));
+            end;
+
+         when IDENTIFIER_ID =>
+            declare
+               Kind : constant String := Data.Grammar_Lexer.Buffer_Text (Token 
(2).Byte_Region);
+            begin
+               if Kind = "case_insensitive" then
+                  Data.Language_Params.Case_Insensitive := True;
+
+               elsif Kind = "conflict" then
+                  declare
+                     Tree_Indices : constant 
Syntax_Trees.Valid_Node_Index_Array := Tree.Get_Terminals
+                       (Tokens (3));
+                     --   %conflict <action_a>/<action_b> in state <LHS_A>, 
<LHS_B> on token <on>
+                     --              1        2 3         4  5      6     7  8 
     9  10     11
+                  begin
+                     Data.Conflicts.Append
+                       ((Source_Line => Data.Terminals.all (Tree.Terminal 
(Tree_Indices (1))).Line,
+                         Action_A    => +Get_Text (Data, Tree, Tree_Indices 
(1)),
+                         LHS_A       => +Get_Text (Data, Tree, Tree_Indices 
(6)),
+                         Action_B    => +Get_Text (Data, Tree, Tree_Indices 
(3)),
+                         LHS_B       => +Get_Text (Data, Tree, Tree_Indices 
(8)),
+                         On          => +Get_Text (Data, Tree, Tree_Indices 
(11))));
+                  end;
+
+               elsif Kind = "end" then
+                  --  matching '%if' specified current lexer.
+                  null;
+
+               elsif Kind = "elisp_face" then
+                  Data.User_Names.Faces.Append (Get_Text (Data, Tree, Tokens 
(3), Strip_Quotes => True));
+
+               elsif Kind = "elisp_indent" then
+                  Data.User_Names.Indents.Append
+                    ((Name  => +Get_Child_Text (Data, Tree, Tokens (3), 1, 
Strip_Quotes => True),
+                      Value => +Get_Child_Text (Data, Tree, Tokens (3), 2)));
+
+               elsif Kind = "regexp_name" then
+                  Data.User_Names.Regexps.Append
+                    ((Name  => +Get_Child_Text (Data, Tree, Tokens (3), 1),
+                      Value => +Get_Child_Text (Data, Tree, Tokens (3), 2)));
+
+               elsif Kind = "embedded_quote_escape_doubled" then
+                  Data.Language_Params.Embedded_Quote_Escape_Doubled := True;
+
+               elsif Kind = "end_names_optional_option" then
+                  Data.Language_Params.End_Names_Optional_Option := +Get_Text 
(Data, Tree, Tokens (3));
+
+
+               elsif Kind = "generate" then
+                  declare
+                     Children : constant Syntax_Trees.Valid_Node_Index_Array 
:= Tree.Get_Terminals (Tokens (3));
+                     Tuple     : WisiToken.BNF.Generate_Tuple;
+                  begin
+                     Tuple.Gen_Alg  := WisiToken.BNF.Generate_Algorithm'Value 
(Get_Text (Data, Tree, Children (1)));
+                     Tuple.Out_Lang := WisiToken.BNF.To_Output_Language 
(Get_Text (Data, Tree, Children (2)));
+                     for I in 3 .. SAL.Base_Peek_Type (Children'Length) loop
+                        declare
+                           Text : constant String := Get_Text (Data, Tree, 
Children (I));
+                        begin
+                           if Text = "text_rep" then
+                              Tuple.Text_Rep := True;
+
+                           elsif (for some I of WisiToken.BNF.Lexer_Image => 
Text = I.all) then
+                              Tuple.Lexer := WisiToken.BNF.To_Lexer (Text);
+
+                           elsif (for some I in WisiToken.BNF.Valid_Interface 
=>
+                                    WisiToken.BNF.To_Lower (Text) = 
WisiToken.BNF.To_Lower
+                                      (WisiToken.BNF.Valid_Interface'Image 
(I)))
+                           then
+                              Tuple.Interface_Kind := 
WisiToken.BNF.Valid_Interface'Value (Text);
+                           else
+                              declare
+                                 Token : Base_Token renames Data.Terminals.all 
(Tree.Terminal (Children (I)));
+                              begin
+                                 raise Grammar_Error with Error_Message
+                                   (Data.Grammar_Lexer.File_Name, Token.Line, 
Token.Column,
+                                    "invalid generate param '" & Text & "'");
+                              end;
+                           end if;
+                        end;
+                     end loop;
+                     WisiToken.BNF.Add (Data.Generate_Set, Tuple);
+                  end;
+
+               elsif Kind = "mckenzie_check_limit" then
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Check_Limit := Token_Index'Value 
(Get_Text (Data, Tree, Tokens (3)));
+
+               elsif Kind = "mckenzie_check_delta_limit" then
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Check_Delta_Limit := Integer'Value 
(Get_Text (Data, Tree, Tokens (3)));
+
+               elsif Kind = "mckenzie_cost_default" then
+                  if Tree.Get_Terminals (Tokens (3))'Length /= 4 then
+                     raise Grammar_Error with
+                       Error_Message
+                         (Data.Grammar_Lexer.File_Name, Data.Terminals.all 
(Tree.Min_Terminal_Index (Tokens (3))).Line,
+                          "too " & (if Tree.Get_Terminals (Tokens (3))'Length 
> 4 then "many" else "few") &
+                            " default costs; should be 'insert, delete, push 
back, ignore check fail'.");
+                  end if;
+
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Source_Line := Data.Terminals.all 
(Tree.Min_Terminal_Index (Tokens (1))).Line;
+
+                  Data.McKenzie_Recover.Default_Insert          := 
Natural'Value
+                    (Get_Child_Text (Data, Tree, Tokens (3), 1));
+                  Data.McKenzie_Recover.Default_Delete_Terminal := 
Natural'Value
+                    (Get_Child_Text (Data, Tree, Tokens (3), 2));
+                  Data.McKenzie_Recover.Default_Push_Back       := 
Natural'Value
+                    (Get_Child_Text (Data, Tree, Tokens (3), 3));
+                  Data.McKenzie_Recover.Ignore_Check_Fail       := 
Natural'Value
+                    (Get_Child_Text (Data, Tree, Tokens (3), 4));
+
+               elsif Kind = "mckenzie_cost_delete" then
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Delete.Append
+                    ((+Get_Child_Text (Data, Tree, Tokens (3), 1),
+                      +Get_Child_Text (Data, Tree, Tokens (3), 2)));
+
+               elsif Kind = "mckenzie_cost_insert" then
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Insert.Append
+                    ((+Get_Child_Text (Data, Tree, Tokens (3), 1),
+                      +Get_Child_Text (Data, Tree, Tokens (3), 2)));
+
+               elsif Kind = "mckenzie_cost_limit" then
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Cost_Limit := Natural'Value (Get_Text 
(Data, Tree, Tokens (3)));
+
+               elsif Kind = "mckenzie_cost_push_back" then
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Push_Back.Append
+                    ((+Get_Child_Text (Data, Tree, Tokens (3), 1),
+                      +Get_Child_Text (Data, Tree, Tokens (3), 2)));
+
+               elsif Kind = "mckenzie_enqueue_limit" then
+                  Data.Language_Params.Error_Recover := True;
+                  Data.McKenzie_Recover.Enqueue_Limit := Natural'Value 
(Get_Text (Data, Tree, Tokens (3)));
+
+               elsif Kind = "no_language_runtime" then
+                  Data.Language_Params.Language_Runtime := False;
+
+               elsif Kind = "no_enum" then
+                  Data.Language_Params.Declare_Enums := False;
+
+               elsif Kind = "start" then
+                  Data.Language_Params.Start_Token := +Get_Text (Data, Tree, 
Tokens (3));
+
+               elsif Kind = "re2c_regexp" then
+                  Data.Tokens.Regexps.Append
+                    ((+Get_Child_Text (Data, Tree, Tokens (3), 1),
+                      +Get_Child_Text (Data, Tree, Tokens (3), 2)));
+
+               else
+                  raise Grammar_Error with Error_Message
+                    (Data.Grammar_Lexer.File_Name, Token (2).Line, Token 
(2).Column, "unexpected syntax");
+
+               end if;
+            end;
+
+         when others =>
+            raise Grammar_Error with Error_Message
+              (Data.Grammar_Lexer.File_Name, Token (2).Line, Token (2).Column, 
"unexpected syntax");
+         end case;
+
+      when Syntax_Trees.Virtual_Terminal =>
+         raise SAL.Programmer_Error;
+      end case;
+   end Add_Declaration;
+
+   procedure Add_Nonterminal
+     (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+      Tree      : in     WisiToken.Syntax_Trees.Tree;
+      Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array)
+   is
+      Data : User_Data_Type renames User_Data_Type (User_Data);
+
+      LHS : constant String := Get_Text (Data, Tree, Tokens (1));
+
+      Right_Hand_Sides : WisiToken.BNF.RHS_Lists.List;
+   begin
+      Data.Rule_Count := Data.Rule_Count + 1;
+
+      Get_Right_Hand_Sides (Data, Tree, Right_Hand_Sides, Tokens (3));
+
+      if WisiToken.BNF.Is_Present (Data.Tokens.Rules, LHS) then
+         declare
+            LHS_Token : Base_Token renames Data.Terminals.all (Tree.Terminal 
(Tokens (1)));
+         begin
+            raise Grammar_Error with Error_Message
+              (Data.Grammar_Lexer.File_Name, LHS_Token.Line, LHS_Token.Column, 
"duplicate nonterm");
+         end;
+      else
+         Data.Tokens.Rules.Append
+           ((+LHS, Right_Hand_Sides,
+             Source_Line => Data.Terminals.all (Tree.Min_Terminal_Index 
(Tokens (1))).Line));
+      end if;
+   end Add_Nonterminal;
+
+end WisiToken_Grammar_Runtime;
diff --git a/wisitoken_grammar_runtime.ads b/wisitoken_grammar_runtime.ads
new file mode 100644
index 0000000..1e87241
--- /dev/null
+++ b/wisitoken_grammar_runtime.ads
@@ -0,0 +1,85 @@
+--  Abstract :
+--
+--  Runtime utils for wisi_grammar.wy actions.
+--
+--  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);
+
+with WisiToken.BNF;
+with WisiToken.Lexer;
+with WisiToken.Syntax_Trees;
+package WisiToken_Grammar_Runtime is
+
+   type User_Data_Type is new WisiToken.Syntax_Trees.User_Data_Type with
+   record
+      Grammar_Lexer : WisiToken.Lexer.Handle; -- used to read the .wy file now.
+
+      User_Lexer : WisiToken.BNF.Lexer_Type := WisiToken.BNF.None;
+      --  Used to read the user language file, after user parser is generated;
+      --  used now in '%if lexer' statements.
+
+      User_Parser : WisiToken.BNF.Generate_Algorithm := WisiToken.BNF.None;
+      --  Used to generate the user parser; used now in '%if parser'
+      --  statements.
+
+      Generate_Set : WisiToken.BNF.Generate_Set_Access;
+      --  As specified by %generate directives or command line.
+
+      Terminals        : WisiToken.Base_Token_Array_Access;
+      Raw_Code         : WisiToken.BNF.Raw_Code;
+      Language_Params  : WisiToken.BNF.Language_Param_Type;
+      Tokens           : aliased WisiToken.BNF.Tokens;
+      User_Names       : WisiToken.BNF.User_Names;
+      Conflicts        : WisiToken.BNF.Conflict_Lists.List;
+      McKenzie_Recover : WisiToken.BNF.McKenzie_Recover_Param_Type;
+
+      Rule_Count      : Integer := 0;
+      Action_Count    : Integer := 0;
+      Check_Count     : Integer := 0;
+
+      If_Lexer_Present  : Boolean := False;
+      If_Parser_Present : Boolean := False;
+      --  Set True by %if statements in Execute_Actions.
+
+      Ignore_Lines : Boolean := False;
+      --  An '%if' specified a different lexer, during Excute_Actions
+   end record;
+
+   overriding
+   procedure Set_Lexer_Terminals
+     (User_Data : in out User_Data_Type;
+      Lexer     : in     WisiToken.Lexer.Handle;
+      Terminals : in     WisiToken.Base_Token_Array_Access);
+
+   overriding procedure Reset (Data : in out User_Data_Type);
+
+   procedure Start_If
+     (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+      Tree      : in     WisiToken.Syntax_Trees.Tree;
+      Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+
+   procedure End_If (User_Data : in out 
WisiToken.Syntax_Trees.User_Data_Type'Class);
+
+   procedure Add_Declaration
+     (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+      Tree      : in     WisiToken.Syntax_Trees.Tree;
+      Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+
+   procedure Add_Nonterminal
+     (User_Data : in out WisiToken.Syntax_Trees.User_Data_Type'Class;
+      Tree      : in     WisiToken.Syntax_Trees.Tree;
+      Tokens    : in     WisiToken.Syntax_Trees.Valid_Node_Index_Array);
+
+end WisiToken_Grammar_Runtime;



reply via email to

[Prev in Thread] Current Thread [Next in Thread]