SymTree — Build and search nested hash tables of symbols (Emacs package)

This package is part of the Pansymbolic project.

Below you find the latest version of (1) the package's README and (2) its main source file.

For the git repository and issue tracker, see the project's page on sr.ht.

For more packages, see Software.


README.org

Overview

SymTree is a tool to create a specific kind of nested hash table from one of two kinds of flat hash tables.

It has a narrow focus; it deals with a very specific task.

It transforms an input hash table that maps symbols to the packages that define them (or vice-versa).

The output is a nested hash table that consolidates all tokenized symbols (as strings) and add the package(s) that define them to the leaves (as a list of symbols).

SymTree also has functions to query "prefixes" in that huge nested hash table, which it can also convert to an org tree in a separate file or buffer.

Usage examples? See:

Summary of callables

Here's an overview of this package's callables:

Function Summary
symtree-query-pre-demo Return list of packages having PREFIX.
symtree-query-pre-inits-demo Return alist of packages having inits of PREFIX.
symtree-query-pre Return list of packages having PREFIX.
symtree-query-pre-inits Return alist of packages having inits of PREFIX.
symtree-select Read H-FILE, convert it, write trees, then show org tree.
symtree-h-write Given INPUT-TYPE and HORHF, write HTREE-FILE.
symtree-h-make Given INPUT-TYPE and HORHF, output consolidated nested hash table.
symtree-h-make-from-packsyms-h Given HORHF, output consolidated nested hash table.
symtree-h-make-from-sympacks-h Given HORHF, output consolidated nested hash table.
symtree-merge-tables Merge hash tables in FILES-FROM, writing FILE-TO.
symtree-org-tree-write Write complete org tree from nested hash table in HORHF.
symtree-org-tree-buffer Show buffer with complete org tree from nested hash table in HORHF.
symtree-tokenize Tokenize object OBJ. Use ‘symtree-tokenize-around’.
symtree-tokenize-around Tokenize object OBJ, splitting before and after each separator.
symtree-see-readme Open symtree's README.org file.
symtree-see-news See the News in symtree's README.org file.

They're described in more detail below.

Functions

Query and report

Demos
symtree-query-pre-demo (prefix)

Return list of packages having PREFIX.

For the meaning of PREFIX, see symtree-query-pre.
The table comes from out/%s-tree.el, where %s is symtree-default-in.

;;;; Note: these depend on input and output files
(symtree-query-pre-demo "global")    => '(dash llama xht)
(symtree-query-pre-demo "")          => '(dash llama)
(symtree-query-pre-demo "!")         => '(dash)
(symtree-query-pre-demo "-")         => '(dash)
(symtree-query-pre-demo "--")        => '(dash)
(symtree-query-pre-demo "-each")     => '(dash)
(symtree-query-pre-demo "-each-")    => '(dash)
(symtree-query-pre-demo "-each-r")   => '(dash)
(symtree-query-pre-demo "h")         => '(xht)
(symtree-query-pre-demo "h-")        => '(xht)
(symtree-query-pre-demo "h--")       => '(xht)
(symtree-query-pre-demo "h=")        => '(xht)
(symtree-query-pre-demo "h==")       => '(xht)
(symtree-query-pre-demo "h*")        => '(xht)
(symtree-query-pre-demo "xht")       => '(xht)
(symtree-query-pre-demo "xht-")      => '(xht)
(symtree-query-pre-demo "smart")     => '(smart-foo smart-foo-plus smart-forward)
(symtree-query-pre-demo "smart-foo") => '(smart-foo smart-foo-plus)
(symtree-query-pre-demo "smart-up")  => '(smart-forward)
(symtree-query-pre-demo "sb")        => '(smart-bar)
(symtree-query-pre-demo "sb/")       => '(smart-bar)
(symtree-query-pre-demo "sb-")       => nil  ; no "-" separator with sb
(symtree-query-pre-demo "h%")        => nil  ; no "h%" in xht
(symtree-query-pre-demo "xh")        => nil  ;|it only matches at
(symtree-query-pre-demo "sma")       => nil  ;|separator boundaries
(symtree-query-pre-demo "no-exist")  => nil
symtree-query-pre-inits-demo (prefix)

Return alist of packages having inits of PREFIX.

For the meaning of PREFIX, see symtree-query-pre.
The table comes from out/%s-tree.el, where %s is symtree-default-in.

;;;; Note: these depend on input and output files
(symtree-query-pre-inits-demo "h==") => '(("h" xht) ("h=" xht) ("h==" xht))
(symtree-query-pre-inits-demo "sb/") => '(("sb" smart-bar) ("sb/" smart-bar))
(symtree-query-pre-inits-demo "sb-") => '(("sb" smart-bar) ("sb-"))

(symtree-query-pre-inits-demo "smart-foo-")
=> '(("smart"      smart-foo smart-foo-plus smart-forward)
     ("smart-"     smart-foo smart-foo-plus smart-forward)
     ("smart-foo"  smart-foo smart-foo-plus)
     ("smart-foo-" smart-foo smart-foo-plus))
Query prefixes
symtree-query-pre (horhf prefix)

Return list of packages having PREFIX.

HORHF is either a hash table or a file containing one.

A package has PREFIX if it defines any symbol that has PREFIX.

  • If symtree-sep-rx matches PREFIX's end, then the symbol has PREFIX
    if the symbol starts with PREFIX.
  • If symtree-sep-rx does not match PREFIX's end, then the symbol has
    PREFIX either if it's equal to PREFIX, or if it starts with PREFIX
    followed by something that matches symtree-sep-rx.
symtree-query-pre-inits (horhf prefix)

Return alist of packages having inits of PREFIX.

HORHF is either a hash table or a file containing one.

For the meaning of inits, see symtree--pre-inits.
For the meaning of PREFIX, see symtree-query-pre.

Then symtree-query-pre is run over HORHF on each of the inits,
and the results are returned as an alist.

Create symbol trees

Select input interactively and create output trees
symtree-select (&optional h-file input-type pre)

Read H-FILE, convert it, write trees, then show org tree.

Convert to both nested hash table and org tree.
For the meaning of INPUT-TYPE, see symtree-h-make.
PRE is an optional prefix, to write only this branch to .org.

Nested hash table from hash table of symbols defined by packages
symtree-h-write (input-type horhf htree-file &optional h-fmt)

Given INPUT-TYPE and HORHF, write HTREE-FILE.

For the meaning of INPUT-TYPE, see symtree-h-make.

HORHF is either a hash table or a file containing one.

H-FMT is the hash table format to use for writing the file.

When H-FMT is nil, if HORHF has more than symtree-min-for-htbl keys,
default to :htbl. Otherwise, use :h*.

Better use native :htbl format #s(…) for the output when input is large,
as it's read much faster.

Note that :htbl is short for :htbl-pp, which implies pretty-print, so
line breaks are applied. If compact is preferred, pass :htbl-cm instead.
However, this will be an enormous single line, which might freeze for
many users, and is therefore discouraged.

symtree-h-make (input-type horhf)

Given INPUT-TYPE and HORHF, output consolidated nested hash table.

INPUT-TYPE must be either 'ps or 'sp.

HORHF is either a hash table or a file containing one.

 ;;;; Input may be formatted as symbol or string

 ;;;;; Input-type: symbol→packages
 ;;;;; 'sp: symbol and list of packages that define it
 (h= (symtree-h-make 'sp (h* 'pack-a-foo  '(pack-a)
                             'pack-a-bar  '(pack-a)
                             'pack-b-qux  '(pack-b)
                             'pack-b-woo  '(pack-b)))
     (symtree-h-make 'sp (h* 'pack-a-foo  '("pack-a")
                             'pack-a-bar  '("pack-a")
                             'pack-b-qux  '("pack-b")
                             'pack-b-woo  '("pack-b")))
     (symtree-h-make 'sp (h* "pack-a-foo" '("pack-a")
                             "pack-a-bar" '("pack-a")
                             "pack-b-qux" '("pack-b")
                             "pack-b-woo" '("pack-b")))
     (symtree-h-make 'sp (h* "pack-a-foo" '(pack-a)
                             "pack-a-bar" '(pack-a)
                             "pack-b-qux" '(pack-b)
                             "pack-b-woo" '(pack-b)))
     (h* "pack" (h* "-" (h* "a" (h* "-" (h* "foo" (h* :@ '(pack-a))
                                            "bar" (h* :@ '(pack-a))))
                            "b" (h* "-" (h* "qux" (h* :@ '(pack-b))
                                            "woo" (h* :@ '(pack-b))))))))
 => t

;;;;;; Here, the same symbol is being defined in two packages
 (h= (symtree-h-make 'sp (h* "pack-a-foo" '(pack-a pack-x)
                             "pack-a-bar" '(pack-x)
                             "pack-w-woo" '(pack-a)))
     (h* "pack" (h* "-" (h* "a" (h* "-" (h* "foo" (h* :@ '(pack-a pack-x))
                                            "bar" (h* :@ '(pack-x))))
                            "w" (h* "-" (h* "woo" (h* :@ '(pack-a))))))))
 => t

 ;;;;; Input-type: package→symbols
 ;;;;; 'ps: package and list of symbols it defines
 (h= (symtree-h-make 'ps (h* 'pack-a  '(pack-a-foo pack-a-bar)
                             'pack-b  '(pack-b-qux pack-b-woo)))
     (symtree-h-make 'ps (h* 'pack-a  '("pack-a-foo" "pack-a-bar")
                             'pack-b  '("pack-b-qux" "pack-b-woo")))
     (symtree-h-make 'ps (h* "pack-a" '(pack-a-foo pack-a-bar)
                             "pack-b" '(pack-b-qux pack-b-woo)))
     (symtree-h-make 'ps (h* "pack-a" '("pack-a-foo" "pack-a-bar")
                             "pack-b" '("pack-b-qux" "pack-b-woo")))
     (h* "pack" (h* "-" (h* "a" (h* "-" (h* "foo" (h* :@ '(pack-a))
                                            "bar" (h* :@ '(pack-a))))
                            "b" (h* "-" (h* "qux" (h* :@ '(pack-b))
                                            "woo" (h* :@ '(pack-b))))))))
 => t

 ;;;; From file to file
 (let ((in  (h-read-h "in/demo.el"))
       (out (h-read-h "out/demo-tree.el")))
   (h= (symtree-h-make 'ps in)
       out))

 => t
symtree-h-make-from-packsyms-h (horhf)

Given HORHF, output consolidated nested hash table.

HORHF is either a hash table or a file containing one.

HORHF's every KEY is a package, and its VALUE is a list of symbols of a
certain kind (e.g. functions) defined by the package. The keys and list
elements may be passed as either strings or symbols.

symtree-h-make-from-sympacks-h (horhf)

Given HORHF, output consolidated nested hash table.

HORHF is either a hash table or a file containing one.

HORHF's every KEY is a symbol of a certain kind (e.g. functions), and
its VALUE is a list of packages that define that symbol. The keys and
list elements may be passed as either strings or symbols.

Typically, and ideally, only one package will show up in each value's
list — otherwise a collision is likely to be going on.

Merge hash tables
symtree-merge-tables (file-to &rest files-from)

Merge hash tables in FILES-FROM, writing FILE-TO.

FILES-FROM can be any number of files or directories. When it's a
directory, all files inside it are recursively collected and used.

Each file is then read, expecting a hash table inside. An error is
thrown when an input file has no hash table or is unreadable.

All hash tables are then merged and the end result is written as
FILE-TO, without confirmation.

If result is simple (dimension = 1), write output in (h*…) format.
Otherwise assume nested, and output #s(hash-table…) native format.

Files in FILES-FROM may be gzip'd.

Org tree from nested hash table
symtree-org-tree-write (horhf otree-file &optional pre header level)

Write complete org tree from nested hash table in HORHF.

By complete we mean a single top node contains everything else.
HORHF is either a hash table or a file containing one.
OTREE-FILE is the file to write to.
HEADER is a header string, and defaults to [all].
PRE is an optional prefix, to show only this branch.
LEVEL is the level of the top node, and defaults to 1.

symtree-org-tree-buffer (horhf &optional pre header level)

Show buffer with complete org tree from nested hash table in HORHF.

By complete we mean a single top node contains everything else.
HORHF is either a hash table or a file containing one.
HEADER is a header string, and defaults to [all].
PRE is an optional prefix, to show only this branch.
LEVEL is the level of the top node, and defaults to 1.

Tokenize
symtree-tokenize (obj)

Tokenize object OBJ. Use symtree-tokenize-around.

(symtree-tokenize 'foo-this-fun)   => '("foo" "-" "this" "-" "fun")
(symtree-tokenize "foo-this-fun")  => '("foo" "-" "this" "-" "fun")
(symtree-tokenize #'foo-this-fun)  => '("foo" "-" "this" "-" "fun")
(symtree-tokenize #'foo--this)     => '("foo" "-" "-" "this")
(symtree-tokenize #'--this-fun)    => '("" "-" "-" "this" "-" "fun")
(symtree-tokenize #'foo-)          => '("foo" "-")
(symtree-tokenize #'h==)           => '("h" "=" "=")
(symtree-tokenize #'h?)            => '("h" "?")
(symtree-tokenize #'sb/foo--bar)   => '("sb" "/" "foo" "-" "-" "bar")
(symtree-tokenize #'##)            => '("")
(symtree-tokenize "")              => '("")
symtree-tokenize-around (obj)

Tokenize object OBJ, splitting before and after each separator.

This makes it much more flexible for querying.

See README

symtree-see-readme (&optional heading narrow)

Open symtree's README.org file.

Search for the file in symtree.el's directory.

If found, open it read-only.

If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string.

If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found.

symtree-see-news ()

See the News in symtree's README.org file.

More examples

  • To test a new input file, run M-x symtree-select (hint: select "primitives.el").
  • See dev/examples.el for query examples, since these depend on input and output files.
  • Then see folders in, out.

Details

I/O

Input
  • One or more files or directories. Directories are recursed.
  • Every file must contain a flat (simple, dimension=1) hash table.
  • Files may be compressed (e.g. gzip'd).
  • These hash tables may be in native format #s(hash-table…) or (h*…).
  • Commented lines are not a problem.
  • Two input formats are accepted
    • 'ps
      • Each key is a package name.
      • Each value is a list of functions defined by that package.
    • 'sp
      • Each key is a function defined by at least one package.
      • Each value is a list of packages that define that function.
  • Package names and functions may be formatted as either symbol or string.
  • Besides "functions", other kinds of symbols, especially variables, can be dealt with later, separately, in exactly the same way.
Output
  • Output files are, for now, written uncompressed.
  • One input file generates one output file.
  • The output file contains a nested hash table.
  • The nested hash table may be in native format #s(hash-table…) or (h*…).
  • Currently, if format is unspecified, it's automatically chosen depending on the size of first level, with native being preferred for larger tables.
  • Native format is noisier than (h*…), so file size is expected to be larger. Nevertheless, gzip can significantly reduce that, and .gz files are automatically read.
  • The output is the result of tokenizing every symbol in the package's functions list and adding it to the hash table. Example:

    Package: 'foo
    Function: #'foo--do-this
    Branch added:

    (h* "foo" (h* "-" (h* "-" (h* "do" (h* "-" (h* "this" (h* :@ '(foo))))))))
    
  • Note that:
    • the function symbol is tokenized and the tokens are output as strings
    • the last hash table always has:
      • as key, the keyword :@
      • as value, a list of symbols representing the packages that define that tokenized function

Portability of the output

  • It can be exported as an org tree with symtree-org-tree-write.
  • The output can also easily be exported to any nested format supported by xht's conversion functions.

    Currently, these are:

    #'h->json*
    #'h->alist*
    #'h->plist*
    

Querying and navigation

  • Querying prefixes is fast and simple with symtree-query-pre.
  • The org tree export format provides readability and navigation.

    It doesn't need to show the whole thing. For example, if "org-foo--" is passed, only the subtree beginning with this exact prefix would be shown.

Performance

Creating the nested hash table

The use of a huge nested hash table composed of thousands of tiny ones may seem unusual.

You'd think that the performance cost of creating them would be prohibitive because of a fixed overhead.

Not quite. See here, especially the benchmark in the beginning.

Reading nested hash table output files

Huge outputs might have thousands of hash tables, and the performance penalty of using (h*…) would likely be high, because each of the many (h*…) functions would need to be read. On the other hand, native #s(hash-table…) would be read directly as an object, so it's much faster, and therefore preferred.

Look up a prefix in the nested hash table

These are hash tables. Lookups are as fast as you can have them.

Install

See my page Software for the most up-to-date instructions on how to download and install any of my Emacs packages.

Having downloaded and installed the package and its dependencies, adapt the configurations below to your init.el file.

(use-package symtree :demand t)

Alternatively:

(require 'symtree)

Contributing

See my page Software for information about how to contribute to any of my Emacs packages.

News

0.2.0

New function
Improvements in docstrings

0.1.0

Release

See also

Other packages

SymTable, which is another package I wrote, complements this one.

License

This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.

Therefore, license and copyright information can be found in:

  • each file's comment header, or
  • an adjacent file of the same name with the additional extension .license, or
  • the .reuse/dep5 file

The full text of the licenses can be found in the LICENSES subdirectory.


symtree.el

Structure

;;; symtree.el --- Build and search nested hash tables of symbols -*- lexical-binding: t -*-
;;; Commentary:
;;;; For all the details, please do see the README
;;; Code:
;;;; Libraries
;;;; Package metadata
;;;; Customizable variables
;;;; Other variables
;;;; Functions
;;;;; Query and report
;;;;;; Demos
;;;;;; Query prefixes
;;;;; Create symbol trees
;;;;;; Select input interactively and create output trees
;;;;;; Nested hash table from hash table of symbols defined by packages
;;;;;;; Merge hash tables
;;;;;;; Read hash table
;;;;;; Org tree from nested hash table
;;;;;; Tokenize
;;;;; See README
;;;; Wrapping up
;;; symtree.el ends here

Contents

;;; symtree.el --- Build and search nested hash tables of symbols -*- lexical-binding: t -*-

;; SPDX-FileCopyrightText: © flandrew <https://flandrew.srht.site/listful>

;;---------------------------------------------------------------------------
;; Author:    flandrew
;; Created:   2024-10-30
;; Updated:   2025-08-08
;; Keywords:  lisp
;; Homepage:  <https://flandrew.srht.site/listful/software.html>
;;---------------------------------------------------------------------------
;; Package-Version:  0.2.0
;; Package-Requires: ((emacs "26.1") (xht "1.0.5") (dash "2.15") (s "1.12"))
;;---------------------------------------------------------------------------

;; SPDX-License-Identifier: GPL-3.0-or-later

;; This file 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.
;;
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this file. If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; From packages and their symbols to nested hash tables of tokenized symbols.
;;
;;;; For all the details, please do see the README
;;
;; Open it easily with:
;;   (find-file-read-only "README.org")   <--- C-x C-e here¹
;;
;; or from any buffer:
;;   M-x symtree-see-readme
;;
;; or read it online:
;;   <https://flandrew.srht.site/listful/sw-emacs-symtree.html>
;;
;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;



;;; Code:
;;;; Libraries

(require 's)
(require 'org)
(require 'xht)       ; <---also by the author of this package
(require 'dash)
(require 'lisp-mnt)  ; lm-summary’, ‘lm-homepage’, ‘lm-version’, ‘lm-header


;;;; Package metadata

(defvar symtree--name "SymTree")

(defvar symtree--dot-el
  (format "%s.el" (file-name-sans-extension (eval-and-compile
                                              (or load-file-name
                                                  buffer-file-name)))))
(defvar symtree--readme-org
  (expand-file-name "README.org" (file-name-directory symtree--dot-el)))

(defvar symtree--summary  (lm-summary   symtree--dot-el))
(defvar symtree--homepage (lm-homepage  symtree--dot-el))
(defvar symtree--version  (lm-with-file symtree--dot-el
                            (or (lm-header "package-version")
                                (lm-version))))


;;;; Customizable variables

(defgroup symtree nil
  (format "%s." symtree--summary)
  :group 'lisp
  :link  '(emacs-library-link :tag "Lisp file" "symtree.el")
  :link  `(file-link :tag "README.org" ,symtree--readme-org)
  :link  `(url-link  :tag "Homepage"   ,symtree--homepage))

(defcustom symtree-default-in "demo"
  "Default file basename for tests."
  :package-version '(symtree "0.1.0")
  :type 'string)

(defcustom symtree-min-for-htbl 50
  "Minimum number of keys to default to :htbl output."
  :package-version '(symtree "0.1.0")
  :type 'integer)


;;;; Other variables

(defvar symtree-sep-rx "[^a-zA-Z0-9@]+"
  "Regexp of separator.")


;;;; Functions
;;;;; Query and report
;;;;;; Demos

(defun symtree-query-pre-demo (prefix)
  "Return list of packages having PREFIX.
For the meaning of PREFIX, see ‘symtree-query-pre’.
The table comes from out/%s-tree.el, where %s is ‘symtree-default-in’."
  (symtree--demoify #'symtree-query-pre prefix))

(defun symtree-query-pre-inits-demo (prefix)
  "Return alist of packages having inits of PREFIX.
For the meaning of PREFIX, see ‘symtree-query-pre’.
The table comes from out/%s-tree.el, where %s is ‘symtree-default-in’."
  (symtree--demoify #'symtree-query-pre-inits prefix))

(defun symtree--demoify (fun &rest rest)
  "Make demo of FUN and REST using default tree."
  (let ((htree-file (format "out/%s-tree.el" symtree-default-in)))
    (apply fun htree-file rest)))

;;;;;; Query prefixes

(defun symtree-query-pre (horhf prefix)
  "Return list of packages having PREFIX.
HORHF is either a hash table or a file containing one.

A package has PREFIX if it defines any symbol that has PREFIX.

- If ‘symtree-sep-rx’ matches PREFIX's end, then the symbol has PREFIX
  if the symbol starts with PREFIX.

- If ‘symtree-sep-rx’ does not match PREFIX's end, then the symbol has
  PREFIX either if it's equal to PREFIX, or if it starts with PREFIX
  followed by something that matches ‘symtree-sep-rx’."
  (let* ((htree (symtree--read-h horhf))
         (token (symtree-tokenize prefix))
         (htble (symtree--query-last-h token htree)))
    (symtree--query-leaves htble)))

(defun symtree-query-pre-inits (horhf prefix)
  "Return alist of packages having inits of PREFIX.
HORHF is either a hash table or a file containing one.

For the meaning of inits, see ‘symtree--pre-inits’.
For the meaning of PREFIX, see ‘symtree-query-pre’.

Then ‘symtree-query-pre’ is run over HORHF on each of the inits,
and the results are returned as an alist."
  (let* ((htree (symtree--read-h horhf))
         (inits (symtree--pre-inits prefix)))
    (--map `(,it . ,(symtree-query-pre htree it)) inits)))

(defun symtree--pre-inits (prefix)
  "Return inits of PREFIX.
The inits of PREFIX is a list containing all the prefixes of
PREFIX. For example, the inits of PREFIX ‘s-exp-foo--’ are:
  (s s- s-exp s-exp- s-exp-foo s-exp-foo- s-exp-foo--)
but with each item as a string."
  (->> prefix  h-as-string  symtree-tokenize  -inits  cdr
       (-map #'string-join)))

(defun symtree--query-last-h (token horhf)
  "Get last hash table value from TOKEN and HORHF.
HORHF is either a hash table or a file containing one."
  (let* ((htree (symtree--read-h horhf))
         (res htree))
    (while (and token res)
      (setq res (h-get res (pop token))))
    (or res (h*))))

(defun symtree--query-leaves (horhf)
  "Given hash table HORHF, return flat uniq sorted list of leaves.
HORHF is either a hash table or a file containing one."
  (let* ((htree (symtree--read-h horhf))
         (leaves ()))
    (h--each htree
      (push (if (h? value)
                (symtree--query-leaves value)
              value)
            leaves))
    (->> leaves  -flatten  -uniq
         (-sort (-on #'string< #'h-as-string)))))


;;;;; Create symbol trees
;;;;;; Select input interactively and create output trees

;;;###autoload
(defun symtree-select (&optional h-file input-type pre)
  "Read H-FILE, convert it, write trees, then show org tree.
Convert to both nested hash table and org tree.
For the meaning of INPUT-TYPE, see ‘symtree-h-make’.
PRE is an optional prefix, to write only this branch to .org."
  (interactive)
  (when h-file
    (unless (file-readable-p h-file)
      (error "Not readable: %s" h-file)))
  (let* ((dir (file-name-directory symtree--dot-el))
         (in  (expand-file-name "in/"  dir))
         (out (expand-file-name "out/" dir))
         (dem (format "%s.el" symtree-default-in))
         (org) (htbl))
    (unless h-file
      (setq h-file (read-file-name "Select input file: " in dem t)))
    (setq htbl (h-read-h h-file))
    (unless (memq (h-as-symbol input-type) '(ps sp))
      (let* ((itg (h-as-string (symtree--guess-input-type htbl)))
             (pro (format "Select input-type (best guess = %s): " itg)))
        (setq input-type (h-as-symbol (completing-read
                                       pro '(ps sp) nil t itg)))))
    (unless pre
      (let ((p (read-from-minibuffer
                "Type a prefix to filter, or RET to not filter: "
                "[DON'T FILTER]")))
        (unless (string= p "[DON'T FILTER]")
          (setq pre p))))
    (setq out (expand-file-name
               (format "%s-tree.el" (file-name-base h-file)) out)
          org (s-replace-regexp "el$" "org" out))
    (symtree-h-write input-type htbl out)
    (symtree-org-tree-write out org pre)
    (find-file-read-only org)))

(defun symtree--guess-input-type (horhf)
  "Given HORHF, guess input-type.
For the meaning of INPUT-TYPE, see ‘symtree-h-make’.
HORHF is either a hash table or a file containing one."
  (let* ((symh (symtree--read-h horhf))
         (size (h-size symh))
         (upto (if (> size 10) 10 size))
         (vals ())
         (cnt  0))
    (catch 'break
      (h--each symh
        (if (< upto cnt)
            (throw 'break nil)
          (setq vals (append value vals)
                cnt  (1+ cnt)))))
    ;; pack→syms: will have large value lists
    ;; sym→packs: value lists will be mostly of size 1
    (if (>= (length vals) (* upto 2)) 'ps 'sp)))


;;;;;; Nested hash table from hash table of symbols defined by packages

(defun symtree-h-write (input-type horhf htree-file &optional h-fmt)
  "Given INPUT-TYPE and HORHF, write HTREE-FILE.
For the meaning of INPUT-TYPE, see ‘symtree-h-make’.

HORHF is either a hash table or a file containing one.

H-FMT is the hash table format to use for writing the file.

When H-FMT is nil, if HORHF has more than ‘symtree-min-for-htbl’ keys,
default to :htbl. Otherwise, use :h*.

Better use native :htbl format #s(…) for the output when input is large,
as it's read much faster.

Note that :htbl is short for :htbl-pp, which implies pretty-print, so
line breaks are applied. If compact is preferred, pass :htbl-cm instead.
However, this will be an enormous single line, which might freeze for
many users, and is therefore discouraged."
  (let* ((sym-h (symtree--read-h horhf))
         (res-h (symtree-h-make input-type sym-h)))
    (unless h-fmt
      (setq h-fmt (if (> (h-size sym-h) symtree-min-for-htbl) :htbl :h*)))
    (h-write-ht-like! htree-file h-fmt res-h)))

(defun symtree-h-make (input-type horhf)
  "Given INPUT-TYPE and HORHF, output consolidated nested hash table.

INPUT-TYPE must be either \\='ps or \\='sp.
- If \\='ps, use ‘symtree-h-make-from-packsyms-h’.
- If \\='sp, use ‘symtree-h-make-from-sympacks-h’.

HORHF is either a hash table or a file containing one."
  (pcase input-type
    ('ps (symtree-h-make-from-packsyms-h horhf))
    ('sp (symtree-h-make-from-sympacks-h horhf))
    (_   (error "‘symtree-h-make’: input-type must be 'ps or 'sp"))))

(defun symtree-h-make-from-packsyms-h (horhf)
  "Given HORHF, output consolidated nested hash table.
HORHF is either a hash table or a file containing one.

HORHF's every KEY is a package, and its VALUE is a list of symbols of a
certain kind (e.g. functions) defined by the package. The keys and list
elements may be passed as either strings or symbols."
  (let ((sym-h (symtree--read-h horhf))
        (res-h (h-new))
        (token))
    (h--each sym-h
      (setq key (h-as-symbol key))
      (--each value
        (setq token (symtree-tokenize it))
        (apply #'h-put*! `(,res-h ,@token :@ (,key)))))
    res-h))

(defun symtree-h-make-from-sympacks-h (horhf)
  "Given HORHF, output consolidated nested hash table.
HORHF is either a hash table or a file containing one.

HORHF's every KEY is a symbol of a certain kind (e.g. functions), and
its VALUE is a list of packages that define that symbol. The keys and
list elements may be passed as either strings or symbols.

Typically, and ideally, only one package will show up in each value's
list — otherwise a collision is likely to be going on."
  (let ((sym-h (symtree--read-h horhf))
        (res-h (h-new))
        (token))
    (h--each sym-h
      (setq token (symtree-tokenize key)
            value (mapcar #'h-as-symbol value))
      (apply #'h-put*! `(,res-h ,@token :@ ,value)))
    res-h))


;;;;;;; Merge hash tables

;; This can be useful to consolidate either the inputs or the outputs of
;; symtree-h-write’. Tables can then be easily updated with diffs.

(defun symtree-merge-tables (file-to &rest files-from)
  "Merge hash tables in FILES-FROM, writing FILE-TO.

FILES-FROM can be any number of files or directories. When it's a
directory, all files inside it are recursively collected and used.

Each file is then read, expecting a hash table inside. An error is
thrown when an input file has no hash table or is unreadable.

All hash tables are then merged and the end result is written as
FILE-TO, without confirmation.

If result is simple (dimension = 1), write output in (h*…) format.
Otherwise assume nested, and output #s(hash-table…) native format.

Files in FILES-FROM may be gzip'd."
  (unless (file-writable-p file-to)
    (error "Not writable: %s" file-to))
  (let* ((from  (-flatten (mapcar #'symtree--files-recursively files-from)))
         (res-h (->> from (mapcar #'h-read-h) (apply #'h-mix*)))
         (h-fmt (if (h-1d? res-h) :h* :htbl)))
    (h-write-ht-like! file-to h-fmt res-h)))

(defun symtree--files-recursively (obj)
  "If OBJ is a file, return it; if dir, recurse and return list.
When OBJ or any of the recursively listed files is unreadable,
signal error."
  (unless (file-readable-p obj)
    (error "Not readable: %s" obj))
  (let ((files (cond ((file-regular-p obj) obj)
                     ((file-directory-p obj)
                      (directory-files-recursively obj "." nil t t))
                     (:otherwise (error "Could not process %s" obj)))))
    (and-let* (((listp files))
               (unrdbl (-filter (-not #'file-readable-p) files))
               ((error "Not readable: %S" unrdbl))))
    files))


;;;;;;; Read hash table

(defun symtree--read-h (horhf)
  "Read HORHF: a hash table or a file containing one."
  (if (h? horhf) horhf (h-read-h horhf)))


;;;;;; Org tree from nested hash table

(defun symtree-org-tree-write (horhf otree-file &optional pre header level)
  "Write complete org tree from nested hash table in HORHF.
By complete we mean a single top node contains everything else.
HORHF is either a hash table or a file containing one.
OTREE-FILE is the file to write to.
HEADER is a header string, and defaults to [all].
PRE is an optional prefix, to show only this branch.
LEVEL is the level of the top node, and defaults to 1."
  (let ((orgstr (symtree--org-tree-1 horhf pre header level)))
    (with-temp-buffer
      (insert orgstr)
      (write-region (point-min) (point-max) otree-file))))

(defun symtree-org-tree-buffer (horhf &optional pre header level)
  "Show buffer with complete org tree from nested hash table in HORHF.
By complete we mean a single top node contains everything else.
HORHF is either a hash table or a file containing one.
HEADER is a header string, and defaults to [all].
PRE is an optional prefix, to show only this branch.
LEVEL is the level of the top node, and defaults to 1."
  (let ((orgstr (symtree--org-tree-1 horhf pre header level))
        (newbuf (get-buffer-create "*symtree*")))
    (with-current-buffer newbuf
      (erase-buffer)
      (insert orgstr)
      (org-mode))
    (switch-to-buffer newbuf)))

(defun symtree--org-tree-1 (horhf &optional pre header level)
  "Return complete org tree string from nested hash table in HORHF.
By complete we mean a single top node contains everything else.
HORHF is either a hash table or a file containing one.
HEADER is a header string, and defaults to [all].
PRE is an optional prefix, to show only this branch.
LEVEL is the level of the top node, and defaults to 1."
  (unless level
    (setq level 1))
  (let* ((htree  (symtree--read-h horhf))
         (orgstr (symtree--org-str-from-htree htree pre (1+ level))))
    (symtree--org-str-complete orgstr header)))

(defun symtree--org-str-complete (orgstr &optional header)
  "Output a complete org tree string from STR.
By complete we mean a single top node contains everything else.
HEADER is a header string, and defaults to [all].
ORGSTR is the output of ‘symtree--org-str-from-htree’.
Level is autodetected by the first line of STR."
  (unless header
    (setq header "[all]"))
  (let* ((stars (s-repeat 20 "*"))
         (level (->> orgstr  (s-shared-start stars)  length  1-))
         (stars (s-repeat level "*")))
    (format "%s %s\n%s" stars header orgstr)))

(defun symtree--org-str-from-htree (horhf &optional pre level)
  "Output an org tree string from nested hash table HORHF.
HORHF is either a hash table or a file containing one.
PRE is an optional prefix, to show only this branch.
LEVEL is the start level, which defaults to 2."
  (unless level
    (setq level 2))
  (let ((htree (symtree--read-h horhf)))
    (when pre
      (let* ((token (symtree-tokenize pre))
             (query (symtree--query-last-h token htree)))
        ;; Insert the tokenized prefix before
        (setq htree (apply #'h-reduce-r (-snoc token query)))))
    (with-output-to-string
      (h--each htree
        (princ (format "%s %s\n"
                       (s-repeat level "*")
                       (if (h? value) key value)))
        (when (h? value)
          (princ (symtree--org-str-from-htree value nil (1+ level))))))))


;;;;;; Tokenize

(defun symtree-tokenize (obj)
  "Tokenize object OBJ. Use ‘symtree-tokenize-around’."
  (symtree-tokenize-around obj))

(defun symtree-tokenize-around (obj)
  "Tokenize object OBJ, splitting before and after each separator.
This makes it much more flexible for querying."
  (let ((str (format "%s" obj)))
    (with-temp-buffer
      (insert str)
      (unless (bobp)       ; bobp only when obj is either '## or ""
        (backward-char 1)) ; we don't want a "" tail if obj ends in sep
      (while (not (bobp))
        (and (or (looking-at   symtree-sep-rx t)
                 (looking-back symtree-sep-rx (1- (point))))
             (open-line 1))
        (backward-char 1))
      (when (looking-at symtree-sep-rx t)
        (open-line 1))     ; we want a "" head if obj starts with sep
      (s-lines (buffer-string)))))
;; Note: there're certainly ways to do this more efficiently using
;; string-match’, but this can be optimized later if really needed.


;;;;; See README

;;;###autoload
(defun symtree-see-readme (&optional heading narrow)
  "Open symtree's README.org file.
Search for the file in symtree.el's directory.

If found, open it read-only.

If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string.

If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found."
  (interactive)
  (let ((readme symtree--readme-org))
    (if (file-exists-p readme)
        (let ((pr (make-progress-reporter
                   (format "Opening %s ... "
                           (abbreviate-file-name readme)))))
          (find-file-read-only readme)
          (when heading
            (symtree--goto-org-heading heading narrow))
          (progress-reporter-done pr))
      (message "Couldn't find %s's README.org" symtree--name))))

;;;###autoload
(defun symtree-see-news ()
  "See the News in symtree's README.org file."
  (interactive)
  (symtree-see-readme "News" 'narrow)
  (symtree--display-org-subtree))

(defun symtree--display-org-subtree ()
  "Selectively display org subtree."
  (let ((cmds '( outline-hide-subtree outline-show-children
                 outline-next-heading outline-show-branches)))
    (and (equal (mapcar #'fboundp cmds) '(t t t t))
         (mapc #'funcall cmds))))

(defun symtree--goto-org-heading (heading &optional narrow)
  "Navigate to org HEADING and optionally NARROW to it."
  (let* ((hrx (format "^[*]+ %s" heading))
         (pos (save-match-data
                (save-excursion
                  (save-restriction
                    (widen)  (goto-char (point-max))
                    (re-search-backward hrx nil t 1))))))
    (when pos
      (widen)  (goto-char pos)
      (if (and narrow (fboundp 'org-narrow-to-subtree))
          (org-narrow-to-subtree)
        (recenter-top-bottom 1))
      (when (fboundp 'outline-show-subtree)
        (outline-show-subtree))
      (when (fboundp 'org-flag-drawer)
        (save-excursion
          (forward-line 1)
          (org-flag-drawer t))))))


;;;; Wrapping up

(provide 'symtree)

;; Local Variables:
;; coding:                     utf-8
;; indent-tabs-mode:           nil
;; sentence-end-double-space:  nil
;; outline-regexp:             ";;;;* "
;; End:

;;; symtree.el ends here