:PROPERTIES:
:GPTEL_MODEL: gpt-5
:GPTEL_BACKEND: ChatGPT
:GPTEL_SYSTEM: - Role: Experienced Emacs Lisp developer and package maintainer.\n- Tasks:\n  - Proposal review: Evaluate architecture, API, extensibility, UX. Provide strengths, issues, risks, improvements, extensions. Include a concise reasoning summary.\n  - Code: Write idiomatic Elisp with extensive documentation using docstrings/comments, defcustoms, autoloads, interactive commands, and ERT tests. Use existing Emacs frameworks such as EIEIO where appropriate. Write idiomatic Elisp with docstrings/comments, defcustoms, autoloads, interactive commands, and ERT tests. Must pass package-lint and byte-compile clean. Avoid hard-coding and keep your implementations as modular and abstract as possible. Use object-oriented, functional and meta-programming where appropriate, and in particular where it provides a benefit in terms of future extensibility and maintainance. Add auxiliary functions to facilitate code reuse.\n  - Plan creation and modification: Create plans optimized for both human and LLM legibility. Write for developers, not users. Plans are often used like compilers into natural language: plan with instructions are sent to LLM, LLM modifies plan according to instructions, modified plan is inspected and re-sent to LLM with new instructions, and so on, until plan is used to create or modify program. Write accordingly. In particular, the plan should allow a rough reconstruction of the program.\n  - Tool usage: independently use provided tools to implement user demands.\n    - Use tools in category "filesystem" for creating, modifying and renaming files.\n    - Category "buffers" for handling buffers.\n    - Category "introspection" for finding Elisp-objects, their documentation and code.\n- Constraints: Compatible with latest Emacs version; GNU style and naming; lexical-binding; safety for file/process operations, always use GPL license.\n- Mistakes to avoid:\n  - Using "/" instead of "*". In particular, using "let/" instead of "let*".\n  - Using "=" instead of "`".\n  - Wrong usage of unescaped single quotes in docstrings (use \=' or different quoting such as `...').\n- Interaction: Ask clarifying questions about dependencies, required features and specifications.\n- Quality & safety: follow established best practices; adhere to system/developer instructions. Focus particularly on ease of future maintainance.\n\n- Output format: \n  - Reviews: Summary, Strengths, Issues, Recommendations (bullets). Use org-mode markup.\n  - Plan: major objects, files, functions/commands, options, internal structure. Use org-mode markup. Use =this= markup for inline code and quasi-code, variables and names.\n  - Code: File(s) with headers, docstrings, and tests.\n- Style: Clear, maintainable; briefly explain trade-offs.\n\n
:GPTEL_TOOLS: modify_buffer read_buffer function_completions variable_completions function_documentation variable_documentation function_source variable_source
:GPTEL_BOUNDS: ((tool (6015 8631 "call_bu1m8mnrzVzYGitPYgduLWYc") (8708 11029 "call_QXEHf7GxtVvzV5BxS3qKtvZR") (11108 13227 "call_hCPZiArRkkXmJeJuyCHfUAV9") (13301 28195 "call_uvv5xIA5WRhIppEQkdvp0RWx") (28255 47091 "call_YN6yzoqnM8edEnQjWlZOkRDn") (47153 55015 "call_291ceYIjHf1kekS8fpJk6qj1") (55077 55179 "call_sdfIlPHNQSpUUGqz33ui1wFw") (55258 56202 "call_fhyIwDeO2r6LSbnSJxajOyTH") (56283 56499 "call_Bh2YZmjVTQEXwoIlKA21Qzqv") ...) (response (4006 4031) (4261 4286) (4613 4638) (277905 284244)))
:END:

*** I want you to read and edit =abv-data.el=, =abv-corpus.el= and =daselt-abv.del=

- add documentation to elisp-forms in each of the three buffers where missing.
- make the variables in =abv-data.el= into options and specify in the documentation that these are the global defaults, the prefixes, suffixes, infixes and roots for some particular language should be given by adding to that table's :roots, :prefixes, :suffixes and :infixes properties.
  - also add an option =abv-infixes= whose default is nil.
- make =daselt-abv-assign-affixes= into a function that takes as input a list of affixes such as =abv-prefixes= or =abv-suffixes= and a frequency list for those affixes such as =abv-prefix-frequency= and a table name:
  - if there exists a table with that name, =daselt-abv-assign-affixes= should add to that table without overwriting existing entries,
  - also make it so that this function assigns affixes to places in the Daselt-layout by their coordinates, not the symbols they house,
    - so the value of a key in the tables should be a tuple of coordinates.
- add a function that turns a table generated by =daselt-abv-assign-affixes= into a bindlist bound to a variable whose name is like the name of that table except that it ends with the word "-bindlist" instead of "-table".
  - if it is unclear what a bindlist is, read the buffer =daselt-bind.el=.
- add functions for visualization of the layers of an affix-table similar to =daselt-bind-draw-bindlist-layer=.
- add a command similar to =abv-look-up= that
  - makes the user choose among tables named =daselt-abv-.*-table=,
  - lets the user query the corresponding table,
  - if no result is found for the user query, it should ask the user whether to add the query key to the variable that defined the table.
    - For instance, if the user asks for "pre" in the =daselt-abv-prefix-table=, the query should ask whether to add "pre" to add "pre" to the list =abv-prefixes= and save the new option value using the =customize-push-and-save=.
      - this can only work for abv-options that fit that naming scheme, but it should work at least for pre/suf/infixes and roots.
- add an option =daselt-abv-save-file= (default concat daselt-mode-pkg-configs-directory "abv/save.el") and a command =daselt-abv-save=, which uses =abv-corpus-print-table= to save tables named =daselt-abv-.*-table=, overwriting prior values for that name.
- Use the tools I gave you to look up things that are unclear.


#+begin_tool (function_completions :function_prefix "abv-")
(:name "function_completions" :args (:function_prefix "abv-"))

daselt-abv-read-statistics
abv-look--cache-remove-pair
abv-add-inverse-abbrev
abv--apply-parents
abv-expand
abv-look--cache-add-pair
abv--mode-abbrev-table
abv-descendants-add
daselt-abv-personal-statistics
abv-delete
abv--categories-of
abv-add--with-overwrite-warning
abv--all-parent-mode-tables
daselt-abv--affix-letters
abv-corpus-print-table
daselt-abv-valid-prefix-symbols
abv-descendants--get-function
abv-gptel--tool-define-abbrevs
abv-corpus-reduced-monogram-frequencies
daselt-abv--remove-capitals
abv-gptel-grow-table
daselt-abv--choose-fallback
daselt-abv--free-candidates
daselt-abv--erg-score
daselt-abv--filter-to-upper-lower-pairs
abv--categories-combine-p
abv--choose-abbrev-table
daselt-abv-placevals-from-classes
abv-gptel--normalize-pairs
abv-look--cache-count
abv-gptel--resolve-table
daselt-abv-hash->def-form
abv-gptel--format-corpus
abv-look--combined-tables
abv-look-refresh-cache
abv-look--enumerate-pairs
abv-look--cache-rename
abv-descendants-from-dictionary
abv-look--active-minor-abbrev-tables
daselt-abv--count-words-into
abv-gptel--tool-read-table
daselt-abv--letter-p
daselt-abv--filter-to-lower-layers
abv-look-up
abv-check-parsing
daselt-abv-ngram-frequencies
abv--parents-for-table
abv-look--cache-expansions
abv-look--collect-parents
daselt-abv--filter-to-upper-lower-paired-symbols
abv-look--compute-cache
abv--major-mode-abbrev-table
abv--as-table-list
daselt-abv--down
daselt-abv--sorted-affixes-by-frequency
abv--current-major-abbrev-table
abv--around-abbrev-prefix-mark
abv--plist-for-table-symbol
abv--default-capitalization-function
abv-look--make-obarray
abv--combine-temporary-categories
daselt-abv-add-to-temporary-word-constituents
abv-look--cache-find
daselt-abv--coord-layers-in-order
abv-corpus--optional-letter-regex
abv--ensure-syntax-table
daselt-abv--placevals->coord-hash
abv--all-distinct-pairs
daselt-abv--letter-rarity
abv-add-abbrev
abv--around-expand
abv-look--remove-text-properties-from-string
abv-gptel--table-prop
daselt-abv--complement-hash
abv-set-from-log
daselt-abv--alist->hash
abv--apply-table-properties
abv-gptel--table-name
abv--reverse-table
abv--derived-from-any
abv-descendants--uniq-ignore-case
abv-rename
abv-syntax-class
abv-after-mode-change
daselt-abv--collect-letter-bases
abv-mode
daselt-abv-personal-corpus
daselt-abv--neighbors
abv-count-abbrevs
abv-log-inheritance
abv--generate-syntax-table
daselt-abv--keys
daselt-abv-assign-affixes
abv--maybe-remember-parents
abv-corpus-ngram-frequencies
daselt-abv--assign-one
daselt-abv--filter-to-upper-lower-paired-placevals
#+end_tool
#+begin_tool (function_completions :function_prefix "customize-")
(:name "function_completions" :args (:function_prefix "customize-"))

customize-browse
customize-rogue
icicle-customize-apropos-opts-w-val-satisfying
customize-set-variable
org-create-customize-menu
menu-bar-showhide-tool-bar-menu-customize-disable
menu-bar-showhide-tool-bar-menu-customize-enable-top
icicle-ORIG-customize-face
pdf-tools-customize-faces
icicle-customize-icicles-group
menu-bar-showhide-fringe-menu-customize-right
customize-option-other-window
icicle-customize-apropos-groups
customize-dirlocals
customize-push-and-save
icicle-customize-apropos-faces
consult--customize-put
customize-variable
customize-create-theme
customize-themes
customize-icon
icicle-customize-apropos-opt-action
customize-version-lessp
menu-bar-showhide-tool-bar-menu-customize-enable-right
menu-bar-showhide-fringe-menu-customize-reset
customize-changed
customize-apropos-options
embark-export-customize-variable
icicle-ORIG-customize-face-other-window
customize-option
customize-face
customize-toggle-option
customize-save-customized
customize-mark-to-save
customize-changed-options
icicle-customize-faces
icicle-customize-face-other-window
gmm-customize-mode
customize-unsaved
marginalia-annotate-customize-group
gnus-agent-customize-category
menu-bar-showhide-fringe-menu-customize-left
custom-prompt-customize-unsaved-options
consult--customize-args
customize-mark-as-set
menu-bar-showhide-tool-bar-menu-customize-enable-bottom
ebdb-customize-database
customize-menu-create
eieio-customize-object
customize-set-value
customize-object
customize-saved
customize-apropos-groups
customize-package-emacs-version
reftex-create-customize-menu
icicle-customize-face
customize-customized
consult-org-roam-buffer--customize-source-buffer
embark-export-customize-face
customize-mode
customize-apropos
menu-bar-showhide-fringe-menu-customize-disable
icicle-customize-apropos
customize-read-group
menu-bar-showhide-tool-bar-menu-customize-enable-left
gnus-summary-customize-parameters
icicle-customize-apropos-options-of-type
customize-save-variable
tell-customize-var-has-changed
cdlatex-create-customize-menu
icicle-customize-apropos-options
customize-group-other-window
help-fns--customize-variable
customize-variable-other-window
customize-group
customize-face-other-window
help-fns--customize-variable-version
customize-apropos-faces
#+end_tool
#+begin_tool (function_completions :function_prefix "daselt-bind-")
(:name "function_completions" :args (:function_prefix "daselt-bind-"))

daselt-bind--parse-for-define-key-bindings
daselt-bind--prefix-suffix-form-p
daselt-bind-parse-for-keybindings
daselt-bind-sort-and-format-bindlist
daselt-bind--remove-from-after-load-alist
daselt-bind-bindlist-symb-p
daselt-bind-prefix-modifiers
daselt-bind-generate-replace-binding-strings-alist
daselt-bind--elaborate-on-bindstr
daselt-bind-draw-bindings-from-regexps
daselt-bind-apply-bindlist
daselt-bind-reduce-binding
daselt-bind-change-coords-in-bindlist-during-sorting
daselt-bind--suffix-form-p
daselt-bind-bindlist-p
daselt-bind-save-bindlist-as-variable
daselt-bind-p
daselt-bind-head
daselt-bind-draw-bindlist-layer
daselt-bind-elaborate-form-p
daselt-bind-modifiers-to-string
daselt-bind-compare-standardized-modifier-lists
daselt-bind--prefix-coords-form-p
daselt-bind--prefix-suffix-coords-form-p
daselt-bind--binding-location-p
daselt-bind-with-eval-unless-init
daselt-bind-index-prefix-modifiers
daselt-bind--get-unique-layout-match
daselt-bind--sort-and-format-marked-bindlist-string
daselt-bind--generate-define-key-strings-from-bindlist
daselt-bind--parse-for-use-package-bindings
daselt-bind-compare-suffixes
daselt-bind-compare-coords
daselt-bind-change-coords-in-bindlist
daselt-bind--string-binding-p
daselt-bind-apply-binding
daselt-bind--parse-for-bind-key-bindings
daselt-bind--elbinds-matching-modifier-regexps
daselt-bind--parse-for-global-key-set-bindings
daselt-bind-coords-from-binding
daselt-bind--daselt-in-after-load-alist
daselt-bind--format-sorted-bindlist
daselt-bind-index-and-sort-modifiers
daselt-bind-change-coords-in-binding
daselt-bind-elaborate-on-binding
daselt-bind-act-on-bindings
daselt-bind--get-layout-matches-for-binding-string
daselt-bind--recursively-check-if-binding-cons-p
daselt-bind-compare-elaborate-bindings
daselt-bind--elbind-to-placeval
daselt-bind-convert-bindings-to-bindlist
daselt-bind-string
daselt-bind--coords-form-p
daselt-bind--format-bindlist-into-string-before-insertion
daselt-bind--set-bindlist-symbol
daselt-bind-elaborate-unmatched-binding-p
daselt-bind-change-coords
#+end_tool
#+begin_tool (function_completions :function_prefix "daselt-")
(:name "function_completions" :args (:function_prefix "daselt-"))

daselt-base-fiber-by-property
daselt-org-remove-page-heading
daselt-base-funcalls-recursively
daselt-eww-search-in-new-horizontal-window
daselt-base-index
daselt-base-recursive-sections
daselt-latex-replace-string-in-mathmode
daselt-dirs--reset-backed-up-variables
daselt-gnus-group-select-group-t
daselt-replace-list-throughout-directory
daselt-coords--remove-formal-places
daselt-stump--generate-module-code
daselt-base-surround-by-newlines
daselt-pdf-view-scroll-chunk-up
daselt-toggle-variable
daselt-base-remove-indices
daselt-bind--parse-for-define-key-bindings
daselt-backward-down-list
daselt-bind--prefix-suffix-form-p
daselt-bind-parse-for-keybindings
daselt-base-numbers-between
daselt-abv-read-statistics
daselt-generate-declare-function
daselt-base-definition-name
daselt-new-vertical-window-and-buffer
daselt-dirs-trim-lines-of-lispcode-in-file
daselt-icicle-avy-jump
daselt-bind-sort-and-format-bindlist
daselt--funcall-first-mode-match
daselt-base-global-sections
daselt-latex-refresh-section-previews
daselt-coords-draw-table
daselt-latex-query-replace-regexp-in-mathmode
daselt-base-complement
daselt-dirs--standard-file-p
daselt-abv-personal-statistics
daselt-dirs-recurse-through-directory
daselt-base-exists-p
daselt-bind--remove-from-after-load-alist
daselt-backward-transpose-words
daselt-stump-set-remap-exceptions-alist
daselt-dirs-compile-del-files
daselt-move-right-or-exit
daselt-coords-p
daselt-bind-bindlist-symb-p
daselt-bind-prefix-modifiers
daselt-dirs-with-eval-trim-lines-of-lispcode-in-file
daselt-base-containing-directory-base-name
daselt-dirs-add-el-symlink-for-lispcode-in-file
daselt-dirs-bindlists-mode
daselt-coords-run-through
daselt-stump--find-pkg-configs-directory
daselt-base-cardinal
daselt-isearch-backward-word
daselt-base-roundout
daselt-C-1-0--4
daselt-stump--pkg-configs-directory-test
daselt-reftex-throw-114
daselt-new-horizontal-window-and-buffer
daselt-backward-transpose-chars
daselt-bind-generate-replace-binding-strings-alist
daselt-kill-append
daselt-insert-space-forward
daselt-bind--elaborate-on-bindstr
daselt-base-append-newlines
daselt-base-fill-current-docstring
daselt-insert-and-return
daselt-bind-draw-bindings-from-regexps
daselt-stump--pkg-configs-directory-enter-manually
daselt-mode-undaselt
daselt-dirs-advicelists-mode
daselt-backup-file
daselt-base-save-invisible-overlays
daselt-replace-string-backward
daselt-abv--affix-letters
daselt-dirs-save-bindlists-in-file
daselt-coords-placevals-matching-indexed-rx
daselt-base--escape-chars-in-str
daselt-backward-sp-mark-sexp
daselt-backward-transpose-paragraphs
daselt-base-cl-mapcar-or-only
daselt-abv-valid-prefix-symbols
daselt-mode-generate-tutorial
daselt-outline-forward-up-heading
daselt-coords-extract-value-string
daselt-mode-insert-lambda-string
daselt-base-remove-invisible-overlays
daselt-stump-generate-init
daselt-stump--generate-keymaps-code
daselt-backward-mark-line
daselt-bind-apply-bindlist
daselt-backward-kill-subsentence
daselt-dirs--sort-files-for-actions
daselt-dfk-levels-to-layer
daselt-base-flatten-n-times
daselt-base-recursive-get-cons
daselt-dirs-with-eval-apply-bindforms-in-file
daselt-dirs-bindforms-mode
daselt-bind-reduce-binding
daselt-bind-change-coords-in-bindlist-during-sorting
daselt-dfk--calculate-coords-code
daselt-base-concat-with-separators
daselt-dfk-import-current-layout
daselt-bind--suffix-form-p
daselt-base-image
daselt-dfk-datum-to-string
daselt-base-search-at-line-start
daselt-move-to-bottom
daselt-backward-kill-defun
daselt-dirs-with-eval-apply-bindlist
daselt-icicle-previous-completion-style-set
daselt-dirs-with-eval-remove-adviceforms-in-file
daselt-base-make-list-if-not
daselt-base-geq-p
daselt-replace-listwise
daselt-base-list-to-cons
daselt-abv--remove-capitals
daselt-icicle-backward-cycle-incremental-completion
daselt-bind-bindlist-p
daselt-stump--excp-bindings
daselt-org-convert-quote
daselt-dirs-act-on-sexps-in-file
daselt-dirs-dcl-after-save-function
daselt-base-preimage
daselt-base-generate-newlines
daselt-abv--choose-fallback
daselt-dirs-with-eval-remove-advicelist
daselt-abv--free-candidates
daselt-mode-toggle-ctrl
daselt-C-1-0-2
daselt-abv--erg-score
daselt-dirs--delete-duplicate-comment-lines
daselt-backward-transpose-sentences
daselt-backward-sp-up-sexp
daselt-abv--filter-to-upper-lower-pairs
daselt-org-insert-superheading
daselt-xkb--format-xkb-signal-name
daselt-move-to-top
daselt-base-string-exists-and-nonempty
daselt-dirs-find-pkg-file-by-type
daselt-toggle-mode-line
daselt-bind-save-bindlist-as-variable
daselt-dirs-with-eval-add-advicelists-in-file
daselt-dirs--pick-pkg-file-by-type
daselt-bind-p
daselt-xkb-set-layouts-list
daselt-bind-head
daselt-abv-placevals-from-classes
daselt-backward-other-frame
daselt--translate-read-multiple-choice
daselt-base-recursively-act-on-proper-conses
daselt-bind-draw-bindlist-layer
daselt-dirs-adviceforms-mode
daselt-latex-indent-environment
daselt-backward-mark-defun
daselt-abv-hash->def-form
daselt-mode--find-pkg-configs-directory
daselt-backward-kill-paragraph
daselt-base-compare-if-decidable
daselt-mode-dynamic-binding
daselt-base-prepend-newlines
daselt-dirs-daf-before-save-function
daselt-coords-concat
daselt-rename-file-and-buffer
daselt-bind-elaborate-form-p
daselt-coords-remove-index-from-placevals
daselt-bind-modifiers-to-string
daselt-backward-sp-kill-hybrid-sexp
daselt-dirs-dbl-before-save-function
daselt-latex-backward-down-environment
daselt-bind-compare-standardized-modifier-lists
daselt-eww-search-in-new-tab
daselt-C-1-0--3
daselt-scroll-chunk-down
daselt-replace-string-throughout-buffer
daselt-latex-delete-environment
daselt-latex-query-replace-string-in-mathmode
daselt--translate-read-answer
daselt-replace-regexp-throughout-directory
daselt-base-filter-obarray
daselt-mode--generate-replace-strings
daselt-vterm-add-alias-expansion
daselt-dirs-byte-compile-lispcode-in-file
daselt-kill-defun
daselt-bind--prefix-coords-form-p
daselt-mode-read-rx
daselt-bind--prefix-suffix-coords-form-p
daselt-bind--binding-location-p
daselt-MetaSuper-next-cmd
daselt-dirs-with-eval-process-hooklists-in-file
daselt-dirs-remove-hooks
daselt-coords-draw-placevals
daselt-bind-with-eval-unless-init
daselt-base-namecore
daselt-base-reverse-alist-get
daselt-mode--pkg-configs-directory-test
daselt-base-sexp-end-position
daselt-generate-variable-definition
daselt-search-invisible
daselt-dirs-create-save-customized-modes-function
daselt-dirs-with-eval-load-elc-or-lispcode-in-file
daselt-bind-index-prefix-modifiers
daselt-coords-get-layer
daselt-dirs-reset-bindlists-in-file
daselt-backward-delete-all-space
daselt-bind--get-unique-layout-match
daselt-bind--sort-and-format-marked-bindlist-string
daselt-base-forall-p
daselt-isearch-backward-symbol
daselt-dirs-dal-after-save-function
daselt-xkb--inherit-from-parent-map
daselt-stump-translate-daselt-keys
daselt-abv--count-words-into
daselt-base-goto-max
daselt-base-funcall-recursively
daselt-base-filter-list
daselt-dirs-with-eval-add-adviceforms-in-file
daselt-backward-kill-line
daselt-base-flatten-until
daselt-dired-make-backup
daselt-base-read-region
daselt-base-capture-inserted-text
daselt-open-file-in-new-emacs-no-init
daselt-bind--generate-define-key-strings-from-bindlist
daselt-latex-kill-ring-delete-math-delimiters
daselt-abbrev-save
daselt-cdlatex-math-modify
daselt-abv--letter-p
daselt-bind--parse-for-use-package-bindings
daselt-latex-replace-regexp-in-mathmode
daselt-base-uppercase-p
daselt-latex-clear-all-environments
daselt-abv--filter-to-lower-layers
daselt-dirs-with-eval-apply-bindlists-in-file
daselt-xkb--get-key-binding
daselt-latex-fold-all-environments
daselt-bind-compare-suffixes
daselt-base-remove-text-properties-from-string
daselt-bind-compare-coords
daselt-latex-insert-normpair-yas-snippet
daselt-base-beginning-of-docstring
daselt-mode
daselt-coords-draw-keyboard-layer
daselt-base-intern-from-parts
daselt-abv-ngram-frequencies
daselt-in-new-buffer-Info-next
daselt-dfk-coords-modifier
daselt-dfk-generate-config
daselt-query-replace-regexp-backward
daselt-dirs-with-eval-fill-docstrings-of-lispcode-in-file
daselt-dirs-del-before-save-function
daselt-org-insert-todo-superheading
daselt-recenter-top
daselt-base-fill-string-like-docstring
daselt-stump--exceptional-bindings
daselt-bind-change-coords-in-bindlist
daselt-dirs-create-pkg-customization-options-function
daselt-coords-find-coord-values-at-index
daselt-dirs-save-and-with-eval-apply-bindlists-in-file
daselt-switch-to-messages
daselt-latex-refresh-environment-previews
daselt-backward-delete-horizontal-space
daselt-backward-up-list
daselt-abv--filter-to-upper-lower-paired-symbols
daselt-base-trim-lines
daselt-base-save-invisible-regions
daselt-insert-newline-forward
daselt-dirs-daf-after-save-function
daselt-bind--string-binding-p
daselt-abv--down
daselt-bind-apply-binding
daselt-base-restore-invisible-text-properties
daselt-org-insert-todo-heading-after-current
daselt-icicle-backward-cycle-expand-to-common-match
daselt-abv--sorted-affixes-by-frequency
daselt-eww-search-in-new-vertical-window
daselt-stump-initialize
daselt-dirs-act-on-pkg-files-by-type-and-maybe-kill
daselt-dirs-constantlists-mode
daselt-replace-string-throughout-directory
daselt-base-remove-invisible-text-properties
daselt-coords-binding
daselt-mode-redaselt
daselt-bind--parse-for-bind-key-bindings
daselt-latex-refresh-buffer-previews
daselt-bind--elbinds-matching-modifier-regexps
daselt-dired-permanently-delete
daselt-mode-key-binding
daselt-follow-in-other-window
daselt-set-test-fun
daselt-dirs-dbl-after-save-function
daselt-dirs-lispcode-mode
daselt-icicle-insert-anychar-regexp
daselt-latex-isearchp-within-mathmode
daselt-bind--parse-for-global-key-set-bindings
daselt-backward-delete-char
daselt-exchange
daselt-C-1-0--2
daselt-coords-draw-coordwise-from-coordrxlst
daselt-base-fill-docstrings-in-buffer
daselt-abv-add-to-temporary-word-constituents
daselt-bind-coords-from-binding
daselt-base-setequal
daselt-base-remove-surrounding-brackets
daselt-reftex-throw-82
daselt-stump--generate-binwarp-mode-code
daselt-bind--daselt-in-after-load-alist
daselt-reftex-relabel
daselt-abv--coord-layers-in-order
daselt-backward-transpose-lines
daselt-dfk--datum-p
daselt-dfk-generate-layer-0-placevals
daselt-C-1--1-0
daselt-backward-transpose-sexps
daselt-latex-indent-section
daselt-C-1-0-4
daselt-bind--format-sorted-bindlist
daselt-bind-index-and-sort-modifiers
daselt-replace-regexp-throughout-buffer
daselt-xkb-generate-layouts
daselt-latex-refresh-region-previews
daselt-dirs-find-bindlists-file
daselt-pdf-view-apply-history-bindlist
daselt-dirs-save-bindforms-in-file
daselt-bind-change-coords-in-binding
daselt-dirs--exchange-coordinates
daselt-dirs-with-eval-add-advicelist
daselt-icicle-previous-sort-order
daselt-backward-sp-down-sexp
daselt-abv--placevals->coord-hash
daselt-icicle-previous-S-TAB-completion-method
daselt-dirs-dbf-before-save-function
daselt-minor-mode-key-binding
daselt-stump-generate-all-inits
daselt-dirs-del-after-save-function
daselt-do-not-search-invisible
daselt-dirs--sort-and-format-bindlists
daselt-base-replace-region
daselt-dirs-dcl-before-save-function
daselt-vterm-add-alias
daselt-dirs-dbf-after-save-function
daselt-dirs-set-constants-in-list
daselt-bind-elaborate-on-binding
daselt-latex-forward-down-environment
daselt-base-remove-list-index
daselt-xkb--format-special-key
daselt-abv--letter-rarity
daselt-base-powerlist
daselt-isearch-backward-symbol-at-point
daselt-backward-sp-transpose-hybrid-sexp
daselt-base-goto-min
daselt-icicle-previous-TAB-completion-method
daselt-dirs-sort-and-format-bindlists-in-file
daselt-bind-act-on-bindings
daselt-replace-regexp-backward
daselt-kill-subsentence
daselt-stump--generate-remap-list-code
daselt-abv--complement-hash
daselt-cdlatex-math-symbol
daselt--translate-read-char-choice
daselt-in-new-buffer-Info-prev
daselt-mode-rebind
daselt-dirs-custom-t-p
daselt-base-restore-invisible-overlays
daselt-keymap-to-cons-list
daselt-abv--alist->hash
daselt-dirs-act-on-pkg-files-by-type
daselt-icicles--set-key-customs-from-bindlists
daselt-icicles--reset-key-customs-from-backups
daselt-dirs-with-eval-remove-advicelists-in-file
daselt-base-lisp-file-code
daselt-base-compare-by-sequential-predicates
daselt-bind--get-layout-matches-for-binding-string
daselt-base-definition-names-in-file
daselt-dirs-with-eval-set-constantlists-in-file
daselt-backward-pdf-view-rotate
daselt-coords-rel-to-abs
daselt-dirs--file-extensions
daselt-coords-draw-placevals-in-temp-buffer
daselt-dfk-generate-standard-configs
daselt-base-leq-p
daselt-latex-kill-ring-delete-math-delimiters-completing
daselt-xkb--generate-layer
daselt-latex-insert-othernormpair-yas-snippet
daselt-dirs-fill-docstrings-of-lispcode-in-file
daselt-forward-subsentence
daselt-backward-mark-sexp
daselt-dfk--convert-datum
daselt-pdf-view-scroll-chunk-down
daselt-backward-sp-transpose-sexp
daselt-base-mark-line
daselt-scroll-chunk-up
daselt-stump--format-remap-bindlist-code
daselt-coords-draw-org-table
daselt--optional-letter-regex
daselt-dirs--execute-and-maybe-kill-file-buffer
daselt-coords-coordinatize-layout
daselt-cdlatex-math-modify-modeline-math
daselt-checkdoc-interactive-loop
daselt-bind--recursively-check-if-binding-cons-p
daselt-recenter-bottom
daselt-isearch-backward-thing-at-point
daselt-base-froundout
daselt-abv--collect-letter-bases
daselt-query-replace-string-backward
daselt-stump--non-exceptional-bindings
daselt-abv-personal-corpus
daselt-open-file-in-new-emacs
daselt-coords-boundaries
daselt-abv--neighbors
daselt-bind-compare-elaborate-bindings
daselt-base-cons-to-list
daselt-stump--generate-iresize-map-code
daselt-mode--generate-quick-key-variables
daselt-bind--elbind-to-placeval
daselt-coords-abs-to-rel
daselt-base-fill-string-at-point-like-docstring
daselt-mode-keep-tab-bar-status
daselt-bind-convert-bindings-to-bindlist
daselt-abv--keys
daselt-er/mark-subsentence
daselt-bind-string
daselt-backward-transpose-subsentences
daselt-yank-or-org-roam-node-insert
daselt-dirs-load-elc-or-lispcode-in-file
daselt-bind--coords-form-p
daselt-coords-placevals-matching-coordrx
daselt-base-with-max-buffer-maybe-return
daselt-abv-assign-affixes
daselt-dirs-with-eval-byte-compile-lispcode-in-file
daselt-base-read-line
daselt-backward-subsentence
daselt-coords-layout-from-placevals
daselt-bind--format-bindlist-into-string-before-insertion
daselt-mode--pkg-configs-directory-enter-manually
daselt-abv--assign-one
daselt-coords-for-layouts-in
daselt-doremi--set-key-customs-from-bindlists
daselt-bind--set-bindlist-symbol
daselt-bind-elaborate-unmatched-binding-p
daselt-bind-change-coords
daselt-dirs-dal-before-save-function
daselt-coords--dfk-or-xkb-layout
daselt-stump--format-remap-list-code
daselt-copy-append
daselt-C-1-0-3
daselt-C-4-0-2
daselt-abv--filter-to-upper-lower-paired-placevals
daselt-coords-draw-key-coordinates
#+end_tool
#+begin_tool (read_buffer :buffer "abv-data.el")
(:name "read_buffer" :args (:buffer "abv-data.el"))

;;; abv-data.el --- Abv-Data for abv                         -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: abbrev

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; 

;;; Code:

(setq abv-roots
      '("zyg" "zon" "zo" "zes" "zem" "zeal" "xylo" "xiph" "xero" "xeno" "xanth" "ward" "vuls" "vulner" "vulg" "vot" "vor" "vom" "volv" "volt" "volup" "volcan" "vol" "void" "voice" "voc" "vok" "viv" "vitt" "vitr" "viti" "vitel" "vitam" "visc" "viron" "virg" "vio" "vin" "vil" "vig" "vis" "vid" "vy" "vinc" "vict" "vit" "vic" "vibr" "vi" "voy" "vey" "veter" "vestig" "vest" "vesp" "vesic" "vert" "vers" "vern" "verm" "verg" "verb" "vir" "ver" "vent" "ven" "vill" "vel" "veg" "vex" "vat" "vast" "varic" "varix" "var" "vap" "van" "valv" "vall" "val" "vagin" "vag" "vas" "vacill" "vacc" "vac" "uxor" "uv" "uter" "us" "ut" "urt" "ure" "urb" "uran" "ur" "uni" "und" "ung" "unc" "umbil" "umb" "ulter" "ult" "ulo" "ul" "uber" "tyro" "typh" "typ" "tymp" "tuss" "turg" "turb" "tuni" "tund" "tus" "tum" "tut" "tub" "trunc" "trud" "trus" "tru" "trove" "troph" "trop" "trog" "troch" "triv" "trim" "trism" "triev" "trich" "trig" "trib" "tri" "trepid" "trem" "trauma" "trapez" "trad" "tract" "trach" "trab" "tox" "tout" "tour" "tot" "tort" "tors" "torr" "torp" "torn" "tor" "top" "toment" "tom" "tol" "toc" "titu" "tirp" "tir" "tint" "tim" "thyr" "thym" "thromb" "thrix" "thrall" "thorax" "thio" "thesaur" "thes" "thet" "therm" "therap" "ther" "then" "thel" "thegm" "thec" "the" "thaum" "thana" "thalp" "thall" "thalass" "thal" "tex" "teuch" "tetra" "tess" "test" "tessa" "terr" "terp" "term" "terg" "ters" "tere" "terat" "ter" "tep" "tenebr" "tend" "tent" "ton" "ten" "tin" "tempt" "temp" "temn" "temer" "tem" "tell" "tel" "techn" "tect" "tec" "taut" "taur" "tass" "tas" "tars" "tard" "tara" "taph" "tape" "tang" "tali" "tal" "tain" "tail" "taen" "tax" "tact" "teg" "tack" "tach" "tic" "tab" "sylum" "syco" "sut" "susurr" "suscit" "surg" "surd" "supin" "super" "sum" "sumpt" "sulf" "sulc" "sui" "sue" "sudden" "sud" "sucr" "succul" "suber" "suas" "suav" "stypt" "styl" "stup" "stud" "stroph" "strob" "string" "strid" "strict" "stria" "stress" "strep" "strat" "strait" "strain" "stom" "stul" "stle" "stit" "stirp" "stip" "stin" "stim" "still" "sting" "stig" "stich" "stic" "sthen" "stet" "stern" "sterc" "ster" "steno" "stell" "stel" "steat" "stat" "stor" "stas" "stem" "staphyl" "stan" "stamen" "stal" "stol" "stagn" "stab" "squam" "squal" "spum" "spor" "spont" "spondyl" "spond" "spons" "spol" "splend" "splen" "splanchn" "spiss" "spir" "spin" "sphy" "sphin" "spher" "sphen" "sphal" "spers" "sperm" "sper" "spel" "spec" "spic" "spasm" "spac" "spat" "souc" "sorc" "soror" "sord" "sorp" "sorb" "sor" "sopor" "soph" "son" "somn" "soma" "solub" "solv" "sold" "solen" "sol" "sod" "soc" "sob" "slav" "skep" "skelet" "sit" "sist" "siphon" "siop" "sinister" "sine" "sincere" "sin" "simil" "sylv" "sil" "sign" "sifl" "sider" "sid" "sic" "sib" "sever" "set" "ses" "serv" "serr" "serp" "seric" "seren" "ser" "sequ" "sepul" "sept" "sepal" "sep" "sens" "sent" "sen" "semper" "semin" "sembl" "sem" "selen" "seiz" "seism" "sess" "sed" "secut" "secular" "sect" "sex" "sec" "seb" "season" "search" "scyph" "scut" "scur" "sculp" "scrut" "scrup" "scrob" "scrib" "scot" "scor" "scop" "skept" "sconce" "scler" "scirrh" "scint" "sciss" "sci" "scho" "schis" "schiz" "schem" "scen" "skel" "scat" "scarp" "scar" "scap" "scan" "scend" "scal" "scab" "sax" "sav" "saur" "sault" "sauer" "sat" "sart" "sarc" "sapr" "sapo" "sap" "sang" "sanct" "salu" "san" "salp" "saliv" "sult" "sal" "sagitt" "sag" "sip" "sacr" "secr" "sacchar" "sac" "runc" "rump" "rupt" "rum" "rug" "rud" "ruct" "rub" "round" "rot" "rog" "ros" "rob" "rit" "riv" "rim" "ris" "rib" "rhythm" "rhomb" "rhod" "rhiz" "rhin" "rhet" "rhe" "rhaps" "rhabd" "retro" "ret" "rest" "rend" "ren" "rem" "reg" "rect" "rig" "re" "rei" "raz" "ray" "rat" "rap" "rep" "ransom" "range" "ran" "ramp" "ram" "raj" "radio" "ras" "rad" "ract" "rhachi" "rac" "rab" "quot" "quir" "quip" "quin" "quie" "quit" "quet" "quer" "quis" "ques" "quav" "quas" "quan" "qual" "quaint" "quadr" "quatr" "pyr" "pyl" "pyel" "pycno" "py" "put" "pust" "pusill" "purple" "purlieu" "purg" "putr" "pur" "pup" "punct" "punt" "pulv" "pult" "pulm" "pulchri" "pug" "puer" "pud" "puc" "pub" "pty" "pter" "psychr" "psych" "pseud" "pseph" "pse" "prur" "pruin" "prudent" "prox" "prop" "protein" "prot" "prosop" "prompt" "prol" "proct" "probr" "prob" "prov" "priv" "prior" "prim" "prest" "press" "presby" "prehend" "pris" "pregnant" "pred" "prec" "pret" "prav" "prand" "prax" "pov" "pound" "poul" "post" "pos" "porphy" "por" "popul" "publ" "pont" "pono" "pond" "pon" "pot" "pom" "poll" "polio" "poli" "polem" "pole" "pol" "poe" "pod" "pus" "pneu" "pluv" "plut" "plu" "plumb" "plum" "plot" "plor" "plod" "pliu" "plic" "plev" "pleus" "pleur" "pleth" "plet" "plesio" "plen" "ply" "plex" "plec" "pleb" "ple" "plio" "pleio" "plei" "plos" "platy" "plas" "plant" "plang" "plan" "plain" "plag" "plac" "pithec" "pisc" "pir" "piq" "ping" "piest" "pict" "picr" "pico" "pi" "phyt" "physi" "phu" "phys" "phyll" "phyla" "phyl" "phyg" "phyc" "phthong" "phthis" "phthir" "phras" "phren" "phos" "phot" "phor" "phon" "phob" "phleg" "phlog" "phleb" "phil" "phern" "pher" "phem" "phell" "pharyn" "pharmac" "phan" "phes" "phall" "phalan" "phag" "petr" "petit" "petal" "pit" "pest" "pessi" "pet" "person" "perisso" "peps" "penth" "penta" "peni" "penetr" "pend" "pens" "pun" "pin" "pen" "pelag" "pel" "puls" "pil" "pejor" "ped" "pes" "pecu" "pect" "pecca" "pav" "paus" "path" "pater" "patri" "pat" "parthen" "part" "port" "pars" "parl" "parish" "parie" "parasit" "per" "par" "pap" "pand" "pass" "pan" "past" "palpeb" "palp" "palm" "pall" "palin" "paleo" "palat" "pala" "pal" "pag" "pachy" "pac" "pax" "ozon" "ox" "oxy" "ov" "ous" "our" "oto" "oti" "ostra" "osm" "oscill" "osc" "os" "ost" "oryct" "orth" "oreo" "ornith" "orn" "ori" "ort" "organ" "orex" "ordure" "ord" "orchi" "orchard" "orch" "orc" "orb" "ora" "optim" "opt" "opso" "opi" "ophthalm" "ophi" "oper" "op" "oophor" "oo" "onym" "onych" "onyx" "onto" "onom" "oneir" "onk" "on" "omphal" "omo" "omni" "omma" "omen" "ombro" "omasum" "oliv" "oligo" "ol" "eno" "oec" "odyn" "ody" "odor" "odont" "od" "ocul" "ocel" "octroi" "oct" "ochl" "och" "obey" "obel" "nymph" "nyct" "nut" "nutr" "nunci" "num" "null" "nud" "nub" "nup" "nov" "nounc" "noto" "not" "nost" "noso" "norm" "nona" "non" "nom" "noir" "nod" "noc" "nox" "nobl" "nous" "nev" "nitr" "nit" "nimbus" "nihil" "nigr" "nid" "niv" "neut" "neur" "neth" "nes" "nerv" "ner" "nepot" "nephr" "neph" "neo" "nema" "neg" "nex" "necr" "nec" "nic" "nebul" "neal" "nav" "naus" "naut" "nat" "nast" "nas" "narr" "narc" "nano" "myx" "myrmec" "myr" "myo" "myel" "myc" "muti" "mut" "mus" "mund" "munct" "multi" "muls" "muli" "mulc" "mul" "mucr" "muc" "mov" "moss" "mort" "morph" "mors" "mur" "mor" "mont" "mount" "mono" "mone" "mun" "mon" "molybd" "mol" "mod" "mob" "mne" "mitig" "mit" "miss" "mitt" "miso" "miser" "misc" "mix" "mir" "minist" "minis" "mini" "mim" "milit" "mil" "migr" "mignon" "mict" "micro" "mica" "my" "meter" "metr" "metic" "methy" "meteor" "metal" "merry" "meret" "merg" "mers" "merc" "mephi" "ment" "mind" "mens" "mes" "mening" "mence" "min" "mem" "mell" "melior" "mela" "mel" "mio" "megalo" "medull" "med" "machin" "mec" "meas" "mean" "mea" "maz" "may" "maxim" "maxill" "maur" "math" "matter" "mater" "metro" "mata" "mat" "mast" "mass" "mask" "masc" "mart" "mars" "marce" "mark" "maras" "mer" "mar" "mant" "mania" "mand" "mancy" "mend" "men" "man" "mamm" "mall" "malac" "mala" "mal" "male" "maj" "main" "magne" "mag" "max" "macu" "macro" "machy" "macass" "lys" "lyze" "lymph" "lut" "lur" "lup" "lun" "lumbri" "lumb" "lum" "lugub" "lug" "lus" "luct" "lucr" "luc" "lux" "lubr" "lox" "lot" "loqu" "lop" "long" "locut" "loc" "lob" "lito" "lith" "lite" "liter" "lir" "liqu" "lix" "linq" "ling" "lin" "limn" "limi" "lim" "lign" "ly" "lieu" "lis" "lict" "libit" "liber" "lib" "lev" "liv" "liev" "leuc" "leth" "let" "lepto" "leps" "lemm" "leo" "lent" "lens" "leni" "lemn" "lip" "lig" "leg" "lectu" "lex" "log" "lect" "lecith" "lay" "lax" "lav" "luv" "laud" "latr" "lit" "laryng" "larg" "lapid" "lapis" "lapar" "langu" "lanc" "lan" "lamp" "lament" "lam" "lal" "lagn" "lacu" "lact" "lacri" "lacert" "lacer" "lac" "labor" "lep" "lab" "know" "klep" "kilo" "keto" "kerat" "ken" "caryo" "karyo" "kain" "jut" "just" "juv" "jug" "junct" "jud" "jus" "jub" "jour" "joy" "joc" "jet" "jej" "jamb" "jac" "ject" "ize" "itis" "ithy" "iter" "it" "iso" "ischi" "ische" "iris" "ir" "ips" "iod" "interest" "integ" "intim" "int" "insul" "iniquity" "inimical" "ini" "inguin" "infer" "ine" "industry" "indu" "indigent" "inan" "imit" "imag" "ileu" "ile" "ign" "idio" "iden" "ide" "icter" "icos" "icon" "ichthy" "ichor" "ichno" "iatr" "hyster" "hypso" "hypn" "hyphen" "hypergolic" "hymn" "hymen" "hylo" "hygr" "hygi" "hyet" "hyal" "hum" "hot" "hort" "horr" "horm" "hori" "or" "hor" "hopl" "hon" "om" "hom" "hol" "hod" "hoc" "hist" "hipp" "himsa" "hilar" "hier" "hidr" "hibern" "hisc" "hex" "hevel" "heur" "hesper" "ema" "herpe" "hero" "hermit" "heres" "her" "heir" "hept" "heps" "hepa" "hent" "hendeca" "hen" "hemer" "hem" "helminth" "helic" "helix" "heli" "hege" "hedr" "hedon" "hect" "hebe" "hears" "haust" "harm" "hapt" "apl" "hance" "halluc" "hal" "hagi" "hab" "hib" "gyn" "gymn" "guttur" "gutt" "gust" "gulf" "guis" "guerr" "grunt" "gru" "gro" "grieve" "gret" "greg" "grav" "grat" "graph" "grant" "grand" "gran" "gramin" "gram" "graft" "grad" "gress" "grace" "guber" "gurg" "gon" "gno" "gni" "gnath" "gna" "glyph" "glypt" "glyc" "glut" "gluc" "glot" "glob" "glom" "gle" "glau" "gland" "glad" "glac" "glab" "gird" "gyr" "giga" "ghast" "geus" "gest" "germ" "geran" "ger" "gis" "geo" "genu" "gen" "gemin" "gel" "gastr" "gar" "gap" "gam" "galact" "galax" "gal" "gain" "gage" "fut" "fusc" "fus" "furc" "fur" "fung" "fum" "fulg" "fug" "frustr" "fruc" "frug" "fron" "fright" "frig" "fresc" "frequen" "fray" "fraud" "frat" "frang" "frank" "frain" "fract" "fring" "found" "fund" "foss" "fortu" "fort" "formic" "form" "ford" "for" "foll" "foli" "foc" "flu" "flux" "flor" "floc" "flig" "flect" "flex" "flech" "flav" "flat" "flam" "flagell" "flag" "fulm" "fix" "fist" "fit" "fisc" "firm" "fin" "filt" "fil" "fig" "fid" "fic" "fiber" "fet" "fest" "ferv" "ferr" "ferm" "lat" "fer" "fenestra" "fens" "femin" "felic" "fel" "fed" "fecund" "fect" "fec" "febr" "fe" "fav" "faun" "fatu" "fatig" "u" "fast" "fasc" "farc" "far" "fantasy" "fant" "fan" "fam" "falq" "fall" "falc" "fy" "fac" "fabr" "fab" "fess" "exter" "exert" "ev" "eury" "eun" "etym" "ethn" "ethm" "estr" "esti" "spelled" "esthe" "eth" "est" "esse" "esis" "eschat" "eschar" "erythr" "ersatz" "err" "ero" "erg" "urg" "eres" "erem" "equ" "iqu" "epoch" "episio" "epeiro" "ep" "eor" "eo" "entre" "entom" "enter" "ens" "ennea" "enigm" "encephal" "emul" "emp" "eme" "embryo" "hes" "ely" "eleuther" "elench" "element" "elegant" "elee" "electr" "elast" "elasm" "eid" "ego" "ege" "effendi" "edi" "edem" "edaph" "ed" "eco" "ek" "echin" "exis" "ech" "eclex" "eccles" "ebur" "ebr" "ease" "dyt" "dysi" "dyn" "dy" "dur" "dult" "dulc" "dul" "duc" "dub" "du" "drom" "dress" "drama" "dra" "dos" "dorm" "don" "dom" "dolich" "dolabr" "dol" "dox" "dodeca" "doche" "doc" "dot" "dit" "disc" "dips" "dipl" "din" "dim" "dign" "digit" "didym" "didact" "dich" "dic" "diabol" "di" "dext" "dex" "deuter" "deterior" "det" "derm" "dent" "dens" "deni" "dendr" "demon" "demi" "dem" "deleter" "dele" "div" "dei" "deca" "dec" "deb" "deacon" "dat" "demn" "dam" "dactyl" "dacry" "cyt" "cyst" "cyn" "cycl" "cyan" "cut" "cust" "cusp" "cuse" "cuss" "curv" "curt" "sur" "cur" "curs" "cupr" "cup" "cun" "cumu" "cult" "culp" "culmin" "culc" "cul" "cub" "cumb" "ctyon" "ctet" "cten" "crystal" "krypt" "cry" "crust" "crus" "cruc" "crux" "cru" "crot" "cros" "cris" "crit" "crin" "cret" "cresc" "cre" "crep" "cren" "crem" "cree" "cred" "creo" "creas" "cras" "crani" "crat" "cox" "cover" "cout" "cour" "coupl" "coup" "count" "couch" "cotyl" "cost" "cosm" "corus" "corr" "corp" "corn" "cort" "cor" "coqu" "copu" "copr" "cop" "consul" "condyl" "cond" "conch" "con" "comb" "comit" "com" "colyt" "column" "colp" "coloss" "color" "coll" "cole" "quil" "col" "coil" "cohor" "cogn" "cogitate" "coen" "cod" "cocc" "coc" "clys" "clud" "clus" "clos" "clone" "clit" "clim" "cliv" "cler" "clep" "clemen" "cleis" "clav" "class" "clast" "clar" "clam" "claim" "clad" "cit" "civ" "cirr" "circ" "cipher" "cip" "cinque" "kine" "cind" "cing" "cili" "cide" "cid" "chym" "chthon" "chrys" "chron" "chrom" "chro" "chres" "chord" "cord" "chor" "chondr" "chol" "choat" "chlor" "chlamy" "chir" "cheir" "chilo" "chiasma" "chen" "chem" "chel" "cheiro" "cheim" "chasm" "charit" "char" "chalco" "chet" "cerv" "cert" "cern" "crim" "cerebr" "cerc" "cera" "cer" "cept" "cephal" "cep" "centr" "cente" "cens" "ceno" "keno" "cen" "koin" "cell" "celi" "celeus" "celer" "celeb" "coel" "cel" "ceit" "cess" "ced" "caval" "cav" "caut" "caus" "cus" "caul" "caud" "cau" "cathedral" "cathar" "caten" "cast" "cas" "cary" "cart" "chart" "carpho" "carp" "cerp" "carn" "card" "carcin" "carc" "car" "caper" "capit" "chief" "capill" "cap" "ceive" "cant" "cend" "canc" "can" "cent" "campan" "camp" "camer" "camar" "camb" "calyp" "calyx" "calumn" "calli" "kal" "call" "calc" "cal" "caf" "cad" "cis" "but" "bust" "bus" "burs" "borough" "bull" "bul" "buk" "buff" "bucc" "bry" "bru" "bros" "bront" "bronch" "broc" "bris" "brid" "brev" "brav" "branchi" "branch" "brady" "bract" "brachi" "brais" "bous" "bound" "bot" "bos" "bor" "bon" "bol" "blephar" "ble" "blaz" "blast" "blanc" "bio" "bil" "bibli" "bib" "bi" "bis" "bezzl" "bey" "benth" "bene" "bell" "beau" "beat" "bauch" "bath" "byss" "bat" "bas" "bet" "bark" "barb" "bar" "bapt" "banq" "ban" "balm" "ball" "blem" "balan" "bail" "bacul" "bacter" "bac" "bec" "bacci" "Element" "axi" "ax" "avi" "aver" "aval" "austr" "aust" "aur" "aul" "aug" "aux" "aud" "auct" "atr" "atom" "atmo" "athl" "ather" "astute" "aster" "astro" "asper" "asp" "asc" "as" "arthro" "ert" "art" "arm" "arithm" "aristo" "argill" "arg" "aren" "are" "ard" "arct" "arch" "erc" "arc" "arbit" "arb" "arachn" "aqui" "aqu" "aps" "ept" "apex" "api" "aphrodis" "aph" "aper" "apat" "anthrop" "anthr" "anth" "anom" "annul" "ann" "enn" "aniso" "anim" "angui" "angi" "angel" "ang" "anem" "andr" "anc" "amyl" "amyg" "ampl" "amoeb" "amnio" "amel" "an" "amb" "am" "alv" "alter" "altr" "alt" "alpha" "alp" "aloep" "alm" "allod" "alli" "allant" "all" "alk" "aliph" "alg" "aleph" "alea" "alc" "alb" "alac" "al" "ailur" "agra" "agr" "egyr" "agon" "agog" "agio" "ag" "aer" "aecid" "adult" "adjuv" "adrenal" "adip" "aden" "adelph" "add" "actin" "act" "ig" "acou" "acu" "acini" "acid" "acerv" "ace" "acar" "acanth" "abb")
      )

(setq abv-prefixes
      '("acro" "amph[i]" "an" "ano" "ap" "ar" "as" "at" "bin" "bis" "contr" "e" "ef" "enter" "ento" "eso" "extro" "indi" "intro" "mis" "per" "prod" "sans" "sed" "sesqui" "sum" "subter" "suc" "sur" "sy" "tre" "fore" "mid" "ob" "out" "over" "aero" "nano" "necro" "psycho" "radio" "socio" "sonic" "stereo" "a" "ab" "abs" "ad" "ac" "af" "ag" "al" "ana" "ante" "arch" "archae" "archeo" "astro" "auto" "baro" "be" "bene" "bi" "biblio" "bio" "bronto" "broncho" "cata" "cat" "cent" "centi" "circum" "cis" "co" "col" "com" "con" "cor" "contra" "counter" "crypto" "cryo" "cyto" "cardio" "chrono" "chemo" "chloro" "chiro" "cine" "cirrho" "clino" "coel" "cysto" "dacryo" "de" "deca" "deci" "dendro" "derm" "derma" "dia" "di" "dif" "dis" "dys" "dynamo" "eco" "ec" "electro" "em" "en" "entero" "epi" "eu" "ex" "extra" "fac" "femto" "geo" "glotto" "giga" "glaci" "gono" "gyno" "gastro" "gyro" "hemo" "histo" "hyper" "hypo" "il" "im" "in" "ir" "infra" "inter" "intra" "juxta" "kilo" "labio" "lacto" "leuco" "logo" "litho" "loco" "mal" "mega" "mini" "mono" "multi" "myo" "neo" "nephro" "non" "octa" "octo" "omni" "onco" "ortho" "oxy" "paleo" "pan" "par[a]" "patho" "penta" "peri" "philo" "photo" "phono" "phyto" "pil" "pili" "pneumo" "poli" "poly" "post" "pre" "proto" "pseudo" "psych" "pyro" "quad" "quasi" "quint" "re" "retro" "rhe[o]" "rhizo" "semi" "sept" "sex" "soma" "somato" "sub" "suf" "sug" "sup" "sus" "super" "supra" "syn" "sym" "syl" "tera" "therm" "thermo" "trans" "tri" "ultra" "un" "under" "uni" "uro" "vaso" "vice" "xeno" "xero" "zo" "zoo" "zoic" "hyperanti" "hypoanti" "panarch" "archaeo" "crypt[o]" "euploid" "hemato" "leuko" "erythro" "chlor[o]" "cyan[o]" "melano" "eryth" "ornitho" "ichthyo" "herb" "phag[o]" "phil" "phob" "phren" "somn" "tachy" "brady" "isoant" "apo" "allo" "ambi" "hetero" "homo" "para" "pro" "anti" "tele" "antho" "anthro" "arthr[o]" "arthro" "blast" "blasto" "blepharo" "brachy" "bronch[o]" "cardi[o]" "cephal[o]" "cephalo" "cerebro" "cheiro" "chir[o]" "chrom[o]" "cyclo" "cycl" "demi" "dermato" "dipl[o]" "dolicho" "dromo" "ecto" "endo" "epizo" "exo" "ferro" "gam" "gamy" "gene" "gon" "haplo" "hemi" "heter" "holo" "hom[o]" "hydro" "ideo" "idio" "ile" "intero" "iso" "karyo" "kerato" "lachry" "leio" "leuk[o]" "lexi" "lith[o]" "macro" "magn" "malac" "mast" "medi" "meso" "meta" "meter" "metr[o]" "micro" "milli" "miso"))

(setq abv-suffixes
      '("ise" "cene" "escent" "orama" "ose" "ple" "ploid" "able" "ac" "ocity" "ade" "age" "aholic" "oholic" "al" "algia" "ance" "ant" "ar" "ard" "arian" "arium" "ary" "ate" "ation" "ative" "cide" "cracy" "crat" "cule" "cy" "cycle" "dom" "dox" "ectomy" "ed" "ee" "eer" "emia" "en" "ence" "ency" "ent" "er" "ern" "escence" "ese" "esque" "ess" "est" "etic" "ette" "ful" "fy" "gam" "gamy" "gon" "gonic" "hood" "ial" "ian" "iasis" "iatric" "ible" "ic" "ical" "ile" "ily" "ine" "ing" "ion" "ious" "ish" "ism" "ist" "ite" "itis" "ity" "ive" "ization" "ize" "less" "let" "like" "ling" "loger" "logist" "log" "ly" "ment" "ness" "oid" "ology" "oma" "onym" "opia" "opsy" "or" "ory" "osis" "ostomy" "otomy" "ous" "path" "pathy" "phile" "phobia" "phone" "phyte" "plegia" "plegic" "pnea" "scopy" "scope" "scribe" "script" "sect" "ship" "sion" "some" "sophy" "sophic" "th" "tion" "tome" "tomy" "trophy" "tude" "ty" "ular" "uous" "ure" "ward" "ware" "wise" "y"))

(provide 'abv-data)
;;; abv-data.el ends here

#+end_tool
#+begin_tool (read_buffer :buffer "abv-corpus.el")
(:name "read_buffer" :args (:buffer "abv-corpus.el"))

;;; abv-corpus.el --- Corpus analysis for abbrevs    -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: abbrev

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; 

;;; Code:

(defcustom abv-corpus-file-types
  '("org" "txt" "md" "tex")
  ""
  :type '(repeat string)
  :group 'abv)

(defcustom abv-corpus-file
  
  ""
  :type 'boolean
  :group )


(defun daselt-abv-personal-corpus (&optional dir)
  "Create a personal word-frequency statistic by scanning files in DIR.

Recurse through DIR (defaults to the user's home directory) using
`daselt-dirs-act-on-pkg-files-by-type'. For each file whose extension is in
`daselt-abv-corpus-file-types', count the occurrences of each word
\(case-insensitive) and aggregate counts. The resulting association list of
\(word . COUNT) pairs is written to `daselt-abv-corpus-file'. Finally, call
`daselt-abv-read-statistics'."
  (interactive)
  (let* ((start-dir (file-name-as-directory (expand-file-name (or dir "~"))))
         (exts (mapcar #'downcase abv-corpus-file-types))
         (table (make-hash-table :test #'equal))
         ;; Predicate for daselt-dirs to select files by extension
         (pred (lambda (filepath)
                 (let ((ext (file-name-extension filepath)))
                   (and ext (member (downcase ext) exts))))))
    ;; Traverse and count
    (daselt-dirs-act-on-pkg-files-by-type
     (list (cons (lambda (filepath)
                   (daselt-abv--count-words-into filepath table)
                   nil)
                 pred))
     start-dir)
    ;; Persist as a sorted alist for readability
    (let (alist)
      (maphash (lambda (k v) (push (cons k v) alist)) table)
      (setq alist (sort alist (lambda (a b) (> (cdr a) (cdr b)))))
      (let ((file abv-corpus-file))
        (let ((dir (file-name-directory file)))
          (when dir (ignore-errors (make-directory dir t))))
        (with-temp-file file
          (let ((print-length nil)
                (print-level nil))
            (prin1 alist (current-buffer))))))
    (daselt-abv-read-statistics)))

(defun abv-corpus--optional-letter-regex (s)
  "Turn \"ab[c]d\" into a regex that matches \"abcd\" or \"abd\"."
  (let ((start 0) (out ""))
    (while (string-match "\\[\\([[:alpha:]]\\)\\]" s start)
      (setq out (concat out
                        (regexp-quote (substring s start (match-beginning 0)))
                        (match-string 1 s)
                        "?"))
      (setq start (match-end 0)))
    (setq out (concat out (regexp-quote (substring s start))))
    out))

(defun abv-corpus-ngram-frequencies (prefixes corpus n &optional posn)
  "Return an n-deep nested list; leaves are (pseq . count).
pseq is a list of n prefixes (as given), count is how many corpus
entries start with the concatenation of these (with [x] optional).
Corpus entries can be strings or (word . data) pairs."
  (let* ((initial (and (stringp posn) (string= posn "initial")))
         (final (and (stringp posn) (string= posn "final")))
         (pieces (let ((ht (make-hash-table :test 'equal)))
                   (dolist (p prefixes) (puthash p (abv-corpus--optional-letter-regex p) ht))
                   ht))
         (words  (mapcar (lambda (e) (if (consp e) (car e) e)) corpus)))
    (cl-labels
        ((concat-re (pseq)
           (concat (if initial (rx string-start))
                   (mapconcat (lambda (p) (gethash p pieces)) pseq "")
                   (if final (rx string-end))))
         (count-for (pseq)
           (let* ((re (concat-re pseq)) (c 0))
             (dolist (w words) (when (string-match-p re w) (setq c (1+ c))))
             (cons pseq c)))
         (build (k acc)
           (if (zerop k)
               (count-for (nreverse acc))
             (cl-remove-if
              (lambda (cns)
                (= 0 (cdr cns)))
              (daselt-base-flatten-until (mapcar (lambda (p)
                                                   (build (1- k) (cons p acc)))
                                                 prefixes)
                                         (lambda (lt)
                                           (numberp (cdar lt))))))))
      (build n nil))))

(defun abv-corpus-reduced-monogram-frequencies (affixes corpus &optional posn)
  "Return monogram frequencies reduced by counts of their extensions.

Uses `abv-corpus-ngram-frequencies' with n=1 to obtain base counts for each
AFFIX in AFFIXES. For each AFFIX P the returned count is
  max(0, count(P) - sum_{Q in AFFIXES, Q != P and Q starts-with P} count(Q))
This gives the number of corpus entries that match P but not any longer
affix that extends P.
The return value follows the same shape as
`abv-corpus-ngram-frequencies' for n=1: a list of ((P) . COUNT)."
  (let* ((mono (abv-corpus-ngram-frequencies affixes corpus 1 posn))
         (counts (let ((ht (make-hash-table :test #'equal)))
                   (dolist (entry mono)
                     ;; entry is ((p) . count)
                     (puthash (car (car entry)) (cdr entry) ht))
                   ht))
         (result nil))
    (dolist (entry mono)
      (let* ((p (car (car entry)))
             (c (gethash p counts 0))
             (sum-ext 0))
        (dolist (q affixes)
          (when (and (not (equal p q))
                     (string-match-p p q))
            (setq sum-ext (+ sum-ext (gethash q counts 0)))))
        (push (cons (list p) (max 0 (- c sum-ext))) result)))
    (nreverse (sort result :key #'cdr))))

(defun daselt-abv-hash->def-form (var hash &optional doc)
  "Return a defconst form that binds VAR to a hash table equal to HASH.
DOC is an optional docstring."
  (let ((pairs nil))
    (maphash (lambda (k v) (push (cons k v) pairs)) hash)
    ;; Stable order for reproducible diffs
    (setq pairs
          (cl-sort pairs
                   (lambda (a b)
                     (string< (prin1-to-string (car a))
                              (prin1-to-string (car b))))))
    `(defconst ,var
       ,(let ((ht (make-symbol "ht")))
          `(let ((,ht (make-hash-table
                       :test ',(hash-table-test hash)
                       :size ,(hash-table-size hash)
                       :rehash-size ,(hash-table-rehash-size hash)
                       :rehash-threshold ,(hash-table-rehash-threshold hash)
                       ,@(when (hash-table-weakness hash)
                           `(:weakness ,(hash-table-weakness hash))))))
             ,@(mapcar (lambda (kv)
                         `(puthash ',(car kv) ',(cdr kv) ,ht))
                       pairs)
             ,ht))
       ,(or doc ""))))

(defun abv-corpus-print-table (sym)
  (interactive)
  (let ((print-level nil)
        (print-length nil))
    (insert (string-replace
             "\(let" "
\(let"
             (string-replace
              " \"\"" ""
              (string-replace
               "\) \(" "\)
\("
               (format "%S" (daselt-abv-hash->def-form sym (symbol-value sym)))))))
    (save-excursion
      (indent-region (point)
                     (prog2 (forward-sexp)
                         (point))))))
(provide 'abv-corpus)
;;; abv-corpus.el ends here

#+end_tool
#+begin_tool (read_buffer :buffer "daselt-abv.el")
(:name "read_buffer" :args (:buffer "daselt-abv.el"))

error error: buffer daselt-abv.el is not live.
#+end_tool
#+begin_tool (variable_completions :variable_prefix "daselt-mode-")
(:name "variable_completions" :args (:variable_prefix "daselt-mode-"))

daselt-daselt-mode-miscellaneous-map-bindlist
daselt-mode-quick-key-string
daselt-mode-tab-bar-status-functions
daselt-daselt-mode-theme-map-bindlist
daselt-mode-undaselt
daselt-mode-pkg-configs-directory
daselt-mode-quick-key-coords-base-list
daselt-mode-map
daselt-mode-miscellaneous-map
daselt-daselt-mode-miscellaneous-map-backup
daselt-mode-include-imitation-commands
daselt-mode-redaselt-time
daselt-mode-global-udevmon
daselt-mode-eval-log
daselt-mode-quick-key-string-cons
daselt-mode-redaselt
daselt-mode-globalize-daselt-mode-map
daselt-daselt-mode-map-bindlist
daselt-mode-quick-key-list
daselt-mode-put-daselt-mode-map-into-emulation
daselt-mode-hook
daselt-daselt-mode-theme-map-backup
daselt-mode-exchange-H-2-H-6
daselt-daselt-mode-map-backup
daselt-mode-theme-map
daselt-mode-quick-key-layers-list
daselt-mode-keep-tab-bar-status
daselt-mode-show-tutorial
#+end_tool
#+begin_tool (variable_documentation :variable "daselt-mode-pkg- ...)
(:name "variable_documentation" :args (:variable "daselt-mode-pkg-configs-directory"))

The pkg-configs-directory of `daselt-mode'.

This should be the directory named `mode-configs' in the root directory
of Daselt.
#+end_tool
#+begin_tool (function_documentation :function "daselt-abv-place ...)
(:name "function_documentation" :args (:function "daselt-abv-placevals-from-classes"))



(fn CLASSES)
#+end_tool
#+begin_tool (function_source :function "daselt-abv-assign-affixes")
(:name "function_source" :args (:function "daselt-abv-assign-affixes"))

wrong-type-argument integer-or-marker-p nil
#+end_tool
#+begin_tool (function_source :function "daselt-abv--assign-one")
(:name "function_source" :args (:function "daselt-abv--assign-one"))

wrong-type-argument integer-or-marker-p nil
#+end_tool
#+begin_tool (function_source :function "daselt-abv--sorted-affi ...)
(:name "function_source" :args (:function "daselt-abv--sorted-affixes-by-frequency"))

wrong-type-argument integer-or-marker-p nil
#+end_tool
#+begin_tool (function_source :function "daselt-abv--coord-layer ...)
(:name "function_source" :args (:function "daselt-abv--coord-layers-in-order"))

wrong-type-argument integer-or-marker-p nil
#+end_tool
#+begin_tool (function_source :function "daselt-abv--placevals-> ...)
(:name "function_source" :args (:function "daselt-abv--placevals->coord-hash"))

wrong-type-argument integer-or-marker-p nil
#+end_tool
#+begin_tool (function_documentation :function "daselt-abv-hash- ...)
(:name "function_documentation" :args (:function "daselt-abv-hash->def-form"))

Return a defconst form that binds VAR to a hash table equal to HASH.
DOC is an optional docstring.
#+end_tool
#+begin_tool (function_documentation :function "daselt-bind-save ...)
(:name "function_documentation" :args (:function "daselt-bind-save-bindlist-as-variable"))

Save BLIST as a variable.

PFX is the prefix given to the saved bindlists. It is ‘daselt-’ by default.

(fn BLIST &optional PFX)
#+end_tool
#+begin_tool (function_documentation :function "daselt-bind-draw ...)
(:name "function_documentation" :args (:function "daselt-bind-draw-bindlist-layer"))

Draw a layer of the bindlist identified by BLISTSYMB.

Use a maximized window. LAYCOORD specifies the layer to draw, and MODS the
modifiers of the layer.

(fn BLISTSYMB LAYCOORD &rest MODS)
#+end_tool
#+begin_tool (function_documentation :function "daselt-abv-add-t ...)
(:name "function_documentation" :args (:function "daselt-abv-add-to-temporary-word-constituents"))

Add characters from ‘daselt-xkb-layout’ to ‘abv-temporary-word-constituents’.

Adds characters whose syntax-class is in ‘daselt-abv-added-syntax-classes’.

Excludes symbols from ‘daselt-abv-non-constituents’.
#+end_tool
#+begin_tool (function_completions :function_prefix "abv-look-")
(:name "function_completions" :args (:function_prefix "abv-look-"))

abv-look--cache-remove-pair
abv-look--cache-add-pair
abv-look--cache-count
abv-look--combined-tables
abv-look-refresh-cache
abv-look--enumerate-pairs
abv-look--cache-rename
abv-look--active-minor-abbrev-tables
abv-look-up
abv-look--cache-expansions
abv-look--collect-parents
abv-look--compute-cache
abv-look--make-obarray
abv-look--cache-find
abv-look--remove-text-properties-from-string
#+end_tool
#+begin_tool (function_source :function "abv-look-up")
(:name "function_source" :args (:function "abv-look-up"))

(defun abv-look-up (expansion &optional regenerate)
  "Look up abbrev for EXPANSION using the obarray-backed cache.

With prefix argument REGENERATE (or if cache is empty), rebuild the cache first.
Interactively, prompt for EXPANSION from the cache. If not found, prompt for a
destination table via `abv--choose-abbrev-table' and add an abbrev using
`abv-add--with-overwrite-warning'. Return the abbrev string if found/created,
or nil if cancelled."
  (interactive
   (let ((regen current-prefix-arg))
     (when (or regen (null abv-look-cache))
       (abv-look-refresh-cache))
     (list (abv-look--remove-text-properties-from-string
            (completing-read "Expansion: " abv-look-cache nil nil nil
                             'abv-look-expansion-history))
           regen)))
  (when (or regenerate (null abv-look-cache))
    (abv-look-refresh-cache))
  (if (and (stringp expansion) (not (string= expansion "")))
      (let ((abbr (abv-look--cache-find expansion)))
        (if abbr
            (progn (message "%s" abbr) abbr)
          ;; No abbrev found; optionally offer to create one by asking for a table.
          (if (called-interactively-p 'interactive)
              (condition-case err
                  (let* ((choice (abv--choose-abbrev-table))
                         (table (car choice)))
                    (if (and (obarrayp table)
                             (abv-add--with-overwrite-warning table expansion))
                        (let ((new (abv-look--cache-find expansion)))
                          (when new (message "abv: defined %s for %s" new expansion))
                          new)
                      (message "abv: no table chosen or creation cancelled for %s" expansion)
                      nil))
                (quit (message "abv: cancelled") nil)
                (error (message "abv: error: %s" (error-message-string err)) nil))
            (message "abv: no abbrev for expansion: %s" expansion)
            nil)))
    (message "abv: empty expansion")
    nil))

#+end_tool
#+begin_tool (function_source :function "daselt-bind-coords-from ...)
(:name "function_source" :args (:function "daselt-bind-coords-from-binding"))

(defun daselt-bind-coords-from-binding (binding)
  "Retrieve coordinates associated with a BINDING if available.

Otherwise, return nil."
  (declare (ftype (function (t) list
                            ;; (list number) ; Compiler complains.
                            ))
           (pure t))
  (cond ((stringp (car binding))
         nil)
        ((and (consp (car binding)) (daselt-coords-p (cdar binding)))
         (cdar binding))
        ((daselt-coords-p (car binding)) (car binding))))

#+end_tool
#+begin_tool (read_buffer :buffer "daselt-bind.el")
(:name "read_buffer" :args (:buffer "daselt-bind.el"))

error error: buffer daselt-bind.el is not live.
#+end_tool
#+begin_tool (modify_buffer :buffer "abv-data.el" :content ";;;  ...)
(:name "modify_buffer" :args (:buffer "abv-data.el" :content ";;; abv-data.el --- Global defaults for affix data (abv) -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: abbrev, convenience
;; License: GPL-3+

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This library provides global default lists of roots, prefixes, suffixes
;; (and optionally infixes) for abbreviation/affix tooling.  They are
;; intended as language-agnostic defaults.  For a particular language you
;; should extend the tables you generate at runtime with language-specific
;; information via table properties: add values to a table symbol\\='s
;; properties :roots, :prefixes, :suffixes and :infixes instead of
;; modifying these global defaults.
;;
;; Example (pseudo-code):
;;   (let ((tbl daselt-abv-prefix-table))
;;     (put tbl :prefixes '(\"über\" \"unter\")))
;;
;; Packages building affix tables should read the table symbol\\='s
;; properties first and then fall back to these global defaults.

;;; Code:

(require 'cl-lib)

(defgroup abv nil
  \"Abbreviation and affix utilities.\"
  :group 'convenience
  :prefix \"abv-\")

(defcustom abv-roots
  '(\"zyg\" \"zon\" \"zo\" \"zes\" \"zem\" \"zeal\" \"xylo\" \"xiph\" \"xero\" \"xeno\" \"xanth\" \"ward\" \"vuls\" \"vulner\" \"vulg\" \"vot\" \"vor\" \"vom\" \"volv\" \"volt\" \"volup\" \"volcan\" \"vol\" \"void\" \"voice\" \"voc\" \"vok\" \"viv\" \"vitt\" \"vitr\" \"viti\" \"vitel\" \"vitam\" \"visc\" \"viron\" \"virg\" \"vio\" \"vin\" \"vil\" \"vig\" \"vis\" \"vid\" \"vy\" \"vinc\" \"vict\" \"vit\" \"vic\" \"vibr\" \"vi\" \"voy\" \"vey\" \"veter\" \"vestig\" \"vest\" \"vesp\" \"vesic\" \"vert\" \"vers\" \"vern\" \"verm\" \"verg\" \"verb\" \"vir\" \"ver\" \"vent\" \"ven\" \"vill\" \"vel\" \"veg\" \"vex\" \"vat\" \"vast\" \"varic\" \"varix\" \"var\" \"vap\" \"van\" \"valv\" \"vall\" \"val\" \"vagin\" \"vag\" \"vas\" \"vacill\" \"vacc\" \"vac\" \"uxor\" \"uv\" \"uter\" \"us\" \"ut\" \"urt\" \"ure\" \"urb\" \"uran\" \"ur\" \"uni\" \"und\" \"ung\" \"unc\" \"umbil\" \"umb\" \"ulter\" \"ult\" \"ulo\" \"ul\" \"uber\" \"tyro\" \"typh\" \"typ\" \"tymp\" \"tuss\" \"turg\" \"turb\" \"tuni\" \"tund\" \"tus\" \"tum\" \"tut\" \"tub\" \"trunc\" \"trud\" \"trus\" \"tru\" \"trove\" \"troph\" \"trop\" \"trog\" \"troch\" \"triv\" \"trim\" \"trism\" \"triev\" \"trich\" \"trig\" \"trib\" \"tri\" \"trepid\" \"trem\" \"trauma\" \"trapez\" \"trad\" \"tract\" \"trach\" \"trab\" \"tox\" \"tout\" \"tour\" \"tot\" \"tort\" \"tors\" \"torr\" \"torp\" \"torn\" \"tor\" \"top\" \"toment\" \"tom\" \"tol\" \"toc\" \"titu\" \"tirp\" \"tir\" \"tint\" \"tim\" \"thyr\" \"thym\" \"thromb\" \"thrix\" \"thrall\" \"thorax\" \"thio\" \"thesaur\" \"thes\" \"thet\" \"therm\" \"therap\" \"ther\" \"then\" \"thel\" \"thegm\" \"thec\" \"the\" \"thaum\" \"thana\" \"thalp\" \"thall\" \"thalass\" \"thal\" \"tex\" \"teuch\" \"tetra\" \"tess\" \"test\" \"tessa\" \"terr\" \"terp\" \"term\" \"terg\" \"ters\" \"tere\" \"terat\" \"ter\" \"tep\" \"tenebr\" \"tend\" \"tent\" \"ton\" \"ten\" \"tin\" \"tempt\" \"temp\" \"temn\" \"temer\" \"tem\" \"tell\" \"tel\" \"techn\" \"tect\" \"tec\" \"taut\" \"taur\" \"tass\" \"tas\" \"tars\" \"tard\" \"tara\" \"taph\" \"tape\" \"tang\" \"tali\" \"tal\" \"tain\" \"tail\" \"taen\" \"tax\" \"tact\" \"teg\" \"tack\" \"tach\" \"tic\" \"tab\" \"sylum\" \"syco\" \"sut\" \"susurr\" \"suscit\" \"surg\" \"surd\" \"supin\" \"super\" \"sum\" \"sumpt\" \"sulf\" \"sulc\" \"sui\" \"sue\" \"sudden\" \"sud\" \"sucr\" \"succul\" \"suber\" \"suas\" \"suav\" \"stypt\" \"styl\" \"stup\" \"stud\" \"stroph\" \"strob\" \"string\" \"strid\" \"strict\" \"stria\" \"stress\" \"strep\" \"strat\" \"strait\" \"strain\" \"stom\" \"stul\" \"stle\" \"stit\" \"stirp\" \"stip\" \"stin\" \"stim\" \"still\" \"sting\" \"stig\" \"stich\" \"stic\" \"sthen\" \"stet\" \"stern\" \"sterc\" \"ster\" \"steno\" \"stell\" \"stel\" \"steat\" \"stat\" \"stor\" \"stas\" \"stem\" \"staphyl\" \"stan\" \"stamen\" \"stal\" \"stol\" \"stagn\" \"stab\" \"squam\" \"squal\" \"spum\" \"spor\" \"spont\" \"spondyl\" \"spond\" \"spons\" \"spol\" \"splend\" \"splen\" \"splanchn\" \"spiss\" \"spir\" \"spin\" \"sphy\" \"sphin\" \"spher\" \"sphen\" \"sphal\" \"spers\" \"sperm\" \"sper\" \"spel\" \"spec\" \"spic\" \"spasm\" \"spac\" \"spat\" \"souc\" \"sorc\" \"soror\" \"sord\" \"sorp\" \"sorb\" \"sor\" \"sopor\" \"soph\" \"son\" \"somn\" \"soma\" \"solub\" \"solv\" \"sold\" \"solen\" \"sol\" \"sod\" \"soc\" \"sob\" \"slav\" \"skep\" \"skelet\" \"sit\" \"sist\" \"siphon\" \"siop\" \"sinister\" \"sine\" \"sincere\" \"sin\" \"simil\" \"sylv\" \"sil\" \"sign\" \"sifl\" \"sider\" \"sid\" \"sic\" \"sib\" \"sever\" \"set\" \"ses\" \"serv\" \"serr\" \"serp\" \"seric\" \"seren\" \"ser\" \"sequ\" \"sepul\" \"sept\" \"sepal\" \"sep\" \"sens\" \"sent\" \"sen\" \"semper\" \"semin\" \"sembl\" \"sem\" \"selen\" \"seiz\" \"seism\" \"sess\" \"sed\" \"secut\" \"secular\" \"sect\" \"sex\" \"sec\" \"seb\" \"season\" \"search\" \"scyph\" \"scut\" \"scur\" \"sculp\" \"scrut\" \"scrup\" \"scrob\" \"scrib\" \"scot\" \"scor\" \"scop\" \"skept\" \"sconce\" \"scler\" \"scirrh\" \"scint\" \"sciss\" \"sci\" \"scho\" \"schis\" \"schiz\" \"schem\" \"scen\" \"skel\" \"scat\" \"scarp\" \"scar\" \"scap\" \"scan\" \"scend\" \"scal\" \"scab\" \"sax\" \"sav\" \"saur\" \"sault\" \"sauer\" \"sat\" \"sart\" \"sarc\" \"sapr\" \"sapo\" \"sap\" \"sang\" \"sanct\" \"salu\" \"san\" \"salp\" \"saliv\" \"sult\" \"sal\" \"sagitt\" \"sag\" \"sip\" \"sacr\" \"secr\" \"sacchar\" \"sac\" \"runc\" \"rump\" \"rupt\" \"rum\" \"rug\" \"rud\" \"ruct\" \"rub\" \"round\" \"rot\" \"rog\" \"ros\" \"rob\" \"rit\" \"riv\" \"rim\" \"ris\" \"rib\" \"rhythm\" \"rhomb\" \"rhod\" \"rhiz\" \"rhin\" \"rhet\" \"rhe\" \"rhaps\" \"rhabd\" \"retro\" \"ret\" \"rest\" \"rend\" \"ren\" \"rem\" \"reg\" \"rect\" \"rig\" \"re\" \"rei\" \"raz\" \"ray\" \"rat\" \"rap\" \"rep\" \"ransom\" \"range\" \"ran\" \"ramp\" \"ram\" \"raj\" \"radio\" \"ras\" \"rad\" \"ract\" \"rhachi\" \"rac\" \"rab\" \"quot\" \"quir\" \"quip\" \"quin\" \"quie\" \"quit\" \"quet\" \"quer\" \"quis\" \"ques\" \"quav\" \"quas\" \"quan\" \"qual\" \"quaint\" \"quadr\" \"quatr\" \"pyr\" \"pyl\" \"pyel\" \"pycno\" \"py\" \"put\" \"pust\" \"pusill\" \"purple\" \"purlieu\" \"purg\" \"putr\" \"pur\" \"pup\" \"punct\" \"punt\" \"pulv\" \"pult\" \"pulm\" \"pulchri\" \"pug\" \"puer\" \"pud\" \"puc\" \"pub\" \"pty\" \"pter\" \"psychr\" \"psych\" \"pseud\" \"pseph\" \"pse\" \"prur\" \"pruin\" \"prudent\" \"prox\" \"prop\" \"protein\" \"prot\" \"prosop\" \"prompt\" \"prol\" \"proct\" \"probr\" \"prob\" \"prov\" \"priv\" \"prior\" \"prim\" \"prest\" \"press\" \"presby\" \"prehend\" \"pris\" \"pregnant\" \"pred\" \"prec\" \"pret\" \"prav\" \"prand\" \"prax\" \"pov\" \"pound\" \"poul\" \"post\" \"pos\" \"porphy\" \"por\" \"popul\" \"publ\" \"pont\" \"pono\" \"pond\" \"pon\" \"pot\" \"pom\" \"poll\" \"polio\" \"poli\" \"polem\" \"pole\" \"pol\" \"poe\" \"pod\" \"pus\" \"pneu\" \"pluv\" \"plut\" \"plu\" \"plumb\" \"plum\" \"plot\" \"plor\" \"plod\" \"pliu\" \"plic\" \"plev\" \"pleus\" \"pleur\" \"pleth\" \"plet\" \"plesio\" \"plen\" \"ply\" \"plex\" \"plec\" \"pleb\" \"ple\" \"plio\" \"pleio\" \"plei\" \"plos\" \"platy\" \"plas\" \"plant\" \"plang\" \"plan\" \"plain\" \"plag\" \"plac\" \"pithec\" \"pisc\" \"pir\" \"piq\" \"ping\" \"piest\" \"pict\" \"picr\" \"pico\" \"pi\" \"phyt\" \"physi\" \"phu\" \"phys\" \"phyll\" \"phyla\" \"phyl\" \"phyg\" \"phyc\" \"phthong\" \"phthis\" \"phthir\" \"phras\" \"phren\" \"phos\" \"phot\" \"phor\" \"phon\" \"phob\" \"phleg\" \"phlog\" \"phleb\" \"phil\" \"phern\" \"pher\" \"phem\" \"phell\" \"pharyn\" \"pharmac\" \"phan\" \"phes\" \"phall\" \"phalan\" \"phag\" \"petr\" \"petit\" \"petal\" \"pit\" \"pest\" \"pessi\" \"pet\" \"person\" \"perisso\" \"peps\" \"penth\" \"penta\" \"peni\" \"penetr\" \"pend\" \"pens\" \"pun\" \"pin\" \"pen\" \"pelag\" \"pel\" \"puls\" \"pil\" \"pejor\" \"ped\" \"pes\" \"pecu\" \"pect\" \"pecca\" \"pav\" \"paus\" \"path\" \"pater\" \"patri\" \"pat\" \"parthen\" \"part\" \"port\" \"pars\" \"parl\" \"parish\" \"parie\" \"parasit\" \"per\" \"par\" \"pap\" \"pand\" \"pass\" \"pan\" \"past\" \"palpeb\" \"palp\" \"palm\" \"pall\" \"palin\" \"paleo\" \"palat\" \"pala\" \"pal\" \"pag\" \"pachy\" \"pac\" \"pax\" \"ozon\" \"ox\" \"oxy\" \"ov\" \"ous\" \"our\" \"oto\" \"oti\" \"ostra\" \"osm\" \"oscill\" \"osc\" \"os\" \"ost\" \"oryct\" \"orth\" \"oreo\" \"ornith\" \"orn\" \"ori\" \"ort\" \"organ\" \"orex\" \"ordure\" \"ord\" \"orchi\" \"orchard\" \"orch\" \"orc\" \"orb\" \"ora\" \"optim\" \"opt\" \"opso\" \"opi\" \"ophthalm\" \"ophi\" \"oper\" \"op\" \"oophor\" \"oo\" \"onym\" \"onych\" \"onyx\" \"onto\" \"onom\" \"oneir\" \"onk\" \"on\" \"omphal\" \"omo\" \"omni\" \"omma\" \"omen\" \"ombro\" \"omasum\" \"oliv\" \"oligo\" \"ol\" \"eno\" \"oec\" \"odyn\" \"ody\" \"odor\" \"odont\" \"od\" \"ocul\" \"ocel\" \"octroi\" \"oct\" \"ochl\" \"och\" \"obey\" \"obel\" \"nymph\" \"nyct\" \"nut\" \"nutr\" \"nunci\" \"num\" \"null\" \"nud\" \"nub\" \"nup\" \"nov\" \"nounc\" \"noto\" \"not\" \"nost\" \"noso\" \"norm\" \"nona\" \"non\" \"nom\" \"noir\" \"nod\" \"noc\" \"nox\" \"nobl\" \"nous\" \"nev\" \"nitr\" \"nit\" \"nimbus\" \"nihil\" \"nigr\" \"nid\" \"niv\" \"neut\" \"neur\" \"neth\" \"nes\" \"nerv\" \"ner\" \"nepot\" \"nephr\" \"neph\" \"neo\" \"nema\" \"neg\" \"nex\" \"necr\" \"nec\" \"nic\" \"nebul\" \"neal\" \"nav\" \"naus\" \"naut\" \"nat\" \"nast\" \"nas\" \"narr\" \"narc\" \"nano\" \"myx\" \"myrmec\" \"myr\" \"myo\" \"myel\" \"myc\" \"muti\" \"mut\" \"mus\" \"mund\" \"munct\" \"multi\" \"muls\" \"muli\" \"mulc\" \"mul\" \"mucr\" \"muc\" \"mov\" \"moss\" \"mort\" \"morph\" \"mors\" \"mur\" \"mor\" \"mont\" \"mount\" \"mono\" \"mone\" \"mun\" \"mon\" \"molybd\" \"mol\" \"mod\" \"mob\" \"mne\" \"mitig\" \"mit\" \"miss\" \"mitt\" \"miso\" \"miser\" \"misc\" \"mix\" \"mir\" \"minist\" \"minis\" \"mini\" \"mim\" \"milit\" \"mil\" \"migr\" \"mignon\" \"mict\" \"micro\" \"mica\" \"my\" \"meter\" \"metr\" \"metic\" \"methy\" \"meteor\" \"metal\" \"merry\" \"meret\" \"merg\" \"mers\" \"merc\" \"mephi\" \"ment\" \"mind\" \"mens\" \"mes\" \"mening\" \"mence\" \"min\" \"mem\" \"mell\" \"melior\" \"mela\" \"mel\" \"mio\" \"megalo\" \"medull\" \"med\" \"machin\" \"mec\" \"meas\" \"mean\" \"mea\" \"maz\" \"may\" \"maxim\" \"maxill\" \"maur\" \"math\" \"matter\" \"mater\" \"metro\" \"mata\" \"mat\" \"mast\" \"mass\" \"mask\" \"masc\" \"mart\" \"mars\" \"marce\" \"mark\" \"maras\" \"mer\" \"mar\" \"mant\" \"mania\" \"mand\" \"mancy\" \"mend\" \"men\" \"man\" \"mamm\" \"mall\" \"malac\" \"mala\" \"mal\" \"male\" \"maj\" \"main\" \"magne\" \"mag\" \"max\" \"macu\" \"macro\" \"machy\" \"macass\" \"lys\" \"lyze\" \"lymph\" \"lut\" \"lur\" \"lup\" \"lun\" \"lumbri\" \"lumb\" \"lum\" \"lugub\" \"lug\" \"lus\" \"luct\" \"lucr\" \"luc\" \"lux\" \"lubr\" \"lox\" \"lot\" \"loqu\" \"lop\" \"long\" \"locut\" \"loc\" \"lob\" \"lito\" \"lith\" \"lite\" \"liter\" \"lir\" \"liqu\" \"lix\" \"linq\" \"ling\" \"lin\" \"limn\" \"limi\" \"lim\" \"lign\" \"ly\" \"lieu\" \"lis\" \"lict\" \"libit\" \"liber\" \"lib\" \"lev\" \"liv\" \"liev\" \"leuc\" \"leth\" \"let\" \"lepto\" \"leps\" \"lemm\" \"leo\" \"lent\" \"lens\" \"leni\" \"lemn\" \"lip\" \"lig\" \"leg\" \"lectu\" \"lex\" \"log\" \"lect\" \"lecith\" \"lay\" \"lax\" \"lav\" \"luv\" \"laud\" \"latr\" \"lit\" \"laryng\" \"larg\" \"lapid\" \"lapis\" \"lapar\" \"langu\" \"lanc\" \"lan\" \"lamp\" \"lament\" \"lam\" \"lal\" \"lagn\" \"lacu\" \"lact\" \"lacri\" \"lacert\" \"lacer\" \"lac\" \"labor\" \"lep\" \"lab\" \"know\" \"klep\" \"kilo\" \"keto\" \"kerat\" \"ken\" \"caryo\" \"karyo\" \"kain\" \"jut\" \"just\" \"juv\" \"jug\" \"junct\" \"jud\" \"jus\" \"jub\" \"jour\" \"joy\" \"joc\" \"jet\" \"jej\" \"jamb\" \"jac\" \"ject\" \"ize\" \"itis\" \"ithy\" \"iter\" \"it\" \"iso\" \"ischi\" \"ische\" \"iris\" \"ir\" \"ips\" \"iod\" \"interest\" \"integ\" \"intim\" \"int\" \"insul\" \"iniquity\" \"inimical\" \"ini\" \"inguin\" \"infer\" \"ine\" \"industry\" \"indu\" \"indigent\" \"inan\" \"imit\" \"imag\" \"ileu\" \"ile\" \"ign\" \"idio\" \"iden\" \"ide\" \"icter\" \"icos\" \"icon\" \"ichthy\" \"ichor\" \"ichno\" \"iatr\" \"hyster\" \"hypso\" \"hypn\" \"hyphen\" \"hypergolic\" \"hymn\" \"hymen\" \"hylo\" \"hygr\" \"hygi\" \"hyet\" \"hyal\" \"hum\" \"hot\" \"hort\" \"horr\" \"horm\" \"hori\" \"or\" \"hor\" \"hopl\" \"hon\" \"om\" \"hom\" \"hol\" \"hod\" \"hoc\" \"hist\" \"hipp\" \"himsa\" \"hilar\" \"hier\" \"hidr\" \"hibern\" \"hisc\" \"hex\" \"hevel\" \"heur\" \"hesper\" \"ema\" \"herpe\" \"hero\" \"hermit\" \"heres\" \"her\" \"heir\" \"hept\" \"heps\" \"hepa\" \"hent\" \"hendeca\" \"hen\" \"hemer\" \"hem\" \"helminth\" \"helic\" \"helix\" \"heli\" \"hege\" \"hedr\" \"hedon\" \"hect\" \"hebe\" \"hears\" \"haust\" \"harm\" \"hapt\" \"apl\" \"hance\" \"halluc\" \"hal\" \"hagi\" \"hab\" \"hib\" \"gyn\" \"gymn\" \"guttur\" \"gutt\" \"gust\" \"gulf\" \"guis\" \"guerr\" \"grunt\" \"gru\" \"gro\" \"grieve\" \"gret\" \"greg\" \"grav\" \"grat\" \"graph\" \"grant\" \"grand\" \"gran\" \"gramin\" \"gram\" \"graft\" \"grad\" \"gress\" \"grace\" \"guber\" \"gurg\" \"gon\" \"gno\" \"gni\" \"gnath\" \"gna\" \"glyph\" \"glypt\" \"glyc\" \"glut\" \"gluc\" \"glot\" \"glob\" \"glom\" \"gle\" \"glau\" \"gland\" \"glad\" \"glac\" \"glab\" \"gird\" \"gyr\" \"giga\" \"ghast\" \"geus\" \"gest\" \"germ\" \"geran\" \"ger\" \"gis\" \"geo\" \"genu\" \"gen\" \"gemin\" \"gel\" \"gastr\" \"gar\" \"gap\" \"gam\" \"galact\" \"galax\" \"gal\" \"gain\" \"gage\" \"fut\" \"fusc\" \"fus\" \"furc\" \"fur\" \"fung\" \"fum\" \"fulg\" \"fug\" \"frustr\" \"fruc\" \"frug\" \"fron\" \"fright\" \"frig\" \"fresc\" \"frequen\" \"fray\" \"fraud\" \"frat\" \"frang\" \"frank\" \"frain\" \"fract\" \"fring\" \"found\" \"fund\" \"foss\" \"fortu\" \"fort\" \"formic\" \"form\" \"ford\" \"for\" \"foll\" \"foli\" \"foc\" \"flu\" \"flux\" \"flor\" \"floc\" \"flig\" \"flect\" \"flex\" \"flech\" \"flav\" \"flat\" \"flam\" \"flagell\" \"flag\" \"fulm\" \"fix\" \"fist\" \"fit\" \"fisc\" \"firm\" \"fin\" \"filt\" \"fil\" \"fig\" \"fid\" \"fic\" \"fiber\" \"fet\" \"fest\" \"ferv\" \"ferr\" \"ferm\" \"lat\" \"fer\" \"fenestra\" \"fens\" \"femin\" \"felic\" \"fel\" \"fed\" \"fecund\" \"fect\" \"fec\" \"febr\" \"fe\" \"fav\" \"faun\" \"fatu\" \"fatig\" \"u\" \"fast\" \"fasc\" \"farc\" \"far\" \"fantasy\" \"fant\" \"fan\" \"fam\" \"falq\" \"fall\" \"falc\" \"fy\" \"fac\" \"fabr\" \"fab\" \"fess\" \"exter\" \"exert\" \"ev\" \"eury\" \"eun\" \"etym\" \"ethn\" \"ethm\" \"estr\" \"esti\" \"spelled\" \"esthe\" \"eth\" \"est\" \"esse\" \"esis\" \"eschat\" \"eschar\" \"erythr\" \"ersatz\" \"err\" \"ero\" \"erg\" \"urg\" \"eres\" \"erem\" \"equ\" \"iqu\" \"epoch\" \"episio\" \"epeiro\" \"ep\" \"eor\" \"eo\" \"entre\" \"entom\" \"enter\" \"ens\" \"ennea\" \"enigm\" \"encephal\" \"emul\" \"emp\" \"eme\" \"embryo\" \"hes\" \"ely\" \"eleuther\" \"elench\" \"element\" \"elegant\" \"elee\" \"electr\" \"elast\" \"elasm\" \"eid\" \"ego\" \"ege\" \"effendi\" \"edi\" \"edem\" \"edaph\" \"ed\" \"eco\" \"ek\" \"echin\" \"exis\" \"ech\" \"eclex\" \"eccles\" \"ebur\" \"ebr\" \"ease\" \"dyt\" \"dysi\" \"dyn\" \"dy\" \"dur\" \"dult\" \"dulc\" \"dul\" \"duc\" \"dub\" \"du\" \"drom\" \"dress\" \"drama\" \"dra\" \"dos\" \"dorm\" \"don\" \"dom\" \"dolich\" \"dolabr\" \"dol\" \"dox\" \"dodeca\" \"doche\" \"doc\" \"dot\" \"dit\" \"disc\" \"dips\" \"dipl\" \"din\" \"dim\" \"dign\" \"digit\" \"didym\" \"didact\" \"dich\" \"dic\" \"diabol\" \"di\" \"dext\" \"dex\" \"deuter\" \"deterior\" \"det\" \"derm\" \"dent\" \"dens\" \"deni\" \"dendr\" \"demon\" \"demi\" \"dem\" \"deleter\" \"dele\" \"div\" \"dei\" \"deca\" \"dec\" \"deb\" \"deacon\" \"dat\" \"demn\" \"dam\" \"dactyl\" \"dacry\" \"cyt\" \"cyst\" \"cyn\" \"cycl\" \"cyan\" \"cut\" \"cust\" \"cusp\" \"cuse\" \"cuss\" \"curv\" \"curt\" \"sur\" \"cur\" \"curs\" \"cupr\" \"cup\" \"cun\" \"cumu\" \"cult\" \"culp\" \"culmin\" \"culc\" \"cul\" \"cub\" \"cumb\" \"ctyon\" \"ctet\" \"cten\" \"crystal\" \"krypt\" \"cry\" \"crust\" \"crus\" \"cruc\" \"crux\" \"cru\" \"crot\" \"cros\" \"cris\" \"crit\" \"crin\" \"cret\" \"cresc\" \"cre\" \"crep\" \"cren\" \"crem\" \"cree\" \"cred\" \"creo\" \"creas\" \"cras\" \"crani\" \"crat\" \"cox\" \"cover\" \"cout\" \"cour\" \"coupl\" \"coup\" \"count\" \"couch\" \"cotyl\" \"cost\" \"cosm\" \"corus\" \"corr\" \"corp\" \"corn\" \"cort\" \"cor\" \"coqu\" \"copu\" \"copr\" \"cop\" \"consul\" \"condyl\" \"cond\" \"conch\" \"con\" \"comb\" \"comit\" \"com\" \"colyt\" \"column\" \"colp\" \"coloss\" \"color\" \"coll\" \"cole\" \"quil\" \"col\" \"coil\" \"cohor\" \"cogn\" \"cogitate\" \"coen\" \"cod\" \"cocc\" \"coc\" \"clys\" \"clud\" \"clus\" \"clos\" \"clone\" \"clit\" \"clim\" \"cliv\" \"cler\" \"clep\" \"clemen\" \"cleis\" \"clav\" \"class\" \"clast\" \"clar\" \"clam\" \"claim\" \"clad\" \"cit\" \"civ\" \"cirr\" \"circ\" \"cipher\" \"cip\" \"cinque\" \"kine\" \"cind\" \"cing\" \"cili\" \"cide\" \"cid\" \"chym\" \"chthon\" \"chrys\" \"chron\" \"chrom\" \"chro\" \"chres\" \"chord\" \"cord\" \"chor\" \"chondr\" \"chol\" \"choat\" \"chlor\" \"chlamy\" \"chir\" \"cheir\" \"chilo\" \"chiasma\" \"chen\" \"chem\" \"chel\" \"cheiro\" \"cheim\" \"chasm\" \"charit\" \"char\" \"chalco\" \"chet\" \"cerv\" \"cert\" \"cern\" \"crim\" \"cerebr\" \"cerc\" \"cera\" \"cer\" \"cept\" \"cephal\" \"cep\" \"centr\" \"cente\" \"cens\" \"ceno\" \"keno\" \"cen\" \"koin\" \"cell\" \"celi\" \"celeus\" \"celer\" \"celeb\" \"coel\" \"cel\" \"ceit\" \"cess\" \"ced\" \"caval\" \"cav\" \"caut\" \"caus\" \"cus\" \"caul\" \"caud\" \"cau\" \"cathedral\" \"cathar\" \"caten\" \"cast\" \"cas\" \"cary\" \"cart\" \"chart\" \"carpho\" \"carp\" \"cerp\" \"carn\" \"card\" \"carcin\" \"carc\" \"car\" \"caper\" \"capit\" \"chief\" \"capill\" \"cap\" \"ceive\" \"cant\" \"cend\" \"canc\" \"can\" \"cent\" \"campan\" \"camp\" \"camer\" \"camar\" \"camb\" \"calyp\" \"calyx\" \"calumn\" \"calli\" \"kal\" \"call\" \"calc\" \"cal\" \"caf\" \"cad\" \"cis\" \"but\" \"bust\" \"bus\" \"burs\" \"borough\" \"bull\" \"bul\" \"buk\" \"buff\" \"bucc\" \"bry\" \"bru\" \"bros\" \"bront\" \"bronch\" \"broc\" \"bris\" \"brid\" \"brev\" \"brav\" \"branchi\" \"branch\" \"brady\" \"bract\" \"brachi\" \"brais\" \"bous\" \"bound\" \"bot\" \"bos\" \"bor\" \"bon\" \"bol\" \"blephar\" \"ble\" \"blaz\" \"blast\" \"blanc\" \"bio\" \"bil\" \"bibli\" \"bib\" \"bi\" \"bis\" \"bezzl\" \"bey\" \"benth\" \"bene\" \"bell\" \"beau\" \"beat\" \"bauch\" \"bath\" \"byss\" \"bat\" \"bas\" \"bet\" \"bark\" \"barb\" \"bar\" \"bapt\" \"banq\" \"ban\" \"balm\" \"ball\" \"blem\" \"balan\" \"bail\" \"bacul\" \"bacter\" \"bac\" \"bec\" \"bacci\" \"Element\" \"axi\" \"ax\" \"avi\" \"aver\" \"aval\" \"austr\" \"aust\" \"aur\" \"aul\" \"aug\" \"aux\" \"aud\" \"auct\" \"atr\" \"atom\" \"atmo\" \"athl\" \"ather\" \"astute\" \"aster\" \"astro\" \"asper\" \"asp\" \"asc\" \"as\" \"arthro\" \"ert\" \"art\" \"arm\" \"arithm\" \"aristo\" \"argill\" \"arg\" \"aren\" \"are\" \"ard\" \"arct\" \"arch\" \"erc\" \"arc\" \"arbit\" \"arb\" \"arachn\" \"aqui\" \"aqu\" \"aps\" \"ept\" \"apex\" \"api\" \"aphrodis\" \"aph\" \"aper\" \"apat\" \"anthrop\" \"anthr\" \"anth\" \"anom\" \"annul\" \"ann\" \"enn\" \"aniso\" \"anim\" \"angui\" \"angi\" \"angel\" \"ang\" \"anem\" \"andr\" \"anc\" \"amyl\" \"amyg\" \"ampl\" \"amoeb\" \"amnio\" \"amel\" \"an\" \"amb\" \"am\" \"alv\" \"alter\" \"altr\" \"alt\" \"alpha\" \"alp\" \"aloep\" \"alm\" \"allod\" \"alli\" \"allant\" \"all\" \"alk\" \"aliph\" \"alg\" \"aleph\" \"alea\" \"alc\" \"alb\" \"alac\" \"al\" \"ailur\" \"agra\" \"agr\" \"egyr\" \"agon\" \"agog\" \"agio\" \"ag\" \"aer\" \"aecid\" \"adult\" \"adjuv\" \"adrenal\" \"adip\" \"aden\" \"adelph\" \"add\" \"actin\" \"act\" \"ig\" \"acou\" \"acu\" \"acini\" \"acid\" \"acerv\" \"ace\" \"acar\" \"acanth\" \"abb\")
  \"Global default list of etymological roots.

These defaults are used when constructing affix tables unless a table symbol
carries a :roots property specifying language-specific roots.  See
`abv-prefixes', `abv-suffixes' and `abv-infixes' for analogous defaults.\"
  :type '(repeat string)
  :group 'abv)

(defcustom abv-prefixes
  '(\"acro\" \"amph[i]\" \"an\" \"ano\" \"ap\" \"ar\" \"as\" \"at\" \"bin\" \"bis\" \"contr\" \"e\" \"ef\" \"enter\" \"ento\" \"eso\" \"extro\" \"indi\" \"intro\" \"mis\" \"per\" \"prod\" \"sans\" \"sed\" \"sesqui\" \"sum\" \"subter\" \"suc\" \"sur\" \"sy\" \"tre\" \"fore\" \"mid\" \"ob\" \"out\" \"over\" \"aero\" \"nano\" \"necro\" \"psycho\" \"radio\" \"socio\" \"sonic\" \"stereo\" \"a\" \"ab\" \"abs\" \"ad\" \"ac\" \"af\" \"ag\" \"al\" \"ana\" \"ante\" \"arch\" \"archae\" \"archeo\" \"astro\" \"auto\" \"baro\" \"be\" \"bene\" \"bi\" \"biblio\" \"bio\" \"bronto\" \"broncho\" \"cata\" \"cat\" \"cent\" \"centi\" \"circum\" \"cis\" \"co\" \"col\" \"com\" \"con\" \"cor\" \"contra\" \"counter\" \"crypto\" \"cryo\" \"cyto\" \"cardio\" \"chrono\" \"chemo\" \"chloro\" \"chiro\" \"cine\" \"cirrho\" \"clino\" \"coel\" \"cysto\" \"dacryo\" \"de\" \"deca\" \"deci\" \"dendro\" \"derm\" \"derma\" \"dia\" \"di\" \"dif\" \"dis\" \"dys\" \"dynamo\" \"eco\" \"ec\" \"electro\" \"em\" \"en\" \"entero\" \"epi\" \"eu\" \"ex\" \"extra\" \"fac\" \"femto\" \"geo\" \"glotto\" \"giga\" \"glaci\" \"gono\" \"gyno\" \"gastro\" \"gyro\" \"hemo\" \"histo\" \"hyper\" \"hypo\" \"il\" \"im\" \"in\" \"ir\" \"infra\" \"inter\" \"intra\" \"juxta\" \"kilo\" \"labio\" \"lacto\" \"leuco\" \"logo\" \"litho\" \"loco\" \"mal\" \"mega\" \"mini\" \"mono\" \"multi\" \"myo\" \"neo\" \"nephro\" \"non\" \"octa\" \"octo\" \"omni\" \"onco\" \"ortho\" \"oxy\" \"paleo\" \"pan\" \"par[a]\" \"patho\" \"penta\" \"peri\" \"philo\" \"photo\" \"phono\" \"phyto\" \"pil\" \"pili\" \"pneumo\" \"poli\" \"poly\" \"post\" \"pre\" \"proto\" \"pseudo\" \"psych\" \"pyro\" \"quad\" \"quasi\" \"quint\" \"re\" \"retro\" \"rhe[o]\" \"rhizo\" \"semi\" \"sept\" \"sex\" \"soma\" \"somato\" \"sub\" \"suf\" \"sug\" \"sup\" \"sus\" \"super\" \"supra\" \"syn\" \"sym\" \"syl\" \"tera\" \"therm\" \"thermo\" \"trans\" \"tri\" \"ultra\" \"un\" \"under\" \"uni\" \"uro\" \"vaso\" \"vice\" \"xeno\" \"xero\" \"zo\" \"zoo\" \"zoic\" \"hyperanti\" \"hypoanti\" \"panarch\" \"archaeo\" \"crypt[o]\" \"euploid\" \"hemato\" \"leuko\" \"erythro\" \"chlor[o]\" \"cyan[o]\" \"melano\" \"eryth\" \"ornitho\" \"ichthyo\" \"herb\" \"phag[o]\" \"phil\" \"phob\" \"phren\" \"somn\" \"tachy\" \"brady\" \"isoant\" \"apo\" \"allo\" \"ambi\" \"hetero\" \"homo\" \"para\" \"pro\" \"anti\" \"tele\" \"antho\" \"anthro\" \"arthr[o]\" \"arthro\" \"blast\" \"blasto\" \"blepharo\" \"brachy\" \"bronch[o]\" \"cardi[o]\" \"cephal[o]\" \"cephalo\" \"cerebro\" \"cheiro\" \"chir[o]\" \"chrom[o]\" \"cyclo\" \"cycl\" \"demi\" \"dermato\" \"dipl[o]\" \"dolicho\" \"dromo\" \"ecto\" \"endo\" \"epizo\" \"exo\" \"ferro\" \"gam\" \"gamy\" \"gene\" \"gon\" \"haplo\" \"hemi\" \"heter\" \"holo\" \"hom[o]\" \"hydro\" \"ideo\" \"idio\" \"ile\" \"intero\" \"iso\" \"karyo\" \"kerato\" \"lachry\" \"leio\" \"leuk[o]\" \"lexi\" \"lith[o]\" \"macro\" \"magn\" \"malac\" \"mast\" \"medi\" \"meso\" \"meta\" \"meter\" \"metr[o]\" \"micro\" \"milli\" \"miso\")
  \"Global default list of prefixes.

These defaults are used when constructing affix tables unless a table symbol
carries a :prefixes property specifying language-specific prefixes.\"
  :type '(repeat string)
  :group 'abv)

(defcustom abv-suffixes
  '(\"ise\" \"cene\" \"escent\" \"orama\" \"ose\" \"ple\" \"ploid\" \"able\" \"ac\" \"ocity\" \"ade\" \"age\" \"aholic\" \"oholic\" \"al\" \"algia\" \"ance\" \"ant\" \"ar\" \"ard\" \"arian\" \"arium\" \"ary\" \"ate\" \"ation\" \"ative\" \"cide\" \"cracy\" \"crat\" \"cule\" \"cy\" \"cycle\" \"dom\" \"dox\" \"ectomy\" \"ed\" \"ee\" \"eer\" \"emia\" \"en\" \"ence\" \"ency\" \"ent\" \"er\" \"ern\" \"escence\" \"ese\" \"esque\" \"ess\" \"est\" \"etic\" \"ette\" \"ful\" \"fy\" \"gam\" \"gamy\" \"gon\" \"gonic\" \"hood\" \"ial\" \"ian\" \"iasis\" \"iatric\" \"ible\" \"ic\" \"ical\" \"ile\" \"ily\" \"ine\" \"ing\" \"ion\" \"ious\" \"ish\" \"ism\" \"ist\" \"ite\" \"itis\" \"ity\" \"ive\" \"ization\" \"ize\" \"less\" \"let\" \"like\" \"ling\" \"loger\" \"logist\" \"log\" \"ly\" \"ment\" \"ness\" \"oid\" \"ology\" \"oma\" \"onym\" \"opia\" \"opsy\" \"or\" \"ory\" \"osis\" \"ostomy\" \"otomy\" \"ous\" \"path\" \"pathy\" \"phile\" \"phobia\" \"phone\" \"phyte\" \"plegia\" \"plegic\" \"pnea\" \"scopy\" \"scope\" \"scribe\" \"script\" \"sect\" \"ship\" \"sion\" \"some\" \"sophy\" \"sophic\" \"th\" \"tion\" \"tome\" \"tomy\" \"trophy\" \"tude\" \"ty\" \"ular\" \"uous\" \"ure\" \"ward\" \"ware\" \"wise\" \"y\")
  \"Global default list of suffixes.

These defaults are used when constructing affix tables unless a table symbol
carries a :suffixes property specifying language-specific suffixes.\"
  :type '(repeat string)
  :group 'abv)

(defcustom abv-infixes nil
  \"Global default list of infixes (rare).

When constructing affix tables, use this as a fallback only if the table symbol
has no :infixes property.\"
  :type '(repeat string)
  :group 'abv)

(provide 'abv-data)
;;; abv-data.el ends here
"))

nil
#+end_tool
#+begin_tool (modify_buffer :buffer "abv-corpus.el" :content ";; ...)
(:name "modify_buffer" :args (:buffer "abv-corpus.el" :content ";;; abv-corpus.el --- Corpus analysis for abbrevs -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: abbrev, convenience
;; License: GPL-3+

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Corpus helpers for generating affix statistics used by daselt-abv.

;;; Code:

(require 'cl-lib)

(defgroup abv nil
  \"Abbreviation and affix utilities.\"
  :group 'convenience
  :prefix \"abv-\")

(defcustom abv-corpus-file-types
  '(\"org\" \"txt\" \"md\" \"tex\")
  \"File extensions to scan when computing a personal corpus.\"
  :type '(repeat string)
  :group 'abv)

(defcustom abv-corpus-file
  (expand-file-name \"abv/corpus.el\" user-emacs-directory)
  \"Destination file for serialized corpus statistics.
A Lisp file containing a printed alist of (word . count).\"
  :type 'file
  :group 'abv)

(defun daselt-abv-personal-corpus (&optional dir)
  \"Create a personal word-frequency statistic by scanning files in DIR.

Recurse through DIR (defaults to the user\\='s home directory) using
`daselt-dirs-act-on-pkg-files-by-type'.  For each file whose extension is in
`abv-corpus-file-types', count the occurrences of each word (case-insensitive)
and aggregate counts.  The resulting association list of (word . COUNT) pairs
is written to `abv-corpus-file'.  Finally, call `daselt-abv-read-statistics'.\"
  (interactive)
  (let* ((start-dir (file-name-as-directory (expand-file-name (or dir \"~\"))))
         (exts (mapcar #'downcase abv-corpus-file-types))
         (table (make-hash-table :test #'equal))
         ;; Predicate for daselt-dirs to select files by extension
         (pred (lambda (filepath)
                 (let ((ext (file-name-extension filepath)))
                   (and ext (member (downcase ext) exts))))))
    ;; Traverse and count
    (daselt-dirs-act-on-pkg-files-by-type
     (list (cons (lambda (filepath)
                   (daselt-abv--count-words-into filepath table)
                   nil)
                 pred))
     start-dir)
    ;; Persist as a sorted alist for readability
    (let (alist)
      (maphash (lambda (k v) (push (cons k v) alist)) table)
      (setq alist (sort alist (lambda (a b) (> (cdr a) (cdr b)))))
      (let ((file abv-corpus-file))
        (let ((dir (file-name-directory file)))
          (when dir (ignore-errors (make-directory dir t))))
        (with-temp-file file
          (let ((print-length nil)
                (print-level nil))
            (prin1 alist (current-buffer))))))
    (daselt-abv-read-statistics)))

(defun abv-corpus--optional-letter-regex (s)
  \"Return a regex for S with square-bracketed letters optional.

Example: turn the string \\=\"ab[c]d\\=\" into a regex that matches \\=\"abcd\\=\" or
\\=\"abd\\=\".  Only simple single-letter bracket groups like [x] are supported.\"
  (let ((start 0) (out \"\"))
    (while (string-match \"\\\\[\\\\([[:alpha:]]\\\\)\\\\]\" s start)
      (setq out (concat out
                        (regexp-quote (substring s start (match-beginning 0)))
                        (match-string 1 s)
                        \"?\"))
      (setq start (match-end 0)))
    (setq out (concat out (regexp-quote (substring s start))))
    out))

(defun abv-corpus-ngram-frequencies (prefixes corpus n &optional posn)
  \"Return an N-deep nested list of n-gram counts for PREFIXES in CORPUS.

Leaves are conses of the form (PSEQ . COUNT) where PSEQ is a list of N
prefix strings and COUNT the number of CORPUS entries starting with their
concatenation.  Square-bracketed letters in PREFIXES are optional via
`abv-corpus--optional-letter-regex'.

If POSN is the string \\='initial, anchor the regex at string-start; if POSN is
\\='final, anchor at string-end.  CORPUS can be a list of strings or of
conses (word . data), in which case the word is used.\"
  (let* ((initial (and (stringp posn) (string= posn \"initial\")))
         (final (and (stringp posn) (string= posn \"final\")))
         (pieces (let ((ht (make-hash-table :test 'equal)))
                   (dolist (p prefixes) (puthash p (abv-corpus--optional-letter-regex p) ht))
                   ht))
         (words  (mapcar (lambda (e) (if (consp e) (car e) e)) corpus)))
    (cl-labels
        ((concat-re (pseq)
           (concat (if initial (rx string-start))
                   (mapconcat (lambda (p) (gethash p pieces)) pseq \"\")
                   (if final (rx string-end))))
         (count-for (pseq)
           (let* ((re (concat-re pseq)) (c 0))
             (dolist (w words) (when (string-match-p re w) (setq c (1+ c))))
             (cons pseq c)))
         (build (k acc)
           (if (zerop k)
               (count-for (nreverse acc))
             (cl-remove-if
              (lambda (cns)
                (= 0 (cdr cns)))
              (daselt-base-flatten-until (mapcar (lambda (p)
                                                   (build (1- k) (cons p acc)))
                                                 prefixes)
                                         (lambda (lt)
                                           (numberp (cdar lt))))))))
      (build n nil))))

(defun abv-corpus-reduced-monogram-frequencies (affixes corpus &optional posn)
  \"Return monogram frequencies reduced by counts of their extensions.

Uses `abv-corpus-ngram-frequencies' with N=1 to obtain base counts for each
AFFIX in AFFIXES.  For each AFFIX P the returned count is
  max(0, count(P) - sum_{Q in AFFIXES, Q != P and Q starts-with P} count(Q))
This gives the number of CORPUS entries that match P but not any longer affix
that extends P.

The return value follows the same shape as
`abv-corpus-ngram-frequencies' for N=1: a list of ((P) . COUNT).\"
  (let* ((mono (abv-corpus-ngram-frequencies affixes corpus 1 posn))
         (counts (let ((ht (make-hash-table :test #'equal)))
                   (dolist (entry mono)
                     ;; entry is ((p) . count)
                     (puthash (car (car entry)) (cdr entry) ht))
                   ht))
         (result nil))
    (dolist (entry mono)
      (let* ((p (car (car entry)))
             (c (gethash p counts 0))
             (sum-ext 0))
        (dolist (q affixes)
          (when (and (not (equal p q))
                     (string-match-p p q))
            (setq sum-ext (+ sum-ext (gethash q counts 0)))))
        (push (cons (list p) (max 0 (- c sum-ext))) result)))
    (nreverse (sort result (lambda (a b) (> (cdr a) (cdr b)))))))

(defun daselt-abv-hash->def-form (var hash &optional doc)
  \"Return a defconst form that binds VAR to a hash table equal to HASH.
DOC is an optional docstring.\"
  (let ((pairs nil))
    (maphash (lambda (k v) (push (cons k v) pairs)) hash)
    ;; Stable order for reproducible diffs
    (setq pairs
          (cl-sort pairs
                   (lambda (a b)
                     (string< (prin1-to-string (car a))
                              (prin1-to-string (car b))))))
    `(defconst ,var
       ,(let ((ht (make-symbol \"ht\")))
          `(let ((,ht (make-hash-table
                       :test ',(hash-table-test hash)
                       :size ,(hash-table-size hash)
                       :rehash-size ,(hash-table-rehash-size hash)
                       :rehash-threshold ,(hash-table-rehash-threshold hash)
                       ,@(when (hash-table-weakness hash)
                           `(:weakness ,(hash-table-weakness hash))))))
             ,@(mapcar (lambda (kv)
                         `(puthash ',(car kv) ',(cdr kv) ,ht))
                       pairs)
             ,ht))
       ,(or doc \"\"))))

(defun abv-corpus-print-table (sym)
  \"Insert into current buffer a defconst form reconstructing table SYM.

This serializes the hash-table bound to SYM into a `defconst' form using
`daselt-abv-hash->def-form'.  The text is added at point and then indented.\"
  (interactive
   (list (intern (completing-read \"Table symbol: \" obarray #'boundp t nil nil \"daselt-abv-\"))))
  (let ((print-level nil)
        (print-length nil))
    (insert (string-replace
             \"\\(let\" \"\\n\\(let\"
             (string-replace
              \" \\\"\\\"\" \"\"
              (string-replace
               \") (\" \")\\n\\(\"
               (format \"%S\" (daselt-abv-hash->def-form sym (symbol-value sym)))))))
    (save-excursion
      (indent-region (point)
                     (prog2 (forward-sexp)
                         (point))))))

(provide 'abv-corpus)
;;; abv-corpus.el ends here
"))

nil
#+end_tool
#+begin_tool (function_documentation :function "daselt-bind-bind ...)
(:name "function_documentation" :args (:function "daselt-bind-bindlist-p"))

Return t if CAND is a bindlist.

The way used to test this is by recursing through CAND until a binding is
found.

(fn CAND)
#+end_tool
#+begin_tool (function_documentation :function "daselt-bind-sort ...)
(:name "function_documentation" :args (:function "daselt-bind-sort-and-format-bindlist"))

Sort a daselt-xkb bindlist BLIST and format the result.

Key combinations that are not matched by the layout in ‘daselt-xkb-layout’ are
put at the very top (because they are most likely errors or depreciated).

Modifiers are ordered according to their order in MODLIST
(daselt-bind-modifiers-list by default). Sets of modifiers are ordered
according to the modifier in them the furthest back in MODLIST and so are added
below the modifier the furthest back.

Key combinations given by a full combination string are listed before others and
are ordered alphabetically (since they are supposed to be recalled phonetically
or lexically, not positionally). Key combination strings with capital characters
appear after those with downcased characters, and Greek letters appear after
Latin ones. Combinations with symbols that are neither appear after either and
are not otherwise sorted.

Keys are ordered according to their layer, row and place in the row.

With optional argument COORDSONLY, the function translates bindings that are
given by a binding string into ones given by coordinates if that is possible,
i.e. if the end of the binding string corresponds to a signal in the
‘daselt-xkb-layout’.

PREFUN is a function that is applied to the bindlist after it is transformed
into an elaborate bindlist. This is useful to apply functions that should act on
an elaborate bindlist and whose results should be sorted, like coordinate
transformations.

To allow for using this function in ‘d--recursively-act-on-bindlist’, it checks
if the input is an atom or nil and, if so, it returns the input.

This function is declared as side-effect-free, so please don’t use PREFUNS with
side effects.

(fn BLIST &optional COORDSONLY PREFUN MODLIST)
#+end_tool
#+begin_tool (function_completions :function_prefix "daselt-abv-")
(:name "function_completions" :args (:function_prefix "daselt-abv-"))

daselt-abv-read-statistics
daselt-abv-personal-statistics
daselt-abv--affix-letters
daselt-abv-valid-prefix-symbols
daselt-abv--remove-capitals
daselt-abv--choose-fallback
daselt-abv--free-candidates
daselt-abv--erg-score
daselt-abv--filter-to-upper-lower-pairs
daselt-abv-placevals-from-classes
daselt-abv-hash->def-form
daselt-abv--count-words-into
daselt-abv--letter-p
daselt-abv--filter-to-lower-layers
daselt-abv-ngram-frequencies
daselt-abv--filter-to-upper-lower-paired-symbols
daselt-abv--down
daselt-abv--sorted-affixes-by-frequency
daselt-abv-add-to-temporary-word-constituents
daselt-abv--coord-layers-in-order
daselt-abv--placevals->coord-hash
daselt-abv--letter-rarity
daselt-abv--complement-hash
daselt-abv--alist->hash
daselt-abv--collect-letter-bases
daselt-abv-personal-corpus
daselt-abv--neighbors
daselt-abv--keys
daselt-abv-assign-affixes
daselt-abv--assign-one
daselt-abv--filter-to-upper-lower-paired-placevals
#+end_tool
#+begin_tool (function_documentation :function "daselt-abv-assig ...)
(:name "function_documentation" :args (:function "daselt-abv-assign-affixes"))

Compute and set daselt-abv-prefix-table and daselt-abv-suffix-table.
#+end_tool
#+begin_tool (modify_buffer :buffer "daselt-abv.el" :content ";; ...)
(:name "modify_buffer" :args (:buffer "daselt-abv.el" :content ";;; daselt-abv.el --- Affix assignment to Daselt coordinates -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: abbrev, convenience
;; License: GPL-3+

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Tools to generate affix tables assigned to Daselt keyboard coordinates,
;; convert such tables into bindlists, visualize and query them, and persist
;; them to disk.

;;; Code:
(require 'cl-lib)
(require 'abv-data)
(require 'abv-corpus)

(defgroup daselt-abv nil
  \"Daselt helpers for assigning and visualizing affix placements.\"
  :group 'abv
  :prefix \"daselt-abv-\")

(defcustom daselt-abv-coordinate-supply-function nil
  \"Function used to supply a preferred order of coordinates for assignment.

The function is called with no arguments and must return a proper list of
coordinates (each coordinate is typically a list like (LAYER ROW COL), but
only a stable `equal' identity is required).  The order of the list indicates
assignment priority.

If nil, try to use an implementation-provided `daselt-abv--coord-layers-in-order'
if available, otherwise signal an error asking the user to customize this option.\"
  :type '(choice (const :tag \"Use implementation default\" nil)
                 (function :tag \"Supplier function\"))
  :group 'daselt-abv)

(defcustom daselt-abv-save-file
  (let ((dir (ignore-errors (and (boundp 'daselt-mode-pkg-configs-directory)
                                 (file-name-as-directory daselt-mode-pkg-configs-directory)))))
    (expand-file-name (concat (or dir (file-name-as-directory user-emacs-directory))
                              \"abv/save.el\")))
  \"File into which to save generated affix tables.

The file will contain `defconst' forms for all variables whose names match
\"^daselt-abv-.*-table$\".  Existing content is overwritten.\"
  :type 'file
  :group 'daselt-abv)

(defun daselt-abv--ensure-hash-table (sym)
  \"Ensure SYM is bound to a hash-table and return it.

If SYM is unbound or not a hash-table, bind it to a fresh hash-table with
`equal' test.\"
  (unless (and (boundp sym) (hash-table-p (symbol-value sym)))
    (set sym (make-hash-table :test #'equal)))
  (symbol-value sym))

(defun daselt-abv--normalize-frequencies (freqs)
  \"Normalize FREQS into an alist of (AFFIX . COUNT).

Accepts the following shapes:
- alist of (string . integer)
- alist of ((string) . integer) as from `abv-corpus-reduced-monogram-frequencies'
- alist of (symbol . integer)
Return a list of conses with string keys. Entries with non-positive counts are
removed.\"
  (cl-loop for (k . v) in freqs
           for key = (cond
                      ((stringp k) k)
                      ((and (consp k) (stringp (car k))) (car k))
                      ((symbolp k) (symbol-name k))
                      (t nil))
           when (and key (integerp v) (> v 0))
           collect (cons key v)))

(defun daselt-abv--coordinate-supply ()
  \"Return a list of coordinates representing placement priority.

Preference order:
1. User-supplied `daselt-abv-coordinate-supply-function' if non-nil.
2. Implementation-provided `daselt-abv--coord-layers-in-order' (if fboundp),
   flattened left-to-right.
Signal an error if no source is available.\"
  (cond
   ((functionp daselt-abv-coordinate-supply-function)
    (funcall daselt-abv-coordinate-supply-function))
   ((fboundp 'daselt-abv--coord-layers-in-order)
    (let* ((layers (ignore-errors (daselt-abv--coord-layers-in-order)))
           (flat (cl-loop for layer in layers append layer)))
      flat))
   (t (user-error \"No coordinate supply available. Customize daselt-abv-coordinate-supply-function\"))))

(defun daselt-abv--collect-used-coords (table)
  \"Return a list of coordinates already used in TABLE (hash-table).\"
  (let (out)
    (maphash (lambda (_k v)
               (when v (push v out)))
             table)
    out))

(defun daselt-abv--affix-kind-for-table (sym)
  \"Heuristically determine affix kind for table symbol SYM.
Return one of the symbols 'prefixes, 'suffixes, 'infixes, 'roots or nil.\"
  (let* ((name (symbol-name sym))
         (base (and (string-match \"^daselt-abv-\\\\(.*?\\\\)-table$\" name)
                    (match-string 1 name))))
    (cond
     ((and base (string-match-p \"^prefix\" base)) 'prefixes)
     ((and base (string-match-p \"^suffix\" base)) 'suffixes)
     ((and base (string-match-p \"^infix\" base)) 'infixes)
     ((and base (string-match-p \"^root\" base)) 'roots)
     (t nil))))

(defun daselt-abv--option-var-for-table (sym)
  \"Return the abv option variable symbol corresponding to table SYM.
Supported mappings are: prefixes, suffixes, infixes, roots.\"
  (pcase (daselt-abv--affix-kind-for-table sym)
    ('prefixes 'abv-prefixes)
    ('suffixes 'abv-suffixes)
    ('infixes  'abv-infixes)
    ('roots    'abv-roots)
    (_ nil)))

;;;###autoload
(defun daselt-abv-assign-affixes (affixes frequencies table-sym)
  \"Assign AFFIXES to Daselt coordinates by FREQUENCIES into TABLE-SYM.

AFFIXES is a list of affix strings (e.g., `abv-prefixes').  FREQUENCIES is an
alist mapping affix to count, or a list of ((affix) . count) pairs as returned
by `abv-corpus-reduced-monogram-frequencies'.  TABLE-SYM is the symbol of the
hash-table to populate (e.g., 'daselt-abv-prefix-table).

If TABLE-SYM is already bound to a hash-table, extend it without overwriting
existing entries.  The value for each key is the coordinate tuple assigned to
that affix.  Coordinates are chosen in the order given by
`daselt-abv-coordinate-supply-function' or implementation defaults.

This function does not mutate the AFFIXES list or the FREQUENCIES input.  It
may attach metadata to TABLE-SYM via symbol properties :prefixes, :suffixes,
:infixes or :roots to reflect the affix kind inferred from the symbol name.\"
  (interactive
   (let* ((table (intern (completing-read \"Affix table symbol: \" obarray
                                         (lambda (s)
                                           (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s)))
                                         nil nil nil \"daselt-abv-prefix-table\")))
          (kind (daselt-abv--affix-kind-for-table table))
          (opt (or (daselt-abv--option-var-for-table table) 'abv-prefixes))
          (affs (symbol-value opt))
          (freqs (condition-case _
                     (abv-corpus-reduced-monogram-frequencies affs (symbol-value 'daselt-abv-personal-corpus) 'initial)
                   (error (mapcar (lambda (a) (cons a 1)) affs)))))
     (list affs freqs table)))
  (unless (and (listp affixes) (listp frequencies) (symbolp table-sym))
    (user-error \"Invalid inputs: expected (list list symbol)\"))
  (let* ((norm (daselt-abv--normalize-frequencies frequencies))
         (freq (let ((ht (make-hash-table :test #'equal)))
                 (dolist (c norm) (puthash (car c) (cdr c) ht))
                 ht))
         (table (daselt-abv--ensure-hash-table table-sym))
         ;; record existing coords and keys
         (used-coords (daselt-abv--collect-used-coords table))
         (used (let ((ht (make-hash-table :test #'equal)))
                 (maphash (lambda (k _v) (puthash k t ht)) table)
                         ; nil table var
                         table)
                 ht))
         (supply (daselt-abv--coordinate-supply))
         (get-next-coord (lambda ()
                           (catch 'found
                             (while supply
                               (let ((c (car supply)))
                                 (setq supply (cdr supply))
                                 (unless (cl-find c used-coords :test #'equal)
                                   (push c used-coords)
                                   (throw 'found c))))
                             nil)))
         ;; produce list of affixes present in AFFIXES, sorted by frequency desc
         (sorted (cl-sort (cl-remove-if-not (lambda (a) (gethash a freq 0))
                                            (cl-remove-duplicates affixes :test #'equal))
                          #'> :key (lambda (a) (gethash a freq 0)))))
    ;; Attempt to attach kind-specific property for transparency
    (let ((kind (daselt-abv--affix-kind-for-table table-sym)))
      (when kind
        (put table-sym (intern (format \":%s\" (symbol-name kind))) affixes)))
    (dolist (a sorted)
      (unless (gethash a table)
        (let ((coord (funcall get-next-coord)))
          (when coord
            (puthash a coord table)))))
    table))

(defun daselt-abv--table->bindlist-var (table-sym)
  \"Return the symbol to use for the bindlist variable from TABLE-SYM.\"
  (intern (replace-regexp-in-string \"-table$\" \"-bindlist\" (symbol-name table-sym))))

(defun daselt-abv-table->bindlist (table-sym)
  \"Convert TABLE-SYM (an affix table) into a bindlist and define a variable.

The variable name is derived by replacing the trailing \"-table\" with
\"-bindlist\" in TABLE-SYM.  Each binding in the returned bindlist has the form
(COORDS . LABELSYM), where COORDS is the coordinate tuple from the table and
LABELSYM is an interned symbol mirroring the affix string (e.g., \"pre\" ->
'abv-pre).  The variable is created or replaced using `set'.  Return the
bindlist.\"
  (unless (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
    (user-error \"Table %s is not bound to a hash-table\" table-sym))
  (let* ((tbl (symbol-value table-sym))
         (blist nil))
    (maphash (lambda (k v)
               (let* ((aff (if (stringp k) k (format \"%s\" k)))
                      (sym (intern (concat \"abv-\" aff)))
                      (coords v))
                 (push (cons coords sym) blist)))
             tbl)
    (setq blist (nreverse blist))
    (set (daselt-abv--table->bindlist-var table-sym) blist)
    blist))

;;;###autoload
(defun daselt-abv-draw-table-layer (table-sym laycoord &rest mods)
  \"Visualize one layer of TABLE-SYM by delegating to `daselt-bind-draw-bindlist-layer'.

Ensure a bindlist exists for TABLE-SYM by converting if necessary, then draw
layer LAYCOORD with optional MODS.\"
  (interactive
   (let* ((table (intern (completing-read \"Affix table symbol: \" obarray
                                         (lambda (s)
                                           (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                                                (hash-table-p (symbol-value s))))
                                         t)))
          (lay (read (read-from-minibuffer \"Layer coordinate (e.g., (0)): \"))))
     (list table lay)))
  (when (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
    ;; ensure bindlist
    (unless (and (boundp (daselt-abv--table->bindlist-var table-sym))
                 (daselt-bind-bindlist-p (symbol-value (daselt-abv--table->bindlist-var table-sym))))
      (daselt-abv-table->bindlist table-sym))
    (apply #'daselt-bind-draw-bindlist-layer (daselt-abv--table->bindlist-var table-sym) laycoord mods)))

(defun daselt-abv--table-symbols ()
  \"Return a list of symbols bound to affix tables named ^daselt-abv-.*-table$.\"
  (let (out)
    (mapatoms (lambda (s)
                (when (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                           (boundp s) (hash-table-p (symbol-value s)))
                  (push s out))))
    (nreverse out)))

;;;###autoload
(defun daselt-abv-query-table (table-sym query)
  \"Query TABLE-SYM for QUERY affix and maybe add to defining option.

Prompt to choose TABLE-SYM among variables named \"daselt-abv-.*-table\", then
query for key QUERY.  If present, display its coordinates.  If absent, ask
whether to add QUERY to the option variable that defined the table (e.g.,
`abv-prefixes' for the \"daselt-abv-prefix-table\").  The addition is performed
via `customize-push-and-save' if available.\"
  (interactive
   (let* ((table (intern (completing-read \"Affix table: \" (daselt-abv--table-symbols) nil t)))
          (q (read-string (format \"Affix for %s: \" table))))
     (list table q)))
  (unless (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
    (user-error \"Not a bound affix table: %s\" table-sym))
  (let* ((tbl (symbol-value table-sym))
         (coords (gethash query tbl)))
    (if coords
        (progn (message \"%s => %S\" query coords) coords)
      (let* ((opt (daselt-abv--option-var-for-table table-sym))
             (can (and opt (boundp opt))))
        (if (not can)
            (message \"No matching option variable for %s\" table-sym)
          (when (yes-or-no-p (format \"Add %S to %S and save? \" query opt))
            (if (fboundp 'customize-push-and-save)
                (customize-push-and-save opt query)
              ;; Fallback
              (customize-set-variable opt (append (symbol-value opt) (list query)))
              (customize-save-customized))
            (message \"Added %S to %S\" query opt))
          nil)))))

;;;###autoload
(defun daselt-abv-save (&optional file)
  \"Save all tables named \"daselt-abv-.*-table\" into FILE.

FILE defaults to `daselt-abv-save-file'.  This overwrites existing contents.
Tables are serialized using `abv-corpus-print-table'.\"
  (interactive)
  (let ((outfile (or file daselt-abv-save-file)))
    (let ((dir (file-name-directory outfile)))
      (when dir (ignore-errors (make-directory dir t))))
    (with-temp-file outfile
      (insert \";;; Saved daselt-abv tables\\n\\n\")
      (dolist (s (daselt-abv--table-symbols))
        (insert (format \";;; %s\\n\" s))
        (abv-corpus-print-table s)
        (insert \"\\n\\n\")))
    (message \"Saved %d tables to %s\" (length (daselt-abv--table-symbols)) outfile)))

(provide 'daselt-abv)
;;; daselt-abv.el ends here
"))

error error: buffer daselt-abv.el is not live.
#+end_tool
#+begin_tool (read_buffer :buffer "daselt-abv.del")
(:name "read_buffer" :args (:buffer "daselt-abv.del"))

;;; daselt-abv.del --- Daselt's abv module  -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: tools

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;

;;; Code:

(require 'daselt-dirs)
(require 'cl-lib)

;;;; Options
(defgroup daselt-abv
          nil
          ""
          :group 'daselt-mode)

(defcustom daselt-abv-non-constituents
              '(?- ?_ ?: ?. ?, ?! ?? ?' ?`)
              "List of characters in the Daselt-layout that are not added to
`abv-temporary-word-constituents'.

It is not necessary to add non-characters, such as command keys. It is also not
necessary to add brackets and such."
              :type '(repeat character)
              :group 'daselt-mode)

(defcustom daselt-abv-single-symbol-placevals
  (let ((print-level nil) (print-length nil))
    (cl-remove-if
     (lambda (cons)
       (or (< 1 (length (cdr cons)))
           (let* ((char (string-to-char (cdr cons)))
                  (syx (with-syntax-table text-mode-syntax-table (char-syntax char))))
             (cl-member syx '(?\  ?\- ?\") :test #'=))))
     (daselt-base-flatten-n-times (cdr (daselt-coords-coordinatize-layout
                                        (symbol-value daselt-dfk-layout))) 2)))
  "Placevals used for single-symbol abbrevs."
  :group 'daselt-mode)

(defcustom daselt-abv-prefix-classes
  '(?w ?_ ?. ?\) ?\> ?\!)
  "Syntax classes used for multi-symbol abbrevs."
  :group 'daselt-mode)

(defcustom daselt-abv-suffix-classes
  '(?w ?_ ?\( ?\< ?\!)
  "Syntax classes used for multi-symbol abbrevs."
  :group 'daselt-mode)

(defcustom daselt-abv-infix-classes
  '(?w ?_ ?. ?\( ?\< ?\) ?\> ?\!)
  "Syntax classes used for multi-symbol abbrevs."
  :group 'daselt-mode)

(defcustom daselt-abv-standalone-classes
  '(?w ?_ ?. ?\( ?\< ?\) ?\> ?\!)
  "Syntax classes used for multi-symbol abbrevs."
  :group 'daselt-mode)

(defcustom daselt-abv-root-classes
  '(?w ?_)
  "Syntax classes used for multi-symbol abbrevs."
  :group 'daselt-mode)

(defcustom daselt-abv-layer-pairs
              '((1 . 2) (4 . 3) (5 . 6) (8 . 7))
              ""
              :type '(repeat integer)
              :group 'daselt-abv)

(defcustom daselt-abv-special-symbols
          '(?. ?,)
          "Special symbols that should not be used for roots and affixes.

By default, we are using \".\" for grammar delimitation and \",\" for
yasnippet."
          :type '(repeat character)
          :group 'daselt-abv)

(defcustom daselt-abv-special-non-prefix-symbols
  nil
  "Special symbols that should not be used for prefixes."
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-suffix-symbols
      '(?↔)
      "Special symbols that should not be used for suffixes.

By default we are using \"↔\" to disambiguate names."
      :type '(repeat character)
      :group 'daselt-abv)

(defcustom daselt-abv-added-syntax-classes
  '(?_)
  "Syntax classes that are added to `abv-temporary-word-constituents' by Daselt.

By default only consists of the syntax class ?_, which designates symbols."
  :type '(repeat character)
  :group 'daselt-mode)


;; Statistics configuration
(defcustom daselt-abv-statistics-file-types
  '("txt" "org" "md" "el" "del")
  "List of filename extensions to include when computing personal statistics.

Each element is a string like \"txt\" or \"org\". Matching is case-insensitive."
  :type '(repeat string)
  :group 'daselt-mode)

(defcustom daselt-abv-statistics-file
                (expand-file-name "daselt-abv-stats.el" user-emacs-directory)
                "File where personal word-frequency statistics are written and read from."
                :type 'file
                :group 'daselt-mode)




;;;; Variables
(defvar daselt-abv-valid-prefix-placevals
                              (cl-remove-if (lambda (cns)
                                              (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-prefix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-prefix-classes)))

(defvar daselt-abv-valid-lower-paired-prefix-placevals
  (cl-intersection daselt-abv-valid-prefix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
                   :test #'equal))

(defvar daselt-abv-valid-suffix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-suffix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-suffix-classes)))

(defvar daselt-abv-valid-lower-paired-suffix-placevals
  (cl-intersection daselt-abv-valid-suffix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
                   :test #'equal))




(defvar daselt-abv-temporary-word-constituents-backup
                          abv-temporary-word-constituents)

(defvar daselt-abv-prefix-table)
(defvar daselt-abv-suffix-table)

;;;; Functions
(defun daselt-abv-placevals-from-classes (classes)
  (let ((print-level nil) (print-length nil))
    (-filter
     (lambda (cons)
       (and (= 1 (length (cdr cons)))
            (let* ((char (string-to-char (cdr cons)))
                   (syx (with-syntax-table org-mode-syntax-table (char-syntax char))))
              (cl-member syx classes :test #'=))))
     (daselt-base-flatten-n-times
      (cdr (daselt-coords-coordinatize-layout
            (symbol-value daselt-dfk-layout))) 2))))

(defun daselt-abv--filter-to-lower-layers (placevals)
  ""
  (-filter (lambda (cns)
             (cl-member (caar cns) (mapcar #'car daselt-abv-layer-pairs)))
           placevals))

(defun daselt-abv--filter-to-upper-lower-paired-placevals (placevals)
  ""
  (-filter (lambda (cns)
             (let* ((coords (car cns))
                    (uppercoords (list (1+ (alist-get (car coords)
                                                      daselt-abv-layer-pairs))
                                       (nth 1 coords)
                                       (nth 2 coords))))
               (not (< 1 (length (daselt-coords-binding uppercoords))))))
           (daselt-abv--filter-to-lower-layers placevals)))

(defun daselt-abv-add-to-temporary-word-constituents ()
  "Add characters from `daselt-xkb-layout' to `abv-temporary-word-constituents'.

Adds characters whose syntax-class is in `daselt-abv-added-syntax-classes'.

Excludes symbols from `daselt-abv-non-constituents'."
  (declare (ftype (function () t)))
  (setopt abv-temporary-word-constituents
          (cl-union daselt-abv-temporary-word-constituents-backup
                    (let ((laylist (flatten-list (symbol-value daselt-xkb-layout))))
                      (remq
                       nil (mapcar
                            (lambda (entry)
                              (if (= 1 (length entry))
                                  (let ((char (string-to-char entry)))
                                    (if (and (or (memq (char-syntax char)
                                                       daselt-abv-added-syntax-classes))
                                             (not (cl-member
                                                   char
                                                   daselt-abv-non-constituents
                                                   :test #'=)))
                                        char))))
                            laylist)))))
  (abv--generate-syntax-table))

;;;;; Personal statistics

(defvar daselt-abv-statistics-obarray (make-vector 4096 0)
            "Obarray holding personal word-frequency statistics.

Symbols are interned for words; their symbol-values hold the counts.")

(defun daselt-abv--count-words-into (file table)
  "Count words in FILE and accumulate into hash TABLE.

Words are sequences that match the regexp \"\\(\\sw\\|\\s_\\)+\". Counting is
case-insensitive. Files that cannot be decoded or read are skipped."
  (when (and (stringp file) (file-readable-p file))
    (condition-case _err
        (with-temp-buffer
          ;; Use automatic decoding; avoid visiting to keep things light-weight.
          (insert-file-contents file nil nil nil t)
          (goto-char (point-min))
          (let ((case-fold-search t))
            (while (re-search-forward "\\(\\sw\\|\\s_\\)+" nil t)
              (let* ((w (downcase (match-string-no-properties 0)))
                     (old (gethash w table 0)))
                (puthash w (1+ old) table)))))
      (error nil))))

(defun daselt-abv-read-statistics ()
  "Read `daselt-abv-statistics-file' into an obarray.

Returns the newly created obarray stored in `daselt-abv-statistics-obarray'."
  (interactive)
  (setq daselt-abv-statistics-obarray (make-vector 4096 0))
  (when (file-readable-p daselt-abv-statistics-file)
    (with-temp-buffer
            (insert-file-contents daselt-abv-statistics-file)
            (goto-char (point-min))
            (condition-case _err
                      (let ((data (read (current-buffer))))
            (dolist (pair data)
              (when (and (consp pair)
                         (stringp (car pair))
                         (numberp (cdr pair)))
                (let ((sym (intern (car pair) daselt-abv-statistics-obarray)))
                  (set sym (cdr pair))))))
        (error nil))))
  daselt-abv-statistics-obarray)

;;;;; Assignment
(defun daselt-abv--alist->hash (alist &optional key-fn)
    (let ((ht (make-hash-table :test 'equal)))
    (dolist (kv alist ht)
      (let ((k (if key-fn (funcall key-fn (car kv)) (car kv)))
            (v (cdr kv)))
        (puthash k v ht)))))

(defun daselt-abv--placevals->coord-hash (placevals)
  "Return hash (coord -> binding) for PLACEVALS where placeval = ((L r c) .

binding)."
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (pv placevals ht)
      (puthash (car pv) (cdr pv) ht))))

(defun daselt-abv--keys (hash)
  (let (ks)
    (maphash (lambda (k _v) (push k ks)) hash)
    ks))

(defun daselt-abv--letter-p (s)
  (and (stringp s) (= (length s) 1)
       (let ((c (aref s 0)))
         (or (and (<= ?a c) (<= c ?z))
             (and (<= ?A c) (<= c ?Z))))))

(defun daselt-abv--down (s)
  (if (and (stringp s) (= (length s) 1))
      (downcase s)
    s))

(defun daselt-abv--erg-score (coord)
  "Lower is better. Heuristic per main_layout_heuristics.org."
  (pcase-let ((`(,L ,row ,col) coord))
    (let* ((abscol (abs col))
           (row-pen (cond ((= row 0) 0.0)
                          (t 0.6))) ;; top/bottom small penalty
           (col-pen (pcase abscol
                      (1 1.0) (2 1.2) (3 1.5) (4 2.0) (5 3.0) (6 4.0)
                      (_ 5.0)))
           (layer-pen (pcase L
                        (1 0.0)
                        (2 0.2) (4 0.2)
                        (6 0.4) (5 0.4)
                        (7 0.6) (3 0.6)
                        (8 0.8)
                        (_ 1.0))))
      (+ row-pen col-pen layer-pen))))

(defun daselt-abv--coord-layers-in-order (complementp)
  "Order layers by ease. If complementp, prefer 2 before 1 (guide)."
  (let ((base '(1 2 4 6 5 7 3 8)))
    (if complementp
        '(2 1 4 6 5 7 3 8)
      base)))

(defun daselt-abv--neighbors (r c layers)
  "Produce coords around (r,c) across LAYERS in the guide's order."
  (let ((coords '()))
    ;; same row/col across layers
    (dolist (L layers) (push (list L r c) coords))
    ;; adjacent rows then across layers
    (dolist (dr '(-1 1))
      (dolist (L layers) (push (list L (+ r dr) c) coords)))
    ;; column-adjacent then across layers
    (dolist (dc '(-1 1))
      (dolist (L layers) (push (list L r (+ c dc)) coords)))
    (nreverse coords)))

(defun daselt-abv--collect-letter-bases (placevals)
  "Map lowercase letter -> one base coord, prefer layer 1 if present."
  (let ((per-letter (make-hash-table :test 'equal)))
    ;; collect all coords by letter
    (dolist (pv placevals)
      (let* ((coord (car pv)) (binding (daselt-abv--down (cdr pv))))
        (when (daselt-abv--letter-p binding)
          (push coord (gethash binding per-letter)))))
    ;; pick best coord per letter, prefer layer 1, else minimal layer/erg score
    (let ((out (make-hash-table :test 'equal)))
      (maphash
       (lambda (letter coords)
         (let* ((on-l1 (cl-find-if (lambda (cc) (= (car cc) 1)) coords))
                (best (or on-l1
                          (car (cl-sort (copy-sequence coords)
                                        (lambda (a b)
                                          (< (daselt-abv--erg-score a)
                                             (daselt-abv--erg-score b))))))))
           (puthash letter best out)))
       per-letter)
      out)))

(defun daselt-abv--affix-letters (affix)
  "Return list of lowercase unique letters of AFFIX preserving order."
  (let ((seen (make-hash-table :test 'equal))
        (out '()))
    (dotimes (i (length affix))
      (let* ((ch (aref affix i))
             (s (downcase (string ch))))
        (when (and (daselt-abv--letter-p s) (not (gethash s seen)))
          (puthash s t seen)
          (push s out))))
    (nreverse out)))

(defun daselt-abv--letter-rarity (letter preferred-hash)
  "How many places in preferred-hash bind to LETTER (lowercase)."
  (let ((n 0))
    (maphash
     (lambda (_coord binding)
       (when (and (stringp binding) (string= (daselt-abv--down binding) letter))
         (setq n (1+ n))))
     preferred-hash)
    n))

(defun daselt-abv--free-candidates (preferred-hash counts-hash)
  "Return list of coords still unfille d (count 0) in preferred-hash."
  (cl-loop for coord in (daselt-abv--keys preferred-hash)
           unless (> (gethash coord counts-hash 0) 0)
           collect coord))

(defun daselt-abv--choose-fallback (preferred-hash counts-hash)
  "Choose coord by min assignment count, then ergonomic score."
  (let ((coords (daselt-abv--keys preferred-hash)))
    (car (cl-sort (copy-sequence coords)
                  (lambda (a b)
                    (let* ((ca (gethash a counts-hash 0))
                           (cb (gethash b counts-hash 0)))
                      (if (/= ca cb)
                          (< ca cb)
                        (< (daselt-abv--erg-score a)
                           (daselt-abv--erg-score b)))))))))

(defun daselt-abv--assign-one (affix preferred-hash counts-hash letter-base complementp)
  "Return chosen binding and update counts-hash."
  (let* ((letters (daselt-abv--affix-letters affix))
         ;; sort letters by rarity (fewer direct matches in preferred = earlier)
         (letters-sorted
          (cl-stable-sort (copy-sequence letters) #'<
                          :key (lambda (L) (daselt-abv--letter-rarity L preferred-hash))))
         (layers (daselt-abv--coord-layers-in-order complementp))
         (chosen-coord nil))
    ;; letter-guided search
    (cl-loop for L in letters-sorted
             for base = (gethash L letter-base)
             when base do
             (pcase-let ((`(,_ ,r ,c) base))
               (let ((cands (daselt-abv--neighbors r c layers)))
                 (cl-loop for coord in cands
                          for binding = (gethash coord preferred-hash)
                          when (and binding (<= (gethash coord counts-hash 0) 0))
                          do (setq chosen-coord coord)
                          (cl-return))))
             when chosen-coord do (cl-return))
    ;; fallback if none free via letter-guided
    (unless chosen-coord
      (let ((free (daselt-abv--free-candidates preferred-hash counts-hash)))
        (setq chosen-coord
              (if free
                  (car (cl-sort free #'<
                                :key (lambda (cc) (daselt-abv--erg-score cc))))
                ;; multi-fill: everything taken once; pick min-count then ergo
                (daselt-abv--choose-fallback preferred-hash counts-hash)))))
    ;; book-keeping and return binding
    (cl-incf (gethash chosen-coord counts-hash 0))
    (gethash chosen-coord preferred-hash)))

(defun daselt-abv--complement-hash (universe-hash subset-hash)
  "Return a hash of coords present in UNIVERSE-HASH but not in SUBSET-HASH."
  (let ((ht (make-hash-table :test 'equal)))
    (maphash
     (lambda (coord binding)
       (unless (gethash coord subset-hash)
         (puthash coord binding ht)))
     universe-hash)
    ht))

(defun daselt-abv--sorted-affixes-by-frequency (affixes freq-alist)
  "Return AFFIXES sorted by descending frequency; default 0."
  (let* ((freq (daselt-abv--alist->hash freq-alist))
         (pair (mapcar (lambda (a) (cons a (or (gethash a freq) 0))) affixes)))
    (mapcar #'car
            (cl-sort pair (lambda (a b)
                            (let ((fa (cdr a)) (fb (cdr b)))
                              (if (/= fa fb) (> fa fb)
                                ;; tie-break: shorter first, then lex
                                (let ((la (length (car a)))
                                      (lb (length (car b))))
                                  (if (/= la lb) (< la lb)
                                    (string< (car a) (car b)))))))))))

(defun daselt-abv-assign-affixes ()
  "Compute and set daselt-abv-prefix-table and daselt-abv-suffix-table."
  (interactive)
  ;; Validate required globals
  (dolist (v '(abv-prefixes abv-suffixes
                            ahp-prefix-frequency ahp-suffix-frequency
                            daselt-abv-valid-prefix-placevals daselt-abv-valid-suffix-placevals
                            daselt-abv-valid-lower-paired-prefix-placevals
                            daselt-abv-valid-lower-paired-suffix-placevals))
    (unless (boundp v)
      (error "Variable %s is not bound" v)))
  ;; Build placeval hashes
  (let* (;; Universe and paired sets for prefixes
         (prefix-univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
         (prefix-paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-prefix-placevals))
         (prefix-complement-h (daselt-abv--complement-hash prefix-univ-h prefix-paired-h))
         ;; Universe and paired sets for suffixes
         (suffix-univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-suffix-placevals))
         (suffix-paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-suffix-placevals))
         (suffix-complement-h (daselt-abv--complement-hash suffix-univ-h suffix-paired-h))
         ;; Lengths
         (n-prefix-top (length (daselt-abv--keys prefix-paired-h)))
         (n-suffix-top (length (daselt-abv--keys suffix-paired-h)))
         ;; Letter base map from union of both universes
         (letter-base (daselt-abv--collect-letter-bases
                       (append daselt-abv-valid-prefix-placevals
                               daselt-abv-valid-suffix-placevals)))
         ;; Counts per coord (separate for pref/suf)
         (prefix-counts (make-hash-table :test 'equal))
         (suffix-counts (make-hash-table :test 'equal))
         ;; Sort affixes by frequency
         (prefixes-sorted (daselt-abv--sorted-affixes-by-frequency abv-prefixes ahp-prefix-frequency))
         (suffixes-sorted (daselt-abv--sorted-affixes-by-frequency abv-suffixes ahp-suffix-frequency))
         ;; Partition into top N and rest
         (prefix-top (cl-subseq prefixes-sorted 0 (min n-prefix-top (length prefixes-sorted))))
         (prefix-rest (nthcdr (length prefix-top) prefixes-sorted))
         (suffix-top (cl-subseq suffixes-sorted 0 (min n-suffix-top (length suffixes-sorted))))
         (suffix-rest (nthcdr (length suffix-top) suffixes-sorted))
         ;; Output tables
         (pref-ht (make-hash-table :test 'equal))
         (suf-ht (make-hash-table :test 'equal)))
    ;; Assign prefixes
    (dolist (aff prefix-top)
      (puthash aff
               (daselt-abv--assign-one aff prefix-paired-h prefix-counts letter-base nil)
               pref-ht))
    (dolist (aff prefix-rest)
      (puthash aff
               (daselt-abv--assign-one aff prefix-complement-h prefix-counts letter-base t)
               pref-ht))
    ;; Assign suffixes
    (dolist (aff suffix-top)
      (puthash aff
               (daselt-abv--assign-one aff suffix-paired-h suffix-counts letter-base nil)
               suf-ht))
    (dolist (aff suffix-rest)
      (puthash aff
               (daselt-abv--assign-one aff suffix-complement-h suffix-counts letter-base t)
               suf-ht))
    ;; Set globals
    (setq daselt-abv-prefix-table pref-ht
          daselt-abv-suffix-table suf-ht)
    (cons pref-ht suf-ht)))

;;;; Hooks
(add-hook 'abv-mode-hook
          #'daselt-abv-add-to-temporary-word-constituents
          89)

(remove-hook 'daselt-mode-hook
             (lambda ()
                       (unless daselt-mode
                 (remove-hook 'abv-mode-hook
                              #'daselt-abv-add-to-temporary-word-constituents))))

(provide 'daselt-abv)
;;; daselt-abv.del ends here

#+end_tool
#+begin_tool (modify_buffer :buffer "daselt-abv.del" :content "; ...)
(:name "modify_buffer" :args (:buffer "daselt-abv.del" :content ";;; daselt-abv.del --- Daselt's abv module  -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: tools, abbrev, convenience

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Affix placement and visualization utilities for Daselt.
;;
;; This module provides functions to:
;; - derive candidate coordinates from the active Daselt layout by syntax class
;; - assign affixes (prefixes/suffixes/infixes/roots) to coordinates
;;   guided by frequency and letter proximity
;; - convert affix tables into bindlists for the drawing/preview pipeline
;; - visualize single layers of a generated table
;; - query affix tables and extend the defining options if an affix is missing
;; - persist and restore tables.

;;; Code:

(require 'daselt-dirs)
(require 'cl-lib)
(require 'abv-data)
(require 'abv-corpus)

;;;; Options
(defgroup daselt-abv
  nil
  \"Daselt helpers for assigning and visualizing affix placements.\"
  :group 'daselt-mode
  :prefix \"daselt-abv-\")

(defcustom daselt-abv-non-constituents
  '(?- ?_ ?: ?. ?, ?! ?? ?' ?`)
  \"Characters in the Daselt-layout to exclude from word-constituents.

Used by `daselt-abv-add-to-temporary-word-constituents'.  It is not necessary
to add non-characters, such as command keys; brackets are also unnecessary.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-single-symbol-placevals
  (let ((print-level nil) (print-length nil))
    (cl-remove-if
     (lambda (cons)
       (or (< 1 (length (cdr cons)))
           (let* ((char (string-to-char (cdr cons)))
                  (syx (with-syntax-table text-mode-syntax-table (char-syntax char))))
             (cl-member syx '(?\\  ?\\- ?\\\") :test #'=))))
     (daselt-base-flatten-n-times (cdr (daselt-coords-coordinatize-layout
                                        (symbol-value daselt-dfk-layout))) 2)))
  \"Placevals used for single-symbol abbrevs.
Each element is a cons of the form (COORD . STRING-BINDING).\"
  :type '(repeat (cons (repeat integer) string))
  :group 'daselt-mode)

(defcustom daselt-abv-prefix-classes
  '(?w ?_ ?. ?\\) ?\\> ?\\!)
  \"Syntax classes used to collect candidate prefix positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-suffix-classes
  '(?w ?_ ?\\( ?\\< ?\\!)
  \"Syntax classes used to collect candidate suffix positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-infix-classes
  '(?w ?_ ?. ?\\( ?\\< ?\\) ?\\> ?\\!)
  \"Syntax classes used to collect candidate infix positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-standalone-classes
  '(?w ?_ ?. ?\\( ?\\< ?\\) ?\\> ?\\!)
  \"Syntax classes used to collect candidate standalone positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-root-classes
  '(?w ?_)
  \"Syntax classes used to collect candidate root positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-layer-pairs
  '((1 . 2) (4 . 3) (5 . 6) (8 . 7))
  \"Pairs of layers considered lower/upper for letter pairing heuristics.
This is used to prefer lower layers with corresponding upper-case partner.\"
  :type '(repeat (cons integer integer))
  :group 'daselt-abv)

(defcustom daselt-abv-special-symbols
  '(?. ?,)
  \"Special symbols that should not be used for roots and affixes.

By default, we are using \".\" for grammar delimitation and \",\" for yasnippet.\"
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-prefix-symbols
  nil
  \"Special symbols that should not be used for prefixes.\"
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-suffix-symbols
  '(?↔)
  \"Special symbols that should not be used for suffixes.

By default we are using \"↔\" to disambiguate names.\"
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-added-syntax-classes
  '(?_)
  \"Syntax classes added to `abv-temporary-word-constituents' by Daselt.

By default only consists of the syntax class ?_, which designates symbols.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-save-file
  (let ((dir (ignore-errors (and (boundp 'daselt-mode-pkg-configs-directory)
                                 (file-name-as-directory daselt-mode-pkg-configs-directory)))))
    (expand-file-name (concat (or dir (file-name-as-directory user-emacs-directory))
                              \"abv/save.el\")))
  \"File into which to save generated affix tables.

The file will contain `defconst' forms for all variables whose names match
\"^daselt-abv-.*-table$\".  Existing content is overwritten.\"
  :type 'file
  :group 'daselt-abv)


;; Statistics configuration
(defcustom daselt-abv-statistics-file-types
  '(\"txt\" \"org\" \"md\" \"el\" \"del\")
  \"Filename extensions to include when computing personal statistics.
Each element is a string like \\=\"txt\\=\" or \\=\"org\\=\". Matching is case-insensitive.\"
  :type '(repeat string)
  :group 'daselt-mode)

(defcustom daselt-abv-statistics-file
  (expand-file-name \"daselt-abv-stats.el\" user-emacs-directory)
  \"File where personal word-frequency statistics are written and read from.\"
  :type 'file
  :group 'daselt-mode)



;;;; Variables
(defvar daselt-abv-valid-prefix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-prefix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
  \"Placevals considered valid for prefix placement.
Each entry is of the form (COORD . STRING-BINDING).\")

(defvar daselt-abv-valid-lower-paired-prefix-placevals
  (cl-intersection daselt-abv-valid-prefix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
                   :test #'equal)
  \"Subset of valid prefix placevals that are lower/upper paired.\")

(defvar daselt-abv-valid-suffix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-suffix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
  \"Placevals considered valid for suffix placement.\")

(defvar daselt-abv-valid-lower-paired-suffix-placevals
  (cl-intersection daselt-abv-valid-suffix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
                   :test #'equal)
  \"Subset of valid suffix placevals that are lower/upper paired.\")




(defvar daselt-abv-temporary-word-constituents-backup
  abv-temporary-word-constituents
  \"Backup of `abv-temporary-word-constituents' prior to our additions.\")

(defvar daselt-abv-prefix-table nil
  \"Hash-table mapping prefix strings to coordinate tuples.\")
(defvar daselt-abv-suffix-table nil
  \"Hash-table mapping suffix strings to coordinate tuples.\")

;;;; Functions
(defun daselt-abv-placevals-from-classes (classes)
  \"Return placevals in the active layout whose syntax class is in CLASSES.

A placeval is a cons of (COORD . STRING-BINDING).  COORD is a coordinate tuple
like (LAYER ROW COL).  CLASSES is a list of syntax class characters as returned
by `char-syntax'.\"
  (let ((print-level nil) (print-length nil))
    (-filter
     (lambda (cons)
       (and (= 1 (length (cdr cons)))
            (let* ((char (string-to-char (cdr cons)))
                   (syx (with-syntax-table org-mode-syntax-table (char-syntax char))))
              (cl-member syx classes :test #'=))))
     (daselt-base-flatten-n-times
      (cdr (daselt-coords-coordinatize-layout
            (symbol-value daselt-dfk-layout))) 2))))

(defun daselt-abv--filter-to-lower-layers (placevals)
  \"Filter PLACEVALS to those residing on lower layers from `daselt-abv-layer-pairs'.\"
  (-filter (lambda (cns)
             (cl-member (caar cns) (mapcar #'car daselt-abv-layer-pairs)))
           placevals))

(defun daselt-abv--filter-to-upper-lower-paired-placevals (placevals)
  \"Keep PLACEVALS whose upper-layer counterpart is a single symbol as well.

This prefers keys that have a clean upper-case pair on their partner layer.\"
  (-filter (lambda (cns)
             (let* ((coords (car cns))
                    (uppercoords (list (1+ (alist-get (car coords)
                                                      daselt-abv-layer-pairs))
                                       (nth 1 coords)
                                       (nth 2 coords))))
               (not (< 1 (length (daselt-coords-binding uppercoords))))))
           (daselt-abv--filter-to-lower-layers placevals)))

(defun daselt-abv-add-to-temporary-word-constituents ()
  \"Add characters from `daselt-xkb-layout' to `abv-temporary-word-constituents'.

Adds characters whose syntax class is in `daselt-abv-added-syntax-classes'.
Excludes symbols from `daselt-abv-non-constituents'.\"
  (declare (ftype (function () t)))
  (setopt abv-temporary-word-constituents
          (cl-union daselt-abv-temporary-word-constituents-backup
                    (let ((laylist (flatten-list (symbol-value daselt-xkb-layout))))
                      (remq
                       nil (mapcar
                            (lambda (entry)
                              (if (= 1 (length entry))
                                  (let ((char (string-to-char entry)))
                                    (if (and (or (memq (char-syntax char)
                                                       daselt-abv-added-syntax-classes))
                                             (not (cl-member
                                                   char
                                                   daselt-abv-non-constituents
                                                   :test #'=)))
                                        char))))
                            laylist)))))
  (abv--generate-syntax-table))

;;;;; Personal statistics

(defvar daselt-abv-statistics-obarray (make-vector 4096 0)
  \"Obarray holding personal word-frequency statistics.

Symbols are interned for words; their symbol-values hold the counts.\")

(defun daselt-abv--count-words-into (file table)
  \"Count words in FILE and accumulate into hash TABLE.

Words are sequences that match the regexp \"\\\\(\\\\sw\\\\|\\\\s_\\\\)+\". Counting is
case-insensitive. Files that cannot be decoded or read are skipped.\"
  (when (and (stringp file) (file-readable-p file))
    (condition-case _err
        (with-temp-buffer
          ;; Use automatic decoding; avoid visiting to keep things light-weight.
          (insert-file-contents file nil nil nil t)
          (goto-char (point-min))
          (let ((case-fold-search t))
            (while (re-search-forward \"\\\\(\\\\sw\\\\|\\\\s_\\\\)+\" nil t)
              (let* ((w (downcase (match-string-no-properties 0)))
                     (old (gethash w table 0)))
                (puthash w (1+ old) table)))))
      (error nil))))

(defun daselt-abv-read-statistics ()
  \"Read `daselt-abv-statistics-file' into an obarray.

Returns the newly created obarray stored in
`daselt-abv-statistics-obarray'.\"
  (interactive)
  (setq daselt-abv-statistics-obarray (make-vector 4096 0))
  (when (file-readable-p daselt-abv-statistics-file)
    (with-temp-buffer
      (insert-file-contents daselt-abv-statistics-file)
      (goto-char (point-min))
      (condition-case _err
          (let ((data (read (current-buffer))))
            (dolist (pair data)
              (when (and (consp pair)
                         (stringp (car pair))
                         (numberp (cdr pair)))
                (let ((sym (intern (car pair) daselt-abv-statistics-obarray)))
                  (set sym (cdr pair))))))
        (error nil))))
  daselt-abv-statistics-obarray)

;;;;; Assignment helpers
(defun daselt-abv--alist->hash (alist &optional key-fn)
  \"Return a hash table from ALIST; transform keys via KEY-FN if non-nil.\"
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (kv alist ht)
      (let ((k (if key-fn (funcall key-fn (car kv)) (car kv)))
            (v (cdr kv)))
        (puthash k v ht)))))

(defun daselt-abv--placevals->coord-hash (placevals)
  \"Return hash mapping coord -> binding for PLACEVALS.
A placeval has the shape ((LAYER ROW COL) . STRING-BINDING).\"
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (pv placevals ht)
      (puthash (car pv) (cdr pv) ht))))

(defun daselt-abv--keys (hash)
  \"Return a list of keys of HASH.\"
  (let (ks)
    (maphash (lambda (k _v) (push k ks)) hash)
            hash
    ks))

(defun daselt-abv--letter-p (s)
  \"Return non-nil if S is a single ASCII alphabetic letter (A-Z or a-z).\"
  (and (stringp s) (= (length s) 1)
       (let ((c (aref s 0)))
         (or (and (<= ?a c) (<= c ?z))
             (and (<= ?A c) (<= c ?Z))))))

(defun daselt-abv--down (s)
  \"Return lowercase of single-letter string S; identity otherwise.\"
  (if (and (stringp s) (= (length s) 1))
      (downcase s)
    s))

(defun daselt-abv--erg-score (coord)
  \"Return ergonomic score for COORD; lower is better.
Heuristic per main_layout_heuristics.org.\"
  (pcase-let ((`(,L ,row ,col) coord))
    (let* ((abscol (abs col))
           (row-pen (cond ((= row 0) 0.0)
                          (t 0.6))) ;; top/bottom small penalty
           (col-pen (pcase abscol
                      (1 1.0) (2 1.2) (3 1.5) (4 2.0) (5 3.0) (6 4.0)
                      (_ 5.0)))
           (layer-pen (pcase L
                        (1 0.0)
                        (2 0.2) (4 0.2)
                        (6 0.4) (5 0.4)
                        (7 0.6) (3 0.6)
                        (8 0.8)
                        (_ 1.0))))
      (+ row-pen col-pen layer-pen))))

(defun daselt-abv--coord-layers-in-order (complementp)
  \"Return order of layers by ease; if COMPLEMENTP, prefer 2 before 1.\"
  (let ((base '(1 2 4 6 5 7 3 8)))
    (if complementp
        '(2 1 4 6 5 7 3 8)
      base)))

(defun daselt-abv--neighbors (r c layers)
  \"Produce coords around (R,C) across LAYERS in the guide's order.\"
  (let ((coords '()))
    ;; same row/col across layers
    (dolist (L layers) (push (list L r c) coords))
    ;; adjacent rows then across layers
    (dolist (dr '(-1 1))
      (dolist (L layers) (push (list L (+ r dr) c) coords)))
    ;; column-adjacent then across layers
    (dolist (dc '(-1 1))
      (dolist (L layers) (push (list L r (+ c dc)) coords)))
    (nreverse coords)))

(defun daselt-abv--collect-letter-bases (placevals)
  \"Map lowercase letter -> one base coord, prefer layer 1 if present.
PLACEVALS is a list of (COORD . BINDING).\"
  (let ((per-letter (make-hash-table :test 'equal)))
    ;; collect all coords by letter
    (dolist (pv placevals)
      (let* ((coord (car pv)) (binding (daselt-abv--down (cdr pv))))
        (when (daselt-abv--letter-p binding)
          (push coord (gethash binding per-letter)))))
    ;; pick best coord per letter, prefer layer 1, else minimal layer/erg score
    (let ((out (make-hash-table :test 'equal)))
      (maphash
       (lambda (letter coords)
         (let* ((on-l1 (cl-find-if (lambda (cc) (= (car cc) 1)) coords))
                (best (or on-l1
                          (car (cl-sort (copy-sequence coords)
                                        (lambda (a b)
                                          (< (daselt-abv--erg-score a)
                                             (daselt-abv--erg-score b))))))))
           (puthash letter best out)))
       per-letter)
      out)))

(defun daselt-abv--affix-letters (affix)
  \"Return list of lowercase unique letters of AFFIX preserving order.\"
  (let ((seen (make-hash-table :test 'equal))
        (out '()))
    (dotimes (i (length affix))
      (let* ((ch (aref affix i))
             (s (downcase (string ch))))
        (when (and (daselt-abv--letter-p s) (not (gethash s seen)))
          (puthash s t seen)
          (push s out))))
    (nreverse out)))

(defun daselt-abv--letter-rarity (letter preferred-hash)
  \"Return how many places in PREFERRED-HASH bind to LETTER (lowercase).\"
  (let ((n 0))
    (maphash
     (lambda (_coord binding)
       (when (and (stringp binding) (string= (daselt-abv--down binding) letter))
         (setq n (1+ n))))
     preferred-hash)
    n))

(defun daselt-abv--free-candidates (preferred-hash counts-hash)
  \"Return coords still unfilled (assignment count 0) in PREFERRED-HASH.\"
  (cl-loop for coord in (daselt-abv--keys preferred-hash)
           unless (> (gethash coord counts-hash 0) 0)
           collect coord))

(defun daselt-abv--choose-fallback (preferred-hash counts-hash)
  \"Choose coord by minimal assignment count, then ergonomic score.\"
  (let ((coords (daselt-abv--keys preferred-hash)))
    (car (cl-sort (copy-sequence coords)
                  (lambda (a b)
                    (let* ((ca (gethash a counts-hash 0))
                           (cb (gethash b counts-hash 0)))
                      (if (/= ca cb)
                          (< ca cb)
                        (< (daselt-abv--erg-score a)
                           (daselt-abv--erg-score b)))))))))

(defun daselt-abv--assign-one (affix preferred-hash counts-hash letter-base complementp)
  \"Choose a coordinate for AFFIX and update COUNTS-HASH.

PREFERRED-HASH maps coordinates to their single-character binding.  COUNTS-HASH
maps coordinates to times used; it is incremented for the chosen coordinate to
balance load.  LETTER-BASE maps letters to preferred base coordinates.  If
COMPLEMENTP is non-nil, prefer a complementary layer order.  Return the chosen
coordinate tuple.\"
  (let* ((letters (daselt-abv--affix-letters affix))
         ;; sort letters by rarity (fewer direct matches in preferred = earlier)
         (letters-sorted
          (cl-stable-sort (copy-sequence letters) #'<
                          :key (lambda (L) (daselt-abv--letter-rarity L preferred-hash))))
         (layers (daselt-abv--coord-layers-in-order complementp))
         (chosen-coord nil))
    ;; letter-guided search
    (cl-loop for L in letters-sorted
             for base = (gethash L letter-base)
             when base do
             (pcase-let ((`(,_ ,r ,c) base))
               (let ((cands (daselt-abv--neighbors r c layers)))
                 (cl-loop for coord in cands
                          for binding = (gethash coord preferred-hash)
                          when (and binding (<= (gethash coord counts-hash 0) 0))
                          do (setq chosen-coord coord)
                          (cl-return))))
             when chosen-coord do (cl-return))
    ;; fallback if none free via letter-guided
    (unless chosen-coord
      (let ((free (daselt-abv--free-candidates preferred-hash counts-hash)))
        (setq chosen-coord
              (if free
                  (car (cl-sort free #'<
                                :key (lambda (cc) (daselt-abv--erg-score cc))))
                ;; multi-fill: everything taken once; pick min-count then ergo
                (daselt-abv--choose-fallback preferred-hash counts-hash)))))
    ;; book-keeping and return coord
    (cl-incf (gethash chosen-coord counts-hash 0))
    chosen-coord))

(defun daselt-abv--complement-hash (universe-hash subset-hash)
  \"Return a hash of coords present in UNIVERSE-HASH but not in SUBSET-HASH.\"
  (let ((ht (make-hash-table :test 'equal)))
    (maphash
     (lambda (coord binding)
       (unless (gethash coord subset-hash)
         (puthash coord binding ht)))
     universe-hash)
    ht))

(defun daselt-abv--sorted-affixes-by-frequency (affixes freq-alist)
  \"Return AFFIXES sorted by descending frequency using FREQ-ALIST; default 0.
Ties are broken by shorter length first, then lexicographically.\"
  (let* ((freq (daselt-abv--alist->hash freq-alist))
         (pair (mapcar (lambda (a) (cons a (or (gethash a freq) 0))) affixes)))
    (mapcar #'car
            (cl-sort pair (lambda (a b)
                            (let ((fa (cdr a)) (fb (cdr b)))
                              (if (/= fa fb) (> fa fb)
                                ;; tie-break: shorter first, then lex
                                (let ((la (length (car a)))
                                      (lb (length (car b))))
                                  (if (/= la lb) (< la lb)
                                    (string< (car a) (car b)))))))))))

(defun daselt-abv--affix-kind-for-table (sym)
  \"Heuristically determine affix kind for table symbol SYM.
Return one of the symbols 'prefixes, 'suffixes, 'infixes, 'roots or nil.\"
  (let* ((name (symbol-name sym))
         (base (and (string-match \"^daselt-abv-\\(.*?\\)-table$\" name)
                    (match-string 1 name))))
    (cond
     ((and base (string-match-p \"^prefix\" base)) 'prefixes)
     ((and base (string-match-p \"^suffix\" base)) 'suffixes)
     ((and base (string-match-p \"^infix\" base)) 'infixes)
     ((and base (string-match-p \"^root\" base)) 'roots)
     (t nil))))

(defun daselt-abv--option-var-for-table (sym)
  \"Return the abv option variable symbol corresponding to table SYM.
Supported mappings are: prefixes, suffixes, infixes, roots.\"
  (pcase (daselt-abv--affix-kind-for-table sym)
    ('prefixes 'abv-prefixes)
    ('suffixes 'abv-suffixes)
    ('infixes  'abv-infixes)
    ('roots    'abv-roots)
    (_ nil)))

(defun daselt-abv--ensure-hash-table (sym)
  \"Ensure SYM is bound to a hash-table and return it.
If SYM is unbound or not a hash-table, bind it to a fresh hash-table.\"
  (unless (and (boundp sym) (hash-table-p (symbol-value sym)))
    (set sym (make-hash-table :test #'equal)))
  (symbol-value sym))

(defun daselt-abv--normalize-frequencies (freqs)
  \"Normalize FREQS into an alist of (AFFIX . COUNT).

Accepts the following shapes:
- alist of (string . integer)
- alist of ((string) . integer) as from `abv-corpus-reduced-monogram-frequencies'
- alist of (symbol . integer)
Return a list of conses with string keys. Entries with non-positive counts are
removed.\"
  (cl-loop for (k . v) in freqs
           for key = (cond
                      ((stringp k) k)
                      ((and (consp k) (stringp (car k))) (car k))
                      ((symbolp k) (symbol-name k))
                      (t nil))
           when (and key (integerp v) (> v 0))
           collect (cons key v)))

;;;###autoload
(defun daselt-abv-assign-affixes (affixes frequencies table-sym)
  \"Assign AFFIXES to Daselt coordinates by FREQUENCIES into TABLE-SYM.

AFFIXES is a list of affix strings (e.g., `abv-prefixes').  FREQUENCIES is an
alist mapping affix to count, or a list of ((affix) . count) pairs as returned
by `abv-corpus-reduced-monogram-frequencies'.  TABLE-SYM is the symbol of the
hash-table to populate (e.g., 'daselt-abv-prefix-table).

If TABLE-SYM is already bound to a hash-table, extend it without overwriting
existing entries.  The value for each key is the coordinate tuple assigned to
that affix.  Coordinates are chosen from a preferred set first (paired lower
layers), then from its complement.  Affixes are processed in descending
frequency order.

This function assigns by coordinates, not by the symbols they house.\"
  (interactive
   (let* ((table (intern (completing-read \"Affix table symbol: \" obarray
                                         (lambda (s)
                                           (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s)))
                                         nil nil nil \"daselt-abv-prefix-table\")))
          (kind (daselt-abv--affix-kind-for-table table))
          (opt (or (daselt-abv--option-var-for-table table) 'abv-prefixes))
          (affs (symbol-value opt))
          (freqs (let ((corpus (when (boundp 'daselt-abv-statistics-obarray)
                                  daselt-abv-statistics-obarray)))
                   (condition-case _
                       ;; Fallback to equal weights if corpus is not available
                       (abv-corpus-reduced-monogram-frequencies affs (mapatoms #'symbol-name corpus) 'initial)
                     (error (mapcar (lambda (a) (cons a 1)) affs))))))
     (list affs freqs table)))
  (unless (and (listp affixes) (listp frequencies) (symbolp table-sym))
    (user-error \"Invalid inputs: expected (list list symbol)\"))
  (let* ((freqs (daselt-abv--normalize-frequencies frequencies))
         (table (daselt-abv--ensure-hash-table table-sym))
         ;; mark used affixes and coords from existing entries
         (used-coords (make-hash-table :test #'equal))
         (counts (make-hash-table :test #'equal)))
    (maphash (lambda (_a c)
               (puthash c t used-coords)
               (cl-incf (gethash c counts 0)))
             table)
    ;; Build placeval universe according to affix kind
    (pcase (daselt-abv--affix-kind-for-table table-sym)
      ('prefixes
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
              (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-prefix-placevals))
              (compl-h (daselt-abv--complement-hash univ-h paired-h))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             ;; try paired first
             (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
               (when (or (null coord) (gethash coord used-coords))
                 (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
               (when coord
                 (puthash coord t used-coords)
                 (puthash a coord table))))))
      ('suffixes
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-suffix-placevals))
              (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-suffix-placevals))
              (compl-h (daselt-abv--complement-hash univ-h paired-h))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
               (when (or (null coord) (gethash coord used-coords))
                 (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
               (when coord
                 (puthash coord t used-coords)
                 (puthash a coord table))))))
      (_
       ;; generic: use prefix-classes as a reasonable default universe
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             (let ((coord (daselt-abv--assign-one a univ-h counts letter-base nil)))
               (when (and coord (not (gethash coord used-coords)))
                 (puthash coord t used-coords)
                 (puthash a coord table))))))))
    table))

(defun daselt-abv--table->bindlist-var (table-sym)
  \"Return the symbol to use for the bindlist variable from TABLE-SYM.\"
  (intern (replace-regexp-in-string \"-table$\" \"-bindlist\" (symbol-name table-sym))))

(defun daselt-abv-table->bindlist (table-sym)
  \"Convert TABLE-SYM (an affix table) into a bindlist and define a variable.

The variable name is derived by replacing the trailing \"-table\" with
\"-bindlist\" in TABLE-SYM.  Each binding in the returned bindlist has the form
(COORDS . LABELSYM), where COORDS is the coordinate tuple from the table and
LABELSYM is an interned symbol mirroring the affix string (e.g., \\=\"pre\\=\" ->
\\='abv-pre).  The variable is created or replaced using `set'.  Return the
bindlist.\"
  (interactive
   (list (intern (completing-read \"Affix table: \" obarray
                                 (lambda (s)
                                   (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                                        (hash-table-p (symbol-value s))))
                                 t))))
  (unless (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
    (user-error \"Table %s is not bound to a hash-table\" table-sym))
  (let* ((tbl (symbol-value table-sym))
         (blist nil))
    (maphash (lambda (k v)
               (let* ((aff (if (stringp k) k (format \"%s\" k)))
                      (sym (intern (concat \"abv-\" aff)))
                      (coords v))
                 (push (cons coords sym) blist)))
             tbl)
    (setq blist (nreverse blist))
    (set (daselt-abv--table->bindlist-var table-sym) blist)
    blist))

;;; Visualization
;;;###autoload
(defun daselt-abv-draw-table-layer (table-sym laycoord &rest mods)
  \"Visualize one layer of TABLE-SYM by delegating to `daselt-bind-draw-bindlist-layer'.

Ensure a bindlist exists for TABLE-SYM by converting if necessary, then draw
layer LAYCOORD with optional MODS.\"
  (interactive
   (let* ((table (intern (completing-read \"Affix table symbol: \" obarray
                                         (lambda (s)
                                           (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                                                (hash-table-p (symbol-value s))))
                                         t)))
          (lay (read (read-from-minibuffer \"Layer coordinate (e.g., (0)): \"))))
     (list table lay)))
  (when (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
    ;; ensure bindlist
    (unless (and (boundp (daselt-abv--table->bindlist-var table-sym))
                 (daselt-bind-bindlist-p (symbol-value (daselt-abv--table->bindlist-var table-sym))))
      (daselt-abv-table->bindlist table-sym))
    (apply #'daselt-bind-draw-bindlist-layer (daselt-abv--table->bindlist-var table-sym) laycoord mods)))

(defun daselt-abv--table-symbols ()
  \"Return a list of symbols bound to affix tables named ^daselt-abv-.*-table$.\"
  (let (out)
    (mapatoms (lambda (s)
                (when (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                           (boundp s) (hash-table-p (symbol-value s)))
                  (push s out))))
    (nreverse out)))

;;; Query and save
;;;###autoload
(defun daselt-abv-look-up ()
  \"Look up an affix in a chosen table and maybe add it to the defining option.

This command prompts for a table among variables named
\\=\"daselt-abv-.*-table\\=\", then for an affix string.  If found, its
coordinates are displayed.  If no result is found, the user is asked whether
to add the query string to the variable that defined the table (e.g.,
`abv-prefixes', `abv-suffixes', `abv-infixes' or `abv-roots').  The new option
value is saved using `customize-push-and-save' when available.\"
  (interactive)
  (let* ((table (intern (completing-read \"Affix table: \" (daselt-abv--table-symbols) nil t)))
         (query (read-string (format \"Affix for %s: \" table)))
         (tbl (symbol-value table))
         (hit (and (hash-table-p tbl) (gethash query tbl))))
    (if hit
        (message \"%s => %S\" query hit)
      (let ((opt (daselt-abv--option-var-for-table table)))
        (if (not (and opt (boundp opt)))
            (message \"No matching option variable for %s\" table)
          (when (yes-or-no-p (format \"Add %S to %S and save? \" query opt))
            (if (fboundp 'customize-push-and-save)
                (customize-push-and-save opt query)
              (customize-set-variable opt (append (symbol-value opt) (list query)))
              (customize-save-customized))
            (message \"Added %S to %S\" query opt)))))))

;;;###autoload
(defun daselt-abv-save (&optional file)
  \"Save all tables named \\=\"daselt-abv-.*-table\\=\" into FILE.

FILE defaults to `daselt-abv-save-file'.  This overwrites existing contents.
Tables are serialized using `abv-corpus-print-table'.\"
  (interactive)
  (let ((outfile (or file daselt-abv-save-file)))
    (let ((dir (file-name-directory outfile)))
      (when dir (ignore-errors (make-directory dir t))))
    (with-temp-file outfile
      (insert \";;; Saved daselt-abv tables\\n\\n\")
      (dolist (s (daselt-abv--table-symbols))
        (insert (format \";;; %s\\n\" s))
        (abv-corpus-print-table s)
        (insert \"\\n\\n\")))
    (message \"Saved %d tables to %s\" (length (daselt-abv--table-symbols)) outfile)))


;;;; Legacy batch assignment (deprecated)
;; Keeping for reference; new `daselt-abv-assign-affixes' is table-oriented.
(defun daselt-abv-assign-prefixes-and-suffixes ()
  \"Compute and set `daselt-abv-prefix-table' and `daselt-abv-suffix-table'.
Deprecated: prefer calling `daselt-abv-assign-affixes' for each table.\"
  (interactive)
  (daselt-abv-assign-affixes abv-prefixes (mapcar (lambda (a) (cons a 1)) abv-prefixes) 'daselt-abv-prefix-table)
  (daselt-abv-assign-affixes abv-suffixes (mapcar (lambda (a) (cons a 1)) abv-suffixes) 'daselt-abv-suffix-table))

;;;; Hooks
(add-hook 'abv-mode-hook
          #'daselt-abv-add-to-temporary-word-constituents
          89)

(remove-hook 'daselt-mode-hook
             (lambda ()
               (unless daselt-mode
                 (remove-hook 'abv-mode-hook
                              #'daselt-abv-add-to-temporary-word-constituents))))

(provide 'daselt-abv)
;;; daselt-abv.del ends here
"))

nil
#+end_tool
#+begin_tool (read_buffer :buffer "daselt-abv.del")
(:name "read_buffer" :args (:buffer "daselt-abv.del"))

;;; daselt-abv.del --- Daselt's abv module  -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: tools, abbrev, convenience

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Affix placement and visualization utilities for Daselt.
;;
;; This module provides functions to:
;; - derive candidate coordinates from the active Daselt layout by syntax class
;; - assign affixes (prefixes/suffixes/infixes/roots) to coordinates
;;   guided by frequency and letter proximity
;; - convert affix tables into bindlists for the drawing/preview pipeline
;; - visualize single layers of a generated table
;; - query affix tables and extend the defining options if an affix is missing
;; - persist and restore tables.

;;; Code:

(require 'daselt-dirs)
(require 'cl-lib)
(require 'abv-data)
(require 'abv-corpus)

;;;; Options
(defgroup daselt-abv
  nil
  "Daselt helpers for assigning and visualizing affix placements."
  :group 'daselt-mode
  :prefix "daselt-abv-")

(defcustom daselt-abv-non-constituents
  '(?- ?_ ?: ?. ?, ?! ?? ?' ?`)
  "Characters in the Daselt-layout to exclude from word-constituents.

Used by `daselt-abv-add-to-temporary-word-constituents'.  It is not necessary
to add non-characters, such as command keys; brackets are also unnecessary."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-single-symbol-placevals
  (let ((print-level nil) (print-length nil))
    (cl-remove-if
     (lambda (cons)
       (or (< 1 (length (cdr cons)))
           (let* ((char (string-to-char (cdr cons)))
                  (syx (with-syntax-table text-mode-syntax-table (char-syntax char))))
             (cl-member syx '(?\  ?\- ?\") :test #'=))))
     (daselt-base-flatten-n-times (cdr (daselt-coords-coordinatize-layout
                                        (symbol-value daselt-dfk-layout))) 2)))
  "Placevals used for single-symbol abbrevs.
Each element is a cons of the form (COORD . STRING-BINDING)."
  :type '(repeat (cons (repeat integer) string))
  :group 'daselt-mode)

(defcustom daselt-abv-prefix-classes
  '(?w ?_ ?. ?\) ?\> ?\!)
  "Syntax classes used to collect candidate prefix positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-suffix-classes
  '(?w ?_ ?\( ?\< ?\!)
  "Syntax classes used to collect candidate suffix positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-infix-classes
  '(?w ?_ ?. ?\( ?\< ?\) ?\> ?\!)
  "Syntax classes used to collect candidate infix positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-standalone-classes
  '(?w ?_ ?. ?\( ?\< ?\) ?\> ?\!)
  "Syntax classes used to collect candidate standalone positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-root-classes
  '(?w ?_)
  "Syntax classes used to collect candidate root positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-layer-pairs
  '((1 . 2) (4 . 3) (5 . 6) (8 . 7))
  "Pairs of layers considered lower/upper for letter pairing heuristics.
This is used to prefer lower layers with corresponding upper-case partner."
  :type '(repeat (cons integer integer))
  :group 'daselt-abv)

(defcustom daselt-abv-special-symbols
  '(?. ?,)
  "Special symbols that should not be used for roots and affixes.

By default, we are using "." for grammar delimitation and "," for yasnippet."
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-prefix-symbols
  nil
  "Special symbols that should not be used for prefixes."
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-suffix-symbols
  '(?↔)
  "Special symbols that should not be used for suffixes.

By default we are using "↔" to disambiguate names."
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-added-syntax-classes
  '(?_)
  "Syntax classes added to `abv-temporary-word-constituents' by Daselt.

By default only consists of the syntax class ?_, which designates symbols."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-save-file
  (let ((dir (ignore-errors (and (boundp 'daselt-mode-pkg-configs-directory)
                                 (file-name-as-directory daselt-mode-pkg-configs-directory)))))
    (expand-file-name (concat (or dir (file-name-as-directory user-emacs-directory))
                              "abv/save.el")))
  "File into which to save generated affix tables.

The file will contain `defconst' forms for all variables whose names match
"^daselt-abv-.*-table$".  Existing content is overwritten."
  :type 'file
  :group 'daselt-abv)


;; Statistics configuration
(defcustom daselt-abv-statistics-file-types
  '("txt" "org" "md" "el" "del")
  "Filename extensions to include when computing personal statistics.
Each element is a string like \="txt\=" or \="org\=". Matching is case-insensitive."
  :type '(repeat string)
  :group 'daselt-mode)

(defcustom daselt-abv-statistics-file
  (expand-file-name "daselt-abv-stats.el" user-emacs-directory)
  "File where personal word-frequency statistics are written and read from."
  :type 'file
  :group 'daselt-mode)



;;;; Variables
(defvar daselt-abv-valid-prefix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-prefix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
  "Placevals considered valid for prefix placement.
Each entry is of the form (COORD . STRING-BINDING).")

(defvar daselt-abv-valid-lower-paired-prefix-placevals
  (cl-intersection daselt-abv-valid-prefix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
                   :test #'equal)
  "Subset of valid prefix placevals that are lower/upper paired.")

(defvar daselt-abv-valid-suffix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-suffix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
  "Placevals considered valid for suffix placement.")

(defvar daselt-abv-valid-lower-paired-suffix-placevals
  (cl-intersection daselt-abv-valid-suffix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
                   :test #'equal)
  "Subset of valid suffix placevals that are lower/upper paired.")




(defvar daselt-abv-temporary-word-constituents-backup
  abv-temporary-word-constituents
  "Backup of `abv-temporary-word-constituents' prior to our additions.")

(defvar daselt-abv-prefix-table nil
  "Hash-table mapping prefix strings to coordinate tuples.")
(defvar daselt-abv-suffix-table nil
  "Hash-table mapping suffix strings to coordinate tuples.")

;;;; Functions
(defun daselt-abv-placevals-from-classes (classes)
  "Return placevals in the active layout whose syntax class is in CLASSES.

A placeval is a cons of (COORD . STRING-BINDING).  COORD is a coordinate tuple
like (LAYER ROW COL).  CLASSES is a list of syntax class characters as returned
by `char-syntax'."
  (let ((print-level nil) (print-length nil))
    (-filter
     (lambda (cons)
       (and (= 1 (length (cdr cons)))
            (let* ((char (string-to-char (cdr cons)))
                   (syx (with-syntax-table org-mode-syntax-table (char-syntax char))))
              (cl-member syx classes :test #'=))))
     (daselt-base-flatten-n-times
      (cdr (daselt-coords-coordinatize-layout
            (symbol-value daselt-dfk-layout))) 2))))

(defun daselt-abv--filter-to-lower-layers (placevals)
  "Filter PLACEVALS to those residing on lower layers from `daselt-abv-layer-pairs'."
  (-filter (lambda (cns)
             (cl-member (caar cns) (mapcar #'car daselt-abv-layer-pairs)))
           placevals))

(defun daselt-abv--filter-to-upper-lower-paired-placevals (placevals)
  "Keep PLACEVALS whose upper-layer counterpart is a single symbol as well.

This prefers keys that have a clean upper-case pair on their partner layer."
  (-filter (lambda (cns)
             (let* ((coords (car cns))
                    (uppercoords (list (1+ (alist-get (car coords)
                                                      daselt-abv-layer-pairs))
                                       (nth 1 coords)
                                       (nth 2 coords))))
               (not (< 1 (length (daselt-coords-binding uppercoords))))))
           (daselt-abv--filter-to-lower-layers placevals)))

(defun daselt-abv-add-to-temporary-word-constituents ()
  "Add characters from `daselt-xkb-layout' to `abv-temporary-word-constituents'.

Adds characters whose syntax class is in `daselt-abv-added-syntax-classes'.
Excludes symbols from `daselt-abv-non-constituents'."
  (declare (ftype (function () t)))
  (setopt abv-temporary-word-constituents
          (cl-union daselt-abv-temporary-word-constituents-backup
                    (let ((laylist (flatten-list (symbol-value daselt-xkb-layout))))
                      (remq
                       nil (mapcar
                            (lambda (entry)
                              (if (= 1 (length entry))
                                  (let ((char (string-to-char entry)))
                                    (if (and (or (memq (char-syntax char)
                                                       daselt-abv-added-syntax-classes))
                                             (not (cl-member
                                                   char
                                                   daselt-abv-non-constituents
                                                   :test #'=)))
                                        char))))
                            laylist)))))
  (abv--generate-syntax-table))

;;;;; Personal statistics

(defvar daselt-abv-statistics-obarray (make-vector 4096 0)
  "Obarray holding personal word-frequency statistics.

Symbols are interned for words; their symbol-values hold the counts.")

(defun daselt-abv--count-words-into (file table)
  "Count words in FILE and accumulate into hash TABLE.

Words are sequences that match the regexp "\\(\\sw\\|\\s_\\)+". Counting is
case-insensitive. Files that cannot be decoded or read are skipped."
  (when (and (stringp file) (file-readable-p file))
    (condition-case _err
        (with-temp-buffer
          ;; Use automatic decoding; avoid visiting to keep things light-weight.
          (insert-file-contents file nil nil nil t)
          (goto-char (point-min))
          (let ((case-fold-search t))
            (while (re-search-forward "\\(\\sw\\|\\s_\\)+" nil t)
              (let* ((w (downcase (match-string-no-properties 0)))
                     (old (gethash w table 0)))
                (puthash w (1+ old) table)))))
      (error nil))))

(defun daselt-abv-read-statistics ()
  "Read `daselt-abv-statistics-file' into an obarray.

Returns the newly created obarray stored in
`daselt-abv-statistics-obarray'."
  (interactive)
  (setq daselt-abv-statistics-obarray (make-vector 4096 0))
  (when (file-readable-p daselt-abv-statistics-file)
    (with-temp-buffer
      (insert-file-contents daselt-abv-statistics-file)
      (goto-char (point-min))
      (condition-case _err
          (let ((data (read (current-buffer))))
            (dolist (pair data)
              (when (and (consp pair)
                         (stringp (car pair))
                         (numberp (cdr pair)))
                (let ((sym (intern (car pair) daselt-abv-statistics-obarray)))
                  (set sym (cdr pair))))))
        (error nil))))
  daselt-abv-statistics-obarray)

;;;;; Assignment helpers
(defun daselt-abv--alist->hash (alist &optional key-fn)
  "Return a hash table from ALIST; transform keys via KEY-FN if non-nil."
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (kv alist ht)
      (let ((k (if key-fn (funcall key-fn (car kv)) (car kv)))
            (v (cdr kv)))
        (puthash k v ht)))))

(defun daselt-abv--placevals->coord-hash (placevals)
  "Return hash mapping coord -> binding for PLACEVALS.
A placeval has the shape ((LAYER ROW COL) . STRING-BINDING)."
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (pv placevals ht)
      (puthash (car pv) (cdr pv) ht))))

(defun daselt-abv--keys (hash)
  "Return a list of keys of HASH."
  (let (ks)
    (maphash (lambda (k _v) (push k ks)) hash)
    hash
    ks))

(defun daselt-abv--letter-p (s)
  "Return non-nil if S is a single ASCII alphabetic letter (A-Z or a-z)."
  (and (stringp s) (= (length s) 1)
       (let ((c (aref s 0)))
         (or (and (<= ?a c) (<= c ?z))
             (and (<= ?A c) (<= c ?Z))))))

(defun daselt-abv--down (s)
  "Return lowercase of single-letter string S; identity otherwise."
  (if (and (stringp s) (= (length s) 1))
      (downcase s)
    s))

(defun daselt-abv--erg-score (coord)
  "Return ergonomic score for COORD; lower is better.
Heuristic per main_layout_heuristics.org."
  (pcase-let ((`(,L ,row ,col) coord))
    (let* ((abscol (abs col))
           (row-pen (cond ((= row 0) 0.0)
                          (t 0.6))) ;; top/bottom small penalty
           (col-pen (pcase abscol
                      (1 1.0) (2 1.2) (3 1.5) (4 2.0) (5 3.0) (6 4.0)
                      (_ 5.0)))
           (layer-pen (pcase L
                        (1 0.0)
                        (2 0.2) (4 0.2)
                        (6 0.4) (5 0.4)
                        (7 0.6) (3 0.6)
                        (8 0.8)
                        (_ 1.0))))
      (+ row-pen col-pen layer-pen))))

(defun daselt-abv--coord-layers-in-order (complementp)
  "Return order of layers by ease; if COMPLEMENTP, prefer 2 before 1."
  (let ((base '(1 2 4 6 5 7 3 8)))
    (if complementp
        '(2 1 4 6 5 7 3 8)
      base)))

(defun daselt-abv--neighbors (r c layers)
  "Produce coords around (R,C) across LAYERS in the guide's order."
  (let ((coords '()))
    ;; same row/col across layers
    (dolist (L layers) (push (list L r c) coords))
    ;; adjacent rows then across layers
    (dolist (dr '(-1 1))
      (dolist (L layers) (push (list L (+ r dr) c) coords)))
    ;; column-adjacent then across layers
    (dolist (dc '(-1 1))
      (dolist (L layers) (push (list L r (+ c dc)) coords)))
    (nreverse coords)))

(defun daselt-abv--collect-letter-bases (placevals)
  "Map lowercase letter -> one base coord, prefer layer 1 if present.
PLACEVALS is a list of (COORD . BINDING)."
  (let ((per-letter (make-hash-table :test 'equal)))
    ;; collect all coords by letter
    (dolist (pv placevals)
      (let* ((coord (car pv)) (binding (daselt-abv--down (cdr pv))))
        (when (daselt-abv--letter-p binding)
          (push coord (gethash binding per-letter)))))
    ;; pick best coord per letter, prefer layer 1, else minimal layer/erg score
    (let ((out (make-hash-table :test 'equal)))
      (maphash
       (lambda (letter coords)
         (let* ((on-l1 (cl-find-if (lambda (cc) (= (car cc) 1)) coords))
                (best (or on-l1
                          (car (cl-sort (copy-sequence coords)
                                        (lambda (a b)
                                          (< (daselt-abv--erg-score a)
                                             (daselt-abv--erg-score b))))))))
           (puthash letter best out)))
       per-letter)
      out)))

(defun daselt-abv--affix-letters (affix)
  "Return list of lowercase unique letters of AFFIX preserving order."
  (let ((seen (make-hash-table :test 'equal))
        (out '()))
    (dotimes (i (length affix))
      (let* ((ch (aref affix i))
             (s (downcase (string ch))))
        (when (and (daselt-abv--letter-p s) (not (gethash s seen)))
          (puthash s t seen)
          (push s out))))
    (nreverse out)))

(defun daselt-abv--letter-rarity (letter preferred-hash)
  "Return how many places in PREFERRED-HASH bind to LETTER (lowercase)."
  (let ((n 0))
    (maphash
     (lambda (_coord binding)
       (when (and (stringp binding) (string= (daselt-abv--down binding) letter))
         (setq n (1+ n))))
     preferred-hash)
    n))

(defun daselt-abv--free-candidates (preferred-hash counts-hash)
  "Return coords still unfilled (assignment count 0) in PREFERRED-HASH."
  (cl-loop for coord in (daselt-abv--keys preferred-hash)
           unless (> (gethash coord counts-hash 0) 0)
           collect coord))

(defun daselt-abv--choose-fallback (preferred-hash counts-hash)
  "Choose coord by minimal assignment count, then ergonomic score."
  (let ((coords (daselt-abv--keys preferred-hash)))
    (car (cl-sort (copy-sequence coords)
                  (lambda (a b)
                    (let* ((ca (gethash a counts-hash 0))
                           (cb (gethash b counts-hash 0)))
                      (if (/= ca cb)
                          (< ca cb)
                        (< (daselt-abv--erg-score a)
                           (daselt-abv--erg-score b)))))))))

(defun daselt-abv--assign-one (affix preferred-hash counts-hash letter-base complementp)
  "Choose a coordinate for AFFIX and update COUNTS-HASH.

PREFERRED-HASH maps coordinates to their single-character binding.  COUNTS-HASH
maps coordinates to times used; it is incremented for the chosen coordinate to
balance load.  LETTER-BASE maps letters to preferred base coordinates.  If
COMPLEMENTP is non-nil, prefer a complementary layer order.  Return the chosen
coordinate tuple."
  (let* ((letters (daselt-abv--affix-letters affix))
         ;; sort letters by rarity (fewer direct matches in preferred = earlier)
         (letters-sorted
          (cl-stable-sort (copy-sequence letters) #'<
                          :key (lambda (L) (daselt-abv--letter-rarity L preferred-hash))))
         (layers (daselt-abv--coord-layers-in-order complementp))
         (chosen-coord nil))
    ;; letter-guided search
    (cl-loop for L in letters-sorted
             for base = (gethash L letter-base)
             when base do
             (pcase-let ((`(,_ ,r ,c) base))
               (let ((cands (daselt-abv--neighbors r c layers)))
                 (cl-loop for coord in cands
                          for binding = (gethash coord preferred-hash)
                          when (and binding (<= (gethash coord counts-hash 0) 0))
                          do (setq chosen-coord coord)
                          (cl-return))))
             when chosen-coord do (cl-return))
    ;; fallback if none free via letter-guided
    (unless chosen-coord
      (let ((free (daselt-abv--free-candidates preferred-hash counts-hash)))
        (setq chosen-coord
              (if free
                  (car (cl-sort free #'<
                                :key (lambda (cc) (daselt-abv--erg-score cc))))
                ;; multi-fill: everything taken once; pick min-count then ergo
                (daselt-abv--choose-fallback preferred-hash counts-hash)))))
    ;; book-keeping and return coord
    (cl-incf (gethash chosen-coord counts-hash 0))
    chosen-coord))

(defun daselt-abv--complement-hash (universe-hash subset-hash)
  "Return a hash of coords present in UNIVERSE-HASH but not in SUBSET-HASH."
  (let ((ht (make-hash-table :test 'equal)))
    (maphash
     (lambda (coord binding)
       (unless (gethash coord subset-hash)
         (puthash coord binding ht)))
     universe-hash)
    ht))

(defun daselt-abv--sorted-affixes-by-frequency (affixes freq-alist)
  "Return AFFIXES sorted by descending frequency using FREQ-ALIST; default 0.
Ties are broken by shorter length first, then lexicographically."
  (let* ((freq (daselt-abv--alist->hash freq-alist))
         (pair (mapcar (lambda (a) (cons a (or (gethash a freq) 0))) affixes)))
    (mapcar #'car
            (cl-sort pair (lambda (a b)
                            (let ((fa (cdr a)) (fb (cdr b)))
                              (if (/= fa fb) (> fa fb)
                                ;; tie-break: shorter first, then lex
                                (let ((la (length (car a)))
                                      (lb (length (car b))))
                                  (if (/= la lb) (< la lb)
                                    (string< (car a) (car b)))))))))))

(defun daselt-abv--affix-kind-for-table (sym)
  "Heuristically determine affix kind for table symbol SYM.
Return one of the symbols 'prefixes, 'suffixes, 'infixes, 'roots or nil."
  (let* ((name (symbol-name sym))
         (base (and (string-match "^daselt-abv-\(.*?\)-table$" name)
                    (match-string 1 name))))
    (cond
     ((and base (string-match-p "^prefix" base)) 'prefixes)
     ((and base (string-match-p "^suffix" base)) 'suffixes)
     ((and base (string-match-p "^infix" base)) 'infixes)
     ((and base (string-match-p "^root" base)) 'roots)
     (t nil))))

(defun daselt-abv--option-var-for-table (sym)
  "Return the abv option variable symbol corresponding to table SYM.
Supported mappings are: prefixes, suffixes, infixes, roots."
  (pcase (daselt-abv--affix-kind-for-table sym)
    ('prefixes 'abv-prefixes)
    ('suffixes 'abv-suffixes)
    ('infixes  'abv-infixes)
    ('roots    'abv-roots)
    (_ nil)))

(defun daselt-abv--ensure-hash-table (sym)
  "Ensure SYM is bound to a hash-table and return it.
If SYM is unbound or not a hash-table, bind it to a fresh hash-table."
  (unless (and (boundp sym) (hash-table-p (symbol-value sym)))
    (set sym (make-hash-table :test #'equal)))
  (symbol-value sym))

(defun daselt-abv--normalize-frequencies (freqs)
  "Normalize FREQS into an alist of (AFFIX . COUNT).

Accepts the following shapes:
- alist of (string . integer)
- alist of ((string) . integer) as from `abv-corpus-reduced-monogram-frequencies'
- alist of (symbol . integer)
Return a list of conses with string keys. Entries with non-positive counts are
removed."
  (cl-loop for (k . v) in freqs
           for key = (cond
                      ((stringp k) k)
                      ((and (consp k) (stringp (car k))) (car k))
                      ((symbolp k) (symbol-name k))
                      (t nil))
           when (and key (integerp v) (> v 0))
           collect (cons key v)))

;;;###autoload
(defun daselt-abv-assign-affixes (affixes frequencies table-sym)
  "Assign AFFIXES to Daselt coordinates by FREQUENCIES into TABLE-SYM.

AFFIXES is a list of affix strings (e.g., `abv-prefixes').  FREQUENCIES is an
alist mapping affix to count, or a list of ((affix) . count) pairs as returned
by `abv-corpus-reduced-monogram-frequencies'.  TABLE-SYM is the symbol of the
hash-table to populate (e.g., 'daselt-abv-prefix-table).

If TABLE-SYM is already bound to a hash-table, extend it without overwriting
existing entries.  The value for each key is the coordinate tuple assigned to
that affix.  Coordinates are chosen from a preferred set first (paired lower
layers), then from its complement.  Affixes are processed in descending
frequency order.

This function assigns by coordinates, not by the symbols they house."
  (interactive
   (let* ((table (intern (completing-read "Affix table symbol: " obarray
                                          (lambda (s)
                                            (string-match-p "^daselt-abv-.*-table$" (symbol-name s)))
                                          nil nil nil "daselt-abv-prefix-table")))
          (kind (daselt-abv--affix-kind-for-table table))
          (opt (or (daselt-abv--option-var-for-table table) 'abv-prefixes))
          (affs (symbol-value opt))
          (freqs (let ((corpus (when (boundp 'daselt-abv-statistics-obarray)
                                 daselt-abv-statistics-obarray)))
                   (condition-case _
                       ;; Fallback to equal weights if corpus is not available
                       (abv-corpus-reduced-monogram-frequencies affs (mapatoms #'symbol-name corpus) 'initial)
                     (error (mapcar (lambda (a) (cons a 1)) affs))))))
     (list affs freqs table)))
  (unless (and (listp affixes) (listp frequencies) (symbolp table-sym))
    (user-error "Invalid inputs: expected (list list symbol)"))
  (let* ((freqs (daselt-abv--normalize-frequencies frequencies))
         (table (daselt-abv--ensure-hash-table table-sym))
         ;; mark used affixes and coords from existing entries
         (used-coords (make-hash-table :test #'equal))
         (counts (make-hash-table :test #'equal)))
    (maphash (lambda (_a c)
               (puthash c t used-coords)
               (cl-incf (gethash c counts 0)))
             table)
    ;; Build placeval universe according to affix kind
    (pcase (daselt-abv--affix-kind-for-table table-sym)
      ('prefixes
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
              (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-prefix-placevals))
              (compl-h (daselt-abv--complement-hash univ-h paired-h))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             ;; try paired first
             (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
               (when (or (null coord) (gethash coord used-coords))
                 (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
               (when coord
                 (puthash coord t used-coords)
                 (puthash a coord table))))))
       ('suffixes
        (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-suffix-placevals))
               (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-suffix-placevals))
               (compl-h (daselt-abv--complement-hash univ-h paired-h))
               (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
               (letter-base (daselt-abv--collect-letter-bases
                             (append daselt-abv-valid-prefix-placevals
                                     daselt-abv-valid-suffix-placevals))))
          (dolist (a sorted)
            (unless (gethash a table)
              (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
                (when (or (null coord) (gethash coord used-coords))
                  (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
                (when coord
                  (puthash coord t used-coords)
                  (puthash a coord table))))))
        (_
         ;; generic: use prefix-classes as a reasonable default universe
         (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
                (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
                (letter-base (daselt-abv--collect-letter-bases
                              (append daselt-abv-valid-prefix-placevals
                                      daselt-abv-valid-suffix-placevals))))
           (dolist (a sorted)
             (unless (gethash a table)
               (let ((coord (daselt-abv--assign-one a univ-h counts letter-base nil)))
                 (when (and coord (not (gethash coord used-coords)))
                   (puthash coord t used-coords)
                   (puthash a coord table))))))))
       table))

    (defun daselt-abv--table->bindlist-var (table-sym)
      "Return the symbol to use for the bindlist variable from TABLE-SYM."
      (intern (replace-regexp-in-string "-table$" "-bindlist" (symbol-name table-sym))))

    (defun daselt-abv-table->bindlist (table-sym)
      "Convert TABLE-SYM (an affix table) into a bindlist and define a variable.

The variable name is derived by replacing the trailing "-table" with
"-bindlist" in TABLE-SYM.  Each binding in the returned bindlist has the form
(COORDS . LABELSYM), where COORDS is the coordinate tuple from the table and
LABELSYM is an interned symbol mirroring the affix string (e.g., \="pre\=" ->
\='abv-pre).  The variable is created or replaced using `set'.  Return the
bindlist."
      (interactive
       (list (intern (completing-read "Affix table: " obarray
                                      (lambda (s)
                                        (and (string-match-p "^daselt-abv-.*-table$" (symbol-name s))
                                             (hash-table-p (symbol-value s))))
                                      t))))
      (unless (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
        (user-error "Table %s is not bound to a hash-table" table-sym))
      (let* ((tbl (symbol-value table-sym))
             (blist nil))
        (maphash (lambda (k v)
                   (let* ((aff (if (stringp k) k (format "%s" k)))
                          (sym (intern (concat "abv-" aff)))
                          (coords v))
                     (push (cons coords sym) blist)))
                 tbl)
        (setq blist (nreverse blist))
        (set (daselt-abv--table->bindlist-var table-sym) blist)
        blist))

;;; Visualization
;;;###autoload
    (defun daselt-abv-draw-table-layer (table-sym laycoord &rest mods)
      "Visualize one layer of TABLE-SYM by delegating to `daselt-bind-draw-bindlist-layer'.

Ensure a bindlist exists for TABLE-SYM by converting if necessary, then draw
layer LAYCOORD with optional MODS."
      (interactive
       (let* ((table (intern (completing-read "Affix table symbol: " obarray
                                              (lambda (s)
                                                (and (string-match-p "^daselt-abv-.*-table$" (symbol-name s))
                                                     (hash-table-p (symbol-value s))))
                                              t)))
              (lay (read (read-from-minibuffer "Layer coordinate (e.g., (0)): "))))
         (list table lay)))
      (when (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
        ;; ensure bindlist
        (unless (and (boundp (daselt-abv--table->bindlist-var table-sym))
                     (daselt-bind-bindlist-p (symbol-value (daselt-abv--table->bindlist-var table-sym))))
          (daselt-abv-table->bindlist table-sym))
        (apply #'daselt-bind-draw-bindlist-layer (daselt-abv--table->bindlist-var table-sym) laycoord mods)))

    (defun daselt-abv--table-symbols ()
      "Return a list of symbols bound to affix tables named ^daselt-abv-.*-table$."
      (let (out)
        (mapatoms (lambda (s)
                    (when (and (string-match-p "^daselt-abv-.*-table$" (symbol-name s))
                               (boundp s) (hash-table-p (symbol-value s)))
                      (push s out))))
        (nreverse out)))

;;; Query and save
;;;###autoload
    (defun daselt-abv-look-up ()
      "Look up an affix in a chosen table and maybe add it to the defining option.

This command prompts for a table among variables named
\="daselt-abv-.*-table\=", then for an affix string.  If found, its
coordinates are displayed.  If no result is found, the user is asked whether
to add the query string to the variable that defined the table (e.g.,
`abv-prefixes', `abv-suffixes', `abv-infixes' or `abv-roots').  The new option
value is saved using `customize-push-and-save' when available."
      (interactive)
      (let* ((table (intern (completing-read "Affix table: " (daselt-abv--table-symbols) nil t)))
             (query (read-string (format "Affix for %s: " table)))
             (tbl (symbol-value table))
             (hit (and (hash-table-p tbl) (gethash query tbl))))
        (if hit
            (message "%s => %S" query hit)
          (let ((opt (daselt-abv--option-var-for-table table)))
            (if (not (and opt (boundp opt)))
                (message "No matching option variable for %s" table)
              (when (yes-or-no-p (format "Add %S to %S and save? " query opt))
                (if (fboundp 'customize-push-and-save)
                    (customize-push-and-save opt query)
                  (customize-set-variable opt (append (symbol-value opt) (list query)))
                  (customize-save-customized))
                (message "Added %S to %S" query opt)))))))

;;;###autoload
    (defun daselt-abv-save (&optional file)
      "Save all tables named \="daselt-abv-.*-table\=" into FILE.

FILE defaults to `daselt-abv-save-file'.  This overwrites existing contents.
Tables are serialized using `abv-corpus-print-table'."
      (interactive)
      (let ((outfile (or file daselt-abv-save-file)))
        (let ((dir (file-name-directory outfile)))
          (when dir (ignore-errors (make-directory dir t))))
        (with-temp-file outfile
          (insert ";;; Saved daselt-abv tables\n\n")
          (dolist (s (daselt-abv--table-symbols))
            (insert (format ";;; %s\n" s))
            (abv-corpus-print-table s)
            (insert "\n\n")))
        (message "Saved %d tables to %s" (length (daselt-abv--table-symbols)) outfile)))


;;;; Legacy batch assignment (deprecated)
    ;; Keeping for reference; new `daselt-abv-assign-affixes' is table-oriented.
    (defun daselt-abv-assign-prefixes-and-suffixes ()
      "Compute and set `daselt-abv-prefix-table' and `daselt-abv-suffix-table'.
Deprecated: prefer calling `daselt-abv-assign-affixes' for each table."
      (interactive)
      (daselt-abv-assign-affixes abv-prefixes (mapcar (lambda (a) (cons a 1)) abv-prefixes) 'daselt-abv-prefix-table)
      (daselt-abv-assign-affixes abv-suffixes (mapcar (lambda (a) (cons a 1)) abv-suffixes) 'daselt-abv-suffix-table))

;;;; Hooks
    (add-hook 'abv-mode-hook
              #'daselt-abv-add-to-temporary-word-constituents
              89)

    (remove-hook 'daselt-mode-hook
                 (lambda ()
                   (unless daselt-mode
                     (remove-hook 'abv-mode-hook
                                  #'daselt-abv-add-to-temporary-word-constituents))))

    (provide 'daselt-abv)
;;; daselt-abv.del ends here

#+end_tool
#+begin_tool (read_buffer :buffer "daselt-abv.del")
(:name "read_buffer" :args (:buffer "daselt-abv.del"))

;;; daselt-abv.del --- Daselt's abv module  -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: tools, abbrev, convenience

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Affix placement and visualization utilities for Daselt.
;;
;; This module provides functions to:
;; - derive candidate coordinates from the active Daselt layout by syntax class
;; - assign affixes (prefixes/suffixes/infixes/roots) to coordinates
;;   guided by frequency and letter proximity
;; - convert affix tables into bindlists for the drawing/preview pipeline
;; - visualize single layers of a generated table
;; - query affix tables and extend the defining options if an affix is missing
;; - persist and restore tables.

;;; Code:

(require 'daselt-dirs)
(require 'cl-lib)
(require 'abv-data)
(require 'abv-corpus)

;;;; Options
(defgroup daselt-abv
  nil
  "Daselt helpers for assigning and visualizing affix placements."
  :group 'daselt-mode
  :prefix "daselt-abv-")

(defcustom daselt-abv-non-constituents
  '(?- ?_ ?: ?. ?, ?! ?? ?' ?`)
  "Characters in the Daselt-layout to exclude from word-constituents.

Used by `daselt-abv-add-to-temporary-word-constituents'.  It is not necessary
to add non-characters, such as command keys; brackets are also unnecessary."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-single-symbol-placevals
  (let ((print-level nil) (print-length nil))
    (cl-remove-if
     (lambda (cons)
       (or (< 1 (length (cdr cons)))
           (let* ((char (string-to-char (cdr cons)))
                  (syx (with-syntax-table text-mode-syntax-table (char-syntax char))))
             (cl-member syx '(?\  ?\- ?\") :test #'=))))
     (daselt-base-flatten-n-times (cdr (daselt-coords-coordinatize-layout
                                        (symbol-value daselt-dfk-layout))) 2)))
  "Placevals used for single-symbol abbrevs.
Each element is a cons of the form (COORD . STRING-BINDING)."
  :type '(repeat (cons (repeat integer) string))
  :group 'daselt-mode)

(defcustom daselt-abv-prefix-classes
  '(?w ?_ ?. ?\) ?\> ?\!)
  "Syntax classes used to collect candidate prefix positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-suffix-classes
  '(?w ?_ ?\( ?\< ?\!)
  "Syntax classes used to collect candidate suffix positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-infix-classes
  '(?w ?_ ?. ?\( ?\< ?\) ?\> ?\!)
  "Syntax classes used to collect candidate infix positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-standalone-classes
  '(?w ?_ ?. ?\( ?\< ?\) ?\> ?\!)
  "Syntax classes used to collect candidate standalone positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-root-classes
  '(?w ?_)
  "Syntax classes used to collect candidate root positions."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-layer-pairs
  '((1 . 2) (4 . 3) (5 . 6) (8 . 7))
  "Pairs of layers considered lower/upper for letter pairing heuristics.
This is used to prefer lower layers with corresponding upper-case partner."
  :type '(repeat (cons integer integer))
  :group 'daselt-abv)

(defcustom daselt-abv-special-symbols
  '(?. ?,)
  "Special symbols that should not be used for roots and affixes.

By default, we are using "." for grammar delimitation and "," for yasnippet."
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-prefix-symbols
  nil
  "Special symbols that should not be used for prefixes."
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-suffix-symbols
  '(?↔)
  "Special symbols that should not be used for suffixes.

By default we are using "↔" to disambiguate names."
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-added-syntax-classes
  '(?_)
  "Syntax classes added to `abv-temporary-word-constituents' by Daselt.

By default only consists of the syntax class ?_, which designates symbols."
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-save-file
  (let ((dir (ignore-errors (and (boundp 'daselt-mode-pkg-configs-directory)
                                 (file-name-as-directory daselt-mode-pkg-configs-directory)))))
    (expand-file-name (concat (or dir (file-name-as-directory user-emacs-directory))
                              "abv/save.el")))
  "File into which to save generated affix tables.

The file will contain `defconst' forms for all variables whose names match
"^daselt-abv-.*-table$".  Existing content is overwritten."
  :type 'file
  :group 'daselt-abv)


;; Statistics configuration
(defcustom daselt-abv-statistics-file-types
  '("txt" "org" "md" "el" "del")
  "Filename extensions to include when computing personal statistics.
Each element is a string like \="txt\=" or \="org\=". Matching is case-insensitive."
  :type '(repeat string)
  :group 'daselt-mode)

(defcustom daselt-abv-statistics-file
  (expand-file-name "daselt-abv-stats.el" user-emacs-directory)
  "File where personal word-frequency statistics are written and read from."
  :type 'file
  :group 'daselt-mode)



;;;; Variables
(defvar daselt-abv-valid-prefix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-prefix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
  "Placevals considered valid for prefix placement.
Each entry is of the form (COORD . STRING-BINDING).")

(defvar daselt-abv-valid-lower-paired-prefix-placevals
  (cl-intersection daselt-abv-valid-prefix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
                   :test #'equal)
  "Subset of valid prefix placevals that are lower/upper paired.")

(defvar daselt-abv-valid-suffix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-suffix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
  "Placevals considered valid for suffix placement.")

(defvar daselt-abv-valid-lower-paired-suffix-placevals
  (cl-intersection daselt-abv-valid-suffix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
                   :test #'equal)
  "Subset of valid suffix placevals that are lower/upper paired.")




(defvar daselt-abv-temporary-word-constituents-backup
  abv-temporary-word-constituents
  "Backup of `abv-temporary-word-constituents' prior to our additions.")

(defvar daselt-abv-prefix-table nil
  "Hash-table mapping prefix strings to coordinate tuples.")
(defvar daselt-abv-suffix-table nil
  "Hash-table mapping suffix strings to coordinate tuples.")

;;;; Functions
(defun daselt-abv-placevals-from-classes (classes)
  "Return placevals in the active layout whose syntax class is in CLASSES.

A placeval is a cons of (COORD . STRING-BINDING).  COORD is a coordinate tuple
like (LAYER ROW COL).  CLASSES is a list of syntax class characters as returned
by `char-syntax'."
  (let ((print-level nil) (print-length nil))
    (-filter
     (lambda (cons)
       (and (= 1 (length (cdr cons)))
            (let* ((char (string-to-char (cdr cons)))
                   (syx (with-syntax-table org-mode-syntax-table (char-syntax char))))
              (cl-member syx classes :test #'=))))
     (daselt-base-flatten-n-times
      (cdr (daselt-coords-coordinatize-layout
            (symbol-value daselt-dfk-layout))) 2))))

(defun daselt-abv--filter-to-lower-layers (placevals)
  "Filter PLACEVALS to those residing on lower layers from `daselt-abv-layer-pairs'."
  (-filter (lambda (cns)
             (cl-member (caar cns) (mapcar #'car daselt-abv-layer-pairs)))
           placevals))

(defun daselt-abv--filter-to-upper-lower-paired-placevals (placevals)
  "Keep PLACEVALS whose upper-layer counterpart is a single symbol as well.

This prefers keys that have a clean upper-case pair on their partner layer."
  (-filter (lambda (cns)
             (let* ((coords (car cns))
                    (uppercoords (list (1+ (alist-get (car coords)
                                                      daselt-abv-layer-pairs))
                                       (nth 1 coords)
                                       (nth 2 coords))))
               (not (< 1 (length (daselt-coords-binding uppercoords))))))
           (daselt-abv--filter-to-lower-layers placevals)))

(defun daselt-abv-add-to-temporary-word-constituents ()
  "Add characters from `daselt-xkb-layout' to `abv-temporary-word-constituents'.

Adds characters whose syntax class is in `daselt-abv-added-syntax-classes'.
Excludes symbols from `daselt-abv-non-constituents'."
  (declare (ftype (function () t)))
  (setopt abv-temporary-word-constituents
          (cl-union daselt-abv-temporary-word-constituents-backup
                    (let ((laylist (flatten-list (symbol-value daselt-xkb-layout))))
                      (remq
                       nil (mapcar
                            (lambda (entry)
                              (if (= 1 (length entry))
                                  (let ((char (string-to-char entry)))
                                    (if (and (or (memq (char-syntax char)
                                                       daselt-abv-added-syntax-classes))
                                             (not (cl-member
                                                   char
                                                   daselt-abv-non-constituents
                                                   :test #'=)))
                                        char))))
                            laylist)))))
  (abv--generate-syntax-table))

;;;;; Personal statistics

(defvar daselt-abv-statistics-obarray (make-vector 4096 0)
  "Obarray holding personal word-frequency statistics.

Symbols are interned for words; their symbol-values hold the counts.")

(defun daselt-abv--count-words-into (file table)
  "Count words in FILE and accumulate into hash TABLE.

Words are sequences that match the regexp "\\(\\sw\\|\\s_\\)+". Counting is
case-insensitive. Files that cannot be decoded or read are skipped."
  (when (and (stringp file) (file-readable-p file))
    (condition-case _err
        (with-temp-buffer
          ;; Use automatic decoding; avoid visiting to keep things light-weight.
          (insert-file-contents file nil nil nil t)
          (goto-char (point-min))
          (let ((case-fold-search t))
            (while (re-search-forward "\\(\\sw\\|\\s_\\)+" nil t)
              (let* ((w (downcase (match-string-no-properties 0)))
                     (old (gethash w table 0)))
                (puthash w (1+ old) table)))))
      (error nil))))

(defun daselt-abv-read-statistics ()
  "Read `daselt-abv-statistics-file' into an obarray.

Returns the newly created obarray stored in
`daselt-abv-statistics-obarray'."
  (interactive)
  (setq daselt-abv-statistics-obarray (make-vector 4096 0))
  (when (file-readable-p daselt-abv-statistics-file)
    (with-temp-buffer
      (insert-file-contents daselt-abv-statistics-file)
      (goto-char (point-min))
      (condition-case _err
          (let ((data (read (current-buffer))))
            (dolist (pair data)
              (when (and (consp pair)
                         (stringp (car pair))
                         (numberp (cdr pair)))
                (let ((sym (intern (car pair) daselt-abv-statistics-obarray)))
                  (set sym (cdr pair))))))
        (error nil))))
  daselt-abv-statistics-obarray)

;;;;; Assignment helpers
(defun daselt-abv--alist->hash (alist &optional key-fn)
  "Return a hash table from ALIST; transform keys via KEY-FN if non-nil."
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (kv alist ht)
      (let ((k (if key-fn (funcall key-fn (car kv)) (car kv)))
            (v (cdr kv)))
        (puthash k v ht)))))

(defun daselt-abv--placevals->coord-hash (placevals)
  "Return hash mapping coord -> binding for PLACEVALS.
A placeval has the shape ((LAYER ROW COL) . STRING-BINDING)."
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (pv placevals ht)
      (puthash (car pv) (cdr pv) ht))))

(defun daselt-abv--keys (hash)
  "Return a list of keys of HASH."
  (let (ks)
    (maphash (lambda (k _v) (push k ks)) hash)
    hash
    ks))

(defun daselt-abv--letter-p (s)
  "Return non-nil if S is a single ASCII alphabetic letter (A-Z or a-z)."
  (and (stringp s) (= (length s) 1)
       (let ((c (aref s 0)))
         (or (and (<= ?a c) (<= c ?z))
             (and (<= ?A c) (<= c ?Z))))))

(defun daselt-abv--down (s)
  "Return lowercase of single-letter string S; identity otherwise."
  (if (and (stringp s) (= (length s) 1))
      (downcase s)
    s))

(defun daselt-abv--erg-score (coord)
  "Return ergonomic score for COORD; lower is better.
Heuristic per main_layout_heuristics.org."
  (pcase-let ((`(,L ,row ,col) coord))
    (let* ((abscol (abs col))
           (row-pen (cond ((= row 0) 0.0)
                          (t 0.6))) ;; top/bottom small penalty
           (col-pen (pcase abscol
                      (1 1.0) (2 1.2) (3 1.5) (4 2.0) (5 3.0) (6 4.0)
                      (_ 5.0)))
           (layer-pen (pcase L
                        (1 0.0)
                        (2 0.2) (4 0.2)
                        (6 0.4) (5 0.4)
                        (7 0.6) (3 0.6)
                        (8 0.8)
                        (_ 1.0))))
      (+ row-pen col-pen layer-pen))))

(defun daselt-abv--coord-layers-in-order (complementp)
  "Return order of layers by ease; if COMPLEMENTP, prefer 2 before 1."
  (let ((base '(1 2 4 6 5 7 3 8)))
    (if complementp
        '(2 1 4 6 5 7 3 8)
      base)))

(defun daselt-abv--neighbors (r c layers)
  "Produce coords around (R,C) across LAYERS in the guide's order."
  (let ((coords '()))
    ;; same row/col across layers
    (dolist (L layers) (push (list L r c) coords))
    ;; adjacent rows then across layers
    (dolist (dr '(-1 1))
      (dolist (L layers) (push (list L (+ r dr) c) coords)))
    ;; column-adjacent then across layers
    (dolist (dc '(-1 1))
      (dolist (L layers) (push (list L r (+ c dc)) coords)))
    (nreverse coords)))

(defun daselt-abv--collect-letter-bases (placevals)
  "Map lowercase letter -> one base coord, prefer layer 1 if present.
PLACEVALS is a list of (COORD . BINDING)."
  (let ((per-letter (make-hash-table :test 'equal)))
    ;; collect all coords by letter
    (dolist (pv placevals)
      (let* ((coord (car pv)) (binding (daselt-abv--down (cdr pv))))
        (when (daselt-abv--letter-p binding)
          (push coord (gethash binding per-letter)))))
    ;; pick best coord per letter, prefer layer 1, else minimal layer/erg score
    (let ((out (make-hash-table :test 'equal)))
      (maphash
       (lambda (letter coords)
         (let* ((on-l1 (cl-find-if (lambda (cc) (= (car cc) 1)) coords))
                (best (or on-l1
                          (car (cl-sort (copy-sequence coords)
                                        (lambda (a b)
                                          (< (daselt-abv--erg-score a)
                                             (daselt-abv--erg-score b))))))))
           (puthash letter best out)))
       per-letter)
      out)))

(defun daselt-abv--affix-letters (affix)
  "Return list of lowercase unique letters of AFFIX preserving order."
  (let ((seen (make-hash-table :test 'equal))
        (out '()))
    (dotimes (i (length affix))
      (let* ((ch (aref affix i))
             (s (downcase (string ch))))
        (when (and (daselt-abv--letter-p s) (not (gethash s seen)))
          (puthash s t seen)
          (push s out))))
    (nreverse out)))

(defun daselt-abv--letter-rarity (letter preferred-hash)
  "Return how many places in PREFERRED-HASH bind to LETTER (lowercase)."
  (let ((n 0))
    (maphash
     (lambda (_coord binding)
       (when (and (stringp binding) (string= (daselt-abv--down binding) letter))
         (setq n (1+ n))))
     preferred-hash)
    n))

(defun daselt-abv--free-candidates (preferred-hash counts-hash)
  "Return coords still unfilled (assignment count 0) in PREFERRED-HASH."
  (cl-loop for coord in (daselt-abv--keys preferred-hash)
           unless (> (gethash coord counts-hash 0) 0)
           collect coord))

(defun daselt-abv--choose-fallback (preferred-hash counts-hash)
  "Choose coord by minimal assignment count, then ergonomic score."
  (let ((coords (daselt-abv--keys preferred-hash)))
    (car (cl-sort (copy-sequence coords)
                  (lambda (a b)
                    (let* ((ca (gethash a counts-hash 0))
                           (cb (gethash b counts-hash 0)))
                      (if (/= ca cb)
                          (< ca cb)
                        (< (daselt-abv--erg-score a)
                           (daselt-abv--erg-score b)))))))))

(defun daselt-abv--assign-one (affix preferred-hash counts-hash letter-base complementp)
  "Choose a coordinate for AFFIX and update COUNTS-HASH.

PREFERRED-HASH maps coordinates to their single-character binding.  COUNTS-HASH
maps coordinates to times used; it is incremented for the chosen coordinate to
balance load.  LETTER-BASE maps letters to preferred base coordinates.  If
COMPLEMENTP is non-nil, prefer a complementary layer order.  Return the chosen
coordinate tuple."
  (let* ((letters (daselt-abv--affix-letters affix))
         ;; sort letters by rarity (fewer direct matches in preferred = earlier)
         (letters-sorted
          (cl-stable-sort (copy-sequence letters) #'<
                          :key (lambda (L) (daselt-abv--letter-rarity L preferred-hash))))
         (layers (daselt-abv--coord-layers-in-order complementp))
         (chosen-coord nil))
    ;; letter-guided search
    (cl-loop for L in letters-sorted
             for base = (gethash L letter-base)
             when base do
             (pcase-let ((`(,_ ,r ,c) base))
               (let ((cands (daselt-abv--neighbors r c layers)))
                 (cl-loop for coord in cands
                          for binding = (gethash coord preferred-hash)
                          when (and binding (<= (gethash coord counts-hash 0) 0))
                          do (setq chosen-coord coord)
                          (cl-return))))
             when chosen-coord do (cl-return))
    ;; fallback if none free via letter-guided
    (unless chosen-coord
      (let ((free (daselt-abv--free-candidates preferred-hash counts-hash)))
        (setq chosen-coord
              (if free
                  (car (cl-sort free #'<
                                :key (lambda (cc) (daselt-abv--erg-score cc))))
                ;; multi-fill: everything taken once; pick min-count then ergo
                (daselt-abv--choose-fallback preferred-hash counts-hash)))))
    ;; book-keeping and return coord
    (cl-incf (gethash chosen-coord counts-hash 0))
    chosen-coord))

(defun daselt-abv--complement-hash (universe-hash subset-hash)
  "Return a hash of coords present in UNIVERSE-HASH but not in SUBSET-HASH."
  (let ((ht (make-hash-table :test 'equal)))
    (maphash
     (lambda (coord binding)
       (unless (gethash coord subset-hash)
         (puthash coord binding ht)))
     universe-hash)
    ht))

(defun daselt-abv--sorted-affixes-by-frequency (affixes freq-alist)
  "Return AFFIXES sorted by descending frequency using FREQ-ALIST; default 0.
Ties are broken by shorter length first, then lexicographically."
  (let* ((freq (daselt-abv--alist->hash freq-alist))
         (pair (mapcar (lambda (a) (cons a (or (gethash a freq) 0))) affixes)))
    (mapcar #'car
            (cl-sort pair (lambda (a b)
                            (let ((fa (cdr a)) (fb (cdr b)))
                              (if (/= fa fb) (> fa fb)
                                ;; tie-break: shorter first, then lex
                                (let ((la (length (car a)))
                                      (lb (length (car b))))
                                  (if (/= la lb) (< la lb)
                                    (string< (car a) (car b)))))))))))

(defun daselt-abv--affix-kind-for-table (sym)
  "Heuristically determine affix kind for table symbol SYM.
Return one of the symbols 'prefixes, 'suffixes, 'infixes, 'roots or nil."
  (let* ((name (symbol-name sym))
         (base (and (string-match "^daselt-abv-\(.*?\)-table$" name)
                    (match-string 1 name))))
    (cond
     ((and base (string-match-p "^prefix" base)) 'prefixes)
     ((and base (string-match-p "^suffix" base)) 'suffixes)
     ((and base (string-match-p "^infix" base)) 'infixes)
     ((and base (string-match-p "^root" base)) 'roots)
     (t nil))))

(defun daselt-abv--option-var-for-table (sym)
  "Return the abv option variable symbol corresponding to table SYM.
Supported mappings are: prefixes, suffixes, infixes, roots."
  (pcase (daselt-abv--affix-kind-for-table sym)
    ('prefixes 'abv-prefixes)
    ('suffixes 'abv-suffixes)
    ('infixes  'abv-infixes)
    ('roots    'abv-roots)
    (_ nil)))

(defun daselt-abv--ensure-hash-table (sym)
  "Ensure SYM is bound to a hash-table and return it.
If SYM is unbound or not a hash-table, bind it to a fresh hash-table."
  (unless (and (boundp sym) (hash-table-p (symbol-value sym)))
    (set sym (make-hash-table :test #'equal)))
  (symbol-value sym))

(defun daselt-abv--normalize-frequencies (freqs)
  "Normalize FREQS into an alist of (AFFIX . COUNT).

Accepts the following shapes:
- alist of (string . integer)
- alist of ((string) . integer) as from `abv-corpus-reduced-monogram-frequencies'
- alist of (symbol . integer)
Return a list of conses with string keys. Entries with non-positive counts are
removed."
  (cl-loop for (k . v) in freqs
           for key = (cond
                      ((stringp k) k)
                      ((and (consp k) (stringp (car k))) (car k))
                      ((symbolp k) (symbol-name k))
                      (t nil))
           when (and key (integerp v) (> v 0))
           collect (cons key v)))

;;;###autoload
(defun daselt-abv-assign-affixes (affixes frequencies table-sym)
  "Assign AFFIXES to Daselt coordinates by FREQUENCIES into TABLE-SYM.

AFFIXES is a list of affix strings (e.g., `abv-prefixes').  FREQUENCIES is an
alist mapping affix to count, or a list of ((affix) . count) pairs as returned
by `abv-corpus-reduced-monogram-frequencies'.  TABLE-SYM is the symbol of the
hash-table to populate (e.g., 'daselt-abv-prefix-table).

If TABLE-SYM is already bound to a hash-table, extend it without overwriting
existing entries.  The value for each key is the coordinate tuple assigned to
that affix.  Coordinates are chosen from a preferred set first (paired lower
layers), then from its complement.  Affixes are processed in descending
frequency order.

This function assigns by coordinates, not by the symbols they house."
  (interactive
   (let* ((table (intern (completing-read "Affix table symbol: " obarray
                                          (lambda (s)
                                            (string-match-p "^daselt-abv-.*-table$" (symbol-name s)))
                                          nil nil nil "daselt-abv-prefix-table")))
          (kind (daselt-abv--affix-kind-for-table table))
          (opt (or (daselt-abv--option-var-for-table table) 'abv-prefixes))
          (affs (symbol-value opt))
          (freqs (let ((corpus (when (boundp 'daselt-abv-statistics-obarray)
                                 daselt-abv-statistics-obarray)))
                   (condition-case _
                       ;; Fallback to equal weights if corpus is not available
                       (abv-corpus-reduced-monogram-frequencies affs (mapatoms #'symbol-name corpus) 'initial)
                     (error (mapcar (lambda (a) (cons a 1)) affs))))))
     (list affs freqs table)))
  (unless (and (listp affixes) (listp frequencies) (symbolp table-sym))
    (user-error "Invalid inputs: expected (list list symbol)"))
  (let* ((freqs (daselt-abv--normalize-frequencies frequencies))
         (table (daselt-abv--ensure-hash-table table-sym))
         ;; mark used affixes and coords from existing entries
         (used-coords (make-hash-table :test #'equal))
         (counts (make-hash-table :test #'equal)))
    (maphash (lambda (_a c)
               (puthash c t used-coords)
               (cl-incf (gethash c counts 0)))
             table)
    ;; Build placeval universe according to affix kind
    (pcase (daselt-abv--affix-kind-for-table table-sym)
      ('prefixes
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
              (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-prefix-placevals))
              (compl-h (daselt-abv--complement-hash univ-h paired-h))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             ;; try paired first
             (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
               (when (or (null coord) (gethash coord used-coords))
                 (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
               (when coord
                 (puthash coord t used-coords)
                 (puthash a coord table))))))
       ('suffixes
        (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-suffix-placevals))
               (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-suffix-placevals))
               (compl-h (daselt-abv--complement-hash univ-h paired-h))
               (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
               (letter-base (daselt-abv--collect-letter-bases
                             (append daselt-abv-valid-prefix-placevals
                                     daselt-abv-valid-suffix-placevals))))
          (dolist (a sorted)
            (unless (gethash a table)
              (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
                (when (or (null coord) (gethash coord used-coords))
                  (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
                (when coord
                  (puthash coord t used-coords)
                  (puthash a coord table))))))
        (_
         ;; generic: use prefix-classes as a reasonable default universe
         (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
                (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
                (letter-base (daselt-abv--collect-letter-bases
                              (append daselt-abv-valid-prefix-placevals
                                      daselt-abv-valid-suffix-placevals))))
           (dolist (a sorted)
             (unless (gethash a table)
               (let ((coord (daselt-abv--assign-one a univ-h counts letter-base nil)))
                 (when (and coord (not (gethash coord used-coords)))
                   (puthash coord t used-coords)
                   (puthash a coord table))))))))
       table))

    (defun daselt-abv--table->bindlist-var (table-sym)
      "Return the symbol to use for the bindlist variable from TABLE-SYM."
      (intern (replace-regexp-in-string "-table$" "-bindlist" (symbol-name table-sym))))

    (defun daselt-abv-table->bindlist (table-sym)
      "Convert TABLE-SYM (an affix table) into a bindlist and define a variable.

The variable name is derived by replacing the trailing "-table" with
"-bindlist" in TABLE-SYM.  Each binding in the returned bindlist has the form
(COORDS . LABELSYM), where COORDS is the coordinate tuple from the table and
LABELSYM is an interned symbol mirroring the affix string (e.g., \="pre\=" ->
\='abv-pre).  The variable is created or replaced using `set'.  Return the
bindlist."
      (interactive
       (list (intern (completing-read "Affix table: " obarray
                                      (lambda (s)
                                        (and (string-match-p "^daselt-abv-.*-table$" (symbol-name s))
                                             (hash-table-p (symbol-value s))))
                                      t))))
      (unless (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
        (user-error "Table %s is not bound to a hash-table" table-sym))
      (let* ((tbl (symbol-value table-sym))
             (blist nil))
        (maphash (lambda (k v)
                   (let* ((aff (if (stringp k) k (format "%s" k)))
                          (sym (intern (concat "abv-" aff)))
                          (coords v))
                     (push (cons coords sym) blist)))
                 tbl)
        (setq blist (nreverse blist))
        (set (daselt-abv--table->bindlist-var table-sym) blist)
        blist))

;;; Visualization
;;;###autoload
    (defun daselt-abv-draw-table-layer (table-sym laycoord &rest mods)
      "Visualize one layer of TABLE-SYM by delegating to `daselt-bind-draw-bindlist-layer'.

Ensure a bindlist exists for TABLE-SYM by converting if necessary, then draw
layer LAYCOORD with optional MODS."
      (interactive
       (let* ((table (intern (completing-read "Affix table symbol: " obarray
                                              (lambda (s)
                                                (and (string-match-p "^daselt-abv-.*-table$" (symbol-name s))
                                                     (hash-table-p (symbol-value s))))
                                              t)))
              (lay (read (read-from-minibuffer "Layer coordinate (e.g., (0)): "))))
         (list table lay)))
      (when (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
        ;; ensure bindlist
        (unless (and (boundp (daselt-abv--table->bindlist-var table-sym))
                     (daselt-bind-bindlist-p (symbol-value (daselt-abv--table->bindlist-var table-sym))))
          (daselt-abv-table->bindlist table-sym))
        (apply #'daselt-bind-draw-bindlist-layer (daselt-abv--table->bindlist-var table-sym) laycoord mods)))

    (defun daselt-abv--table-symbols ()
      "Return a list of symbols bound to affix tables named ^daselt-abv-.*-table$."
      (let (out)
        (mapatoms (lambda (s)
                    (when (and (string-match-p "^daselt-abv-.*-table$" (symbol-name s))
                               (boundp s) (hash-table-p (symbol-value s)))
                      (push s out))))
        (nreverse out)))

;;; Query and save
;;;###autoload
    (defun daselt-abv-look-up ()
      "Look up an affix in a chosen table and maybe add it to the defining option.

This command prompts for a table among variables named
\="daselt-abv-.*-table\=", then for an affix string.  If found, its
coordinates are displayed.  If no result is found, the user is asked whether
to add the query string to the variable that defined the table (e.g.,
`abv-prefixes', `abv-suffixes', `abv-infixes' or `abv-roots').  The new option
value is saved using `customize-push-and-save' when available."
      (interactive)
      (let* ((table (intern (completing-read "Affix table: " (daselt-abv--table-symbols) nil t)))
             (query (read-string (format "Affix for %s: " table)))
             (tbl (symbol-value table))
             (hit (and (hash-table-p tbl) (gethash query tbl))))
        (if hit
            (message "%s => %S" query hit)
          (let ((opt (daselt-abv--option-var-for-table table)))
            (if (not (and opt (boundp opt)))
                (message "No matching option variable for %s" table)
              (when (yes-or-no-p (format "Add %S to %S and save? " query opt))
                (if (fboundp 'customize-push-and-save)
                    (customize-push-and-save opt query)
                  (customize-set-variable opt (append (symbol-value opt) (list query)))
                  (customize-save-customized))
                (message "Added %S to %S" query opt)))))))

;;;###autoload
    (defun daselt-abv-save (&optional file)
      "Save all tables named \="daselt-abv-.*-table\=" into FILE.

FILE defaults to `daselt-abv-save-file'.  This overwrites existing contents.
Tables are serialized using `abv-corpus-print-table'."
      (interactive)
      (let ((outfile (or file daselt-abv-save-file)))
        (let ((dir (file-name-directory outfile)))
          (when dir (ignore-errors (make-directory dir t))))
        (with-temp-file outfile
          (insert ";;; Saved daselt-abv tables\n\n")
          (dolist (s (daselt-abv--table-symbols))
            (insert (format ";;; %s\n" s))
            (abv-corpus-print-table s)
            (insert "\n\n")))
        (message "Saved %d tables to %s" (length (daselt-abv--table-symbols)) outfile)))


;;;; Legacy batch assignment (deprecated)
    ;; Keeping for reference; new `daselt-abv-assign-affixes' is table-oriented.
    (defun daselt-abv-assign-prefixes-and-suffixes ()
      "Compute and set `daselt-abv-prefix-table' and `daselt-abv-suffix-table'.
Deprecated: prefer calling `daselt-abv-assign-affixes' for each table."
      (interactive)
      (daselt-abv-assign-affixes abv-prefixes (mapcar (lambda (a) (cons a 1)) abv-prefixes) 'daselt-abv-prefix-table)
      (daselt-abv-assign-affixes abv-suffixes (mapcar (lambda (a) (cons a 1)) abv-suffixes) 'daselt-abv-suffix-table))

;;;; Hooks
    (add-hook 'abv-mode-hook
              #'daselt-abv-add-to-temporary-word-constituents
              89)

    (remove-hook 'daselt-mode-hook
                 (lambda ()
                   (unless daselt-mode
                     (remove-hook 'abv-mode-hook
                                  #'daselt-abv-add-to-temporary-word-constituents))))

    (provide 'daselt-abv)
;;; daselt-abv.del ends here

#+end_tool
#+begin_tool (modify_buffer :buffer "daselt-abv.del" :content "; ...)
(:name "modify_buffer" :args (:buffer "daselt-abv.del" :content ";;; daselt-abv.del --- Daselt's abv module  -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Alexander Prähauser

;; Author: Alexander Prähauser <ahprae@protonmail.com>
;; Keywords: tools, abbrev, convenience

;; This program 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 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
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Affix placement and visualization utilities for Daselt.
;;
;; This module provides functions to:
;; - derive candidate coordinates from the active Daselt layout by syntax class
;; - assign affixes (prefixes/suffixes/infixes/roots) to coordinates
;;   guided by frequency and letter proximity
;; - convert affix tables into bindlists for the drawing/preview pipeline
;; - visualize single layers of a generated table
;; - query affix tables and extend the defining options if an affix is missing
;; - persist and restore tables.

;;; Code:

(require 'daselt-dirs)
(require 'cl-lib)
(require 'abv-data)
(require 'abv-corpus)
(require 'dash) ;; for -filter

;;;; Options
(defgroup daselt-abv
  nil
  \"Daselt helpers for assigning and visualizing affix placements.\"
  :group 'daselt-mode
  :prefix \"daselt-abv-\")

(defcustom daselt-abv-non-constituents
  '(?- ?_ ?: ?. ?, ?! ?? ?' ?`)
  \"Characters in the Daselt-layout to exclude from word-constituents.

Used by `daselt-abv-add-to-temporary-word-constituents'.  It is not necessary
to add non-characters, such as command keys; brackets are also unnecessary.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-single-symbol-placevals
  (let ((print-level nil) (print-length nil))
    (cl-remove-if
     (lambda (cons)
       (or (< 1 (length (cdr cons)))
           (let* ((char (string-to-char (cdr cons)))
                  (syx (with-syntax-table text-mode-syntax-table (char-syntax char))))
             (cl-member syx '(?\\  ?\\- ?\\\") :test #'=))))
     (daselt-base-flatten-n-times (cdr (daselt-coords-coordinatize-layout
                                        (symbol-value daselt-dfk-layout))) 2)))
  \"Placevals used for single-symbol abbrevs.
Each element is a cons of the form (COORD . STRING-BINDING).\"
  :type '(repeat (cons (repeat integer) string))
  :group 'daselt-mode)

(defcustom daselt-abv-prefix-classes
  '(?w ?_ ?. ?\\) ?\\> ?\\!)
  \"Syntax classes used to collect candidate prefix positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-suffix-classes
  '(?w ?_ ?\\( ?\\< ?\\!)
  \"Syntax classes used to collect candidate suffix positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-infix-classes
  '(?w ?_ ?. ?\\( ?\\< ?\\) ?\\!)
  \"Syntax classes used to collect candidate infix positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-standalone-classes
  '(?w ?_ ?. ?\\( ?\\< ?\\) ?\\!)
  \"Syntax classes used to collect candidate standalone positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-root-classes
  '(?w ?_)
  \"Syntax classes used to collect candidate root positions.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-layer-pairs
  '((1 . 2) (4 . 3) (5 . 6) (8 . 7))
  \"Pairs of layers considered lower/upper for letter pairing heuristics.
This is used to prefer lower layers with corresponding upper-case partner.\"
  :type '(repeat (cons integer integer))
  :group 'daselt-abv)

(defcustom daselt-abv-special-symbols
  '(?. ?,)
  \"Special symbols that should not be used for roots and affixes.

By default, we are using \".\" for grammar delimitation and \",\" for yasnippet.\"
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-prefix-symbols
  nil
  \"Special symbols that should not be used for prefixes.\"
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-special-non-suffix-symbols
  '(?↔)
  \"Special symbols that should not be used for suffixes.

By default we are using \"↔\" to disambiguate names.\"
  :type '(repeat character)
  :group 'daselt-abv)

(defcustom daselt-abv-added-syntax-classes
  '(?_)
  \"Syntax classes added to `abv-temporary-word-constituents' by Daselt.

By default only consists of the syntax class ?_, which designates symbols.\"
  :type '(repeat character)
  :group 'daselt-mode)

(defcustom daselt-abv-save-file
  (let ((dir (ignore-errors (and (boundp 'daselt-mode-pkg-configs-directory)
                                 (file-name-as-directory daselt-mode-pkg-configs-directory)))))
    (expand-file-name (concat (or dir (file-name-as-directory user-emacs-directory))
                              \"abv/save.el\")))
  \"File into which to save generated affix tables.

The file will contain `defconst' forms for all variables whose names match
\"^daselt-abv-.*-table$\".  Existing content is overwritten.\"
  :type 'file
  :group 'daselt-abv)


;; Statistics configuration
(defcustom daselt-abv-statistics-file-types
  '(\"txt\" \"org\" \"md\" \"el\" \"del\")
  \"Filename extensions to include when computing personal statistics.
Each element is a string like \\=\"txt\\=\" or \\=\"org\\=\". Matching is case-insensitive.\"
  :type '(repeat string)
  :group 'daselt-mode)

(defcustom daselt-abv-statistics-file
  (expand-file-name \"daselt-abv-stats.el\" user-emacs-directory)
  \"File where personal word-frequency statistics are written and read from.\"
  :type 'file
  :group 'daselt-mode)



;;;; Variables
(defvar daselt-abv-valid-prefix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-prefix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
  \"Placevals considered valid for prefix placement.
Each entry is of the form (COORD . STRING-BINDING).\")

(defvar daselt-abv-valid-lower-paired-prefix-placevals
  (cl-intersection daselt-abv-valid-prefix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-prefix-classes))
                   :test #'equal)
  \"Subset of valid prefix placevals that are lower/upper paired.\")

(defvar daselt-abv-valid-suffix-placevals
  (cl-remove-if (lambda (cns)
                  (cl-member (cdr cns)
                             (cl-union daselt-abv-special-symbols
                                       daselt-abv-special-non-suffix-symbols)))
                (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
  \"Placevals considered valid for suffix placement.\")

(defvar daselt-abv-valid-lower-paired-suffix-placevals
  (cl-intersection daselt-abv-valid-suffix-placevals
                   (daselt-abv--filter-to-upper-lower-paired-placevals
                    (daselt-abv-placevals-from-classes daselt-abv-suffix-classes))
                   :test #'equal)
  \"Subset of valid suffix placevals that are lower/upper paired.\")




(defvar daselt-abv-temporary-word-constituents-backup
  abv-temporary-word-constituents
  \"Backup of `abv-temporary-word-constituents' prior to our additions.\")

(defvar daselt-abv-prefix-table nil
  \"Hash-table mapping prefix strings to coordinate tuples.\")
(defvar daselt-abv-suffix-table nil
  \"Hash-table mapping suffix strings to coordinate tuples.\")

;;;; Functions
(defun daselt-abv-placevals-from-classes (classes)
  \"Return placevals in the active layout whose syntax class is in CLASSES.

A placeval is a cons of (COORD . STRING-BINDING).  COORD is a coordinate tuple
like (LAYER ROW COL).  CLASSES is a list of syntax class characters as returned
by `char-syntax'.\"
  (let ((print-level nil) (print-length nil))
    (-filter
     (lambda (cons)
       (and (= 1 (length (cdr cons)))
            (let* ((char (string-to-char (cdr cons)))
                   (syx (with-syntax-table org-mode-syntax-table (char-syntax char))))
              (cl-member syx classes :test #'=))))
     (daselt-base-flatten-n-times
      (cdr (daselt-coords-coordinatize-layout
            (symbol-value daselt-dfk-layout))) 2))))

(defun daselt-abv--filter-to-lower-layers (placevals)
  \"Filter PLACEVALS to those residing on lower layers from `daselt-abv-layer-pairs'.\"
  (-filter (lambda (cns)
             (cl-member (caar cns) (mapcar #'car daselt-abv-layer-pairs)))
           placevals))

(defun daselt-abv--filter-to-upper-lower-paired-placevals (placevals)
  \"Keep PLACEVALS whose upper-layer counterpart is a single symbol as well.

This prefers keys that have a clean upper-case pair on their partner layer.\"
  (-filter (lambda (cns)
             (let* ((coords (car cns))
                    (uppercoords (list (1+ (alist-get (car coords)
                                                      daselt-abv-layer-pairs))
                                       (nth 1 coords)
                                       (nth 2 coords))))
               (not (< 1 (length (daselt-coords-binding uppercoords))))))
           (daselt-abv--filter-to-lower-layers placevals)))

(defun daselt-abv-add-to-temporary-word-constituents ()
  \"Add characters from `daselt-xkb-layout' to `abv-temporary-word-constituents'.

Adds characters whose syntax class is in `daselt-abv-added-syntax-classes'.
Excludes symbols from `daselt-abv-non-constituents'.\"
  (declare (ftype (function () t)))
  (setopt abv-temporary-word-constituents
          (cl-union daselt-abv-temporary-word-constituents-backup
                    (let ((laylist (flatten-list (symbol-value daselt-xkb-layout))))
                      (remq
                       nil (mapcar
                            (lambda (entry)
                              (if (= 1 (length entry))
                                  (let ((char (string-to-char entry)))
                                    (if (and (or (memq (char-syntax char)
                                                       daselt-abv-added-syntax-classes))
                                             (not (cl-member
                                                   char
                                                   daselt-abv-non-constituents
                                                   :test #'=)))
                                        char))))
                            laylist)))))
  (abv--generate-syntax-table))

;;;;; Personal statistics

(defvar daselt-abv-statistics-obarray (make-vector 4096 0)
  \"Obarray holding personal word-frequency statistics.

Symbols are interned for words; their symbol-values hold the counts.\")

(defun daselt-abv--count-words-into (file table)
  \"Count words in FILE and accumulate into hash TABLE.

Words are sequences that match the regexp \"\\\\(\\\\sw\\\\|\\\\s_\\\\)+\". Counting is
case-insensitive. Files that cannot be decoded or read are skipped.\"
  (when (and (stringp file) (file-readable-p file))
    (condition-case _err
        (with-temp-buffer
          ;; Use automatic decoding; avoid visiting to keep things light-weight.
          (insert-file-contents file nil nil nil t)
          (goto-char (point-min))
          (let ((case-fold-search t))
            (while (re-search-forward \"\\\\(\\\\sw\\\\|\\\\s_\\\\)+\" nil t)
              (let* ((w (downcase (match-string-no-properties 0)))
                     (old (gethash w table 0)))
                (puthash w (1+ old) table)))))
      (error nil))))

(defun daselt-abv-read-statistics ()
  \"Read `daselt-abv-statistics-file' into an obarray.

Returns the newly created obarray stored in
`daselt-abv-statistics-obarray'.\"
  (interactive)
  (setq daselt-abv-statistics-obarray (make-vector 4096 0))
  (when (file-readable-p daselt-abv-statistics-file)
    (with-temp-buffer
      (insert-file-contents daselt-abv-statistics-file)
      (goto-char (point-min))
      (condition-case _err
          (let ((data (read (current-buffer))))
            (dolist (pair data)
              (when (and (consp pair)
                         (stringp (car pair))
                         (numberp (cdr pair)))
                (let ((sym (intern (car pair) daselt-abv-statistics-obarray)))
                  (set sym (cdr pair))))))
        (error nil))))
  daselt-abv-statistics-obarray)

;;;;; Assignment helpers
(defun daselt-abv--alist->hash (alist &optional key-fn)
  \"Return a hash table from ALIST; transform keys via KEY-FN if non-nil.\"
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (kv alist ht)
      (let ((k (if key-fn (funcall key-fn (car kv)) (car kv)))
            (v (cdr kv)))
        (puthash k v ht)))))

(defun daselt-abv--placevals->coord-hash (placevals)
  \"Return hash mapping coord -> binding for PLACEVALS.
A placeval has the shape ((LAYER ROW COL) . STRING-BINDING).\"
  (let ((ht (make-hash-table :test 'equal)))
    (dolist (pv placevals ht)
      (puthash (car pv) (cdr pv) ht))))

(defun daselt-abv--keys (hash)
  \"Return a list of keys of HASH.\"
  (let (ks)
    (maphash (lambda (k _v) (push k ks)) hash)
    ks))

(defun daselt-abv--letter-p (s)
  \"Return non-nil if S is a single ASCII alphabetic letter (A-Z or a-z).\"
  (and (stringp s) (= (length s) 1)
       (let ((c (aref s 0)))
         (or (and (<= ?a c) (<= c ?z))
             (and (<= ?A c) (<= c ?Z))))))

(defun daselt-abv--down (s)
  \"Return lowercase of single-letter string S; identity otherwise.\"
  (if (and (stringp s) (= (length s) 1))
      (downcase s)
    s))

(defun daselt-abv--erg-score (coord)
  \"Return ergonomic score for COORD; lower is better.
Heuristic per main_layout_heuristics.org.\"
  (pcase-let ((`(,L ,row ,col) coord))
    (let* ((abscol (abs col))
           (row-pen (cond ((= row 0) 0.0)
                          (t 0.6))) ;; top/bottom small penalty
           (col-pen (pcase abscol
                      (1 1.0) (2 1.2) (3 1.5) (4 2.0) (5 3.0) (6 4.0)
                      (_ 5.0)))
           (layer-pen (pcase L
                        (1 0.0)
                        (2 0.2) (4 0.2)
                        (6 0.4) (5 0.4)
                        (7 0.6) (3 0.6)
                        (8 0.8)
                        (_ 1.0))))
      (+ row-pen col-pen layer-pen))))

(defun daselt-abv--coord-layers-in-order (complementp)
  \"Return order of layers by ease; if COMPLEMENTP, prefer 2 before 1.\"
  (let ((base '(1 2 4 6 5 7 3 8)))
    (if complementp
        '(2 1 4 6 5 7 3 8)
      base)))

(defun daselt-abv--neighbors (r c layers)
  \"Produce coords around (R,C) across LAYERS in the guide's order.\"
  (let ((coords '()))
    ;; same row/col across layers
    (dolist (L layers) (push (list L r c) coords))
    ;; adjacent rows then across layers
    (dolist (dr '(-1 1))
      (dolist (L layers) (push (list L (+ r dr) c) coords)))
    ;; column-adjacent then across layers
    (dolist (dc '(-1 1))
      (dolist (L layers) (push (list L r (+ c dc)) coords)))
    (nreverse coords)))

(defun daselt-abv--collect-letter-bases (placevals)
  \"Map lowercase letter -> one base coord, prefer layer 1 if present.
PLACEVALS is a list of (COORD . BINDING).\"
  (let ((per-letter (make-hash-table :test 'equal)))
    ;; collect all coords by letter
    (dolist (pv placevals)
      (let* ((coord (car pv)) (binding (daselt-abv--down (cdr pv))))
        (when (daselt-abv--letter-p binding)
          (push coord (gethash binding per-letter)))))
    ;; pick best coord per letter, prefer layer 1, else minimal layer/erg score
    (let ((out (make-hash-table :test 'equal)))
      (maphash
       (lambda (letter coords)
         (let* ((on-l1 (cl-find-if (lambda (cc) (= (car cc) 1)) coords))
                (best (or on-l1
                          (car (cl-sort (copy-sequence coords)
                                        (lambda (a b)
                                          (< (daselt-abv--erg-score a)
                                             (daselt-abv--erg-score b))))))))
           (puthash letter best out)))
       per-letter)
      out)))

(defun daselt-abv--affix-letters (affix)
  \"Return list of lowercase unique letters of AFFIX preserving order.\"
  (let ((seen (make-hash-table :test 'equal))
        (out '()))
    (dotimes (i (length affix))
      (let* ((ch (aref affix i))
             (s (downcase (string ch))))
        (when (and (daselt-abv--letter-p s) (not (gethash s seen)))
          (puthash s t seen)
          (push s out))))
    (nreverse out)))

(defun daselt-abv--letter-rarity (letter preferred-hash)
  \"Return how many places in PREFERRED-HASH bind to LETTER (lowercase).\"
  (let ((n 0))
    (maphash
     (lambda (_coord binding)
       (when (and (stringp binding) (string= (daselt-abv--down binding) letter))
         (setq n (1+ n))))
     preferred-hash)
    n))

(defun daselt-abv--free-candidates (preferred-hash counts-hash)
  \"Return coords still unfilled (assignment count 0) in PREFERRED-HASH.\"
  (cl-loop for coord in (daselt-abv--keys preferred-hash)
           unless (> (gethash coord counts-hash 0) 0)
           collect coord))

(defun daselt-abv--choose-fallback (preferred-hash counts-hash)
  \"Choose coord by minimal assignment count, then ergonomic score.\"
  (let ((coords (daselt-abv--keys preferred-hash)))
    (car (cl-sort (copy-sequence coords)
                  (lambda (a b)
                    (let* ((ca (gethash a counts-hash 0))
                           (cb (gethash b counts-hash 0)))
                      (if (/= ca cb)
                          (< ca cb)
                        (< (daselt-abv--erg-score a)
                           (daselt-abv--erg-score b)))))))))

(defun daselt-abv--assign-one (affix preferred-hash counts-hash letter-base complementp)
  \"Choose a coordinate for AFFIX and update COUNTS-HASH.

PREFERRED-HASH maps coordinates to their single-character binding.  COUNTS-HASH
maps coordinates to times used; it is incremented for the chosen coordinate to
balance load.  LETTER-BASE maps letters to preferred base coordinates.  If
COMPLEMENTP is non-nil, prefer a complementary layer order.  Return the chosen
coordinate tuple.\"
  (let* ((letters (daselt-abv--affix-letters affix))
         ;; sort letters by rarity (fewer direct matches in preferred = earlier)
         (letters-sorted
          (cl-stable-sort (copy-sequence letters) #'<
                          :key (lambda (L) (daselt-abv--letter-rarity L preferred-hash))))
         (layers (daselt-abv--coord-layers-in-order complementp))
         (chosen-coord nil))
    ;; letter-guided search
    (cl-loop for L in letters-sorted
             for base = (gethash L letter-base)
             when base do
             (pcase-let ((`(,_ ,r ,c) base))
               (let ((cands (daselt-abv--neighbors r c layers)))
                 (cl-loop for coord in cands
                          for binding = (gethash coord preferred-hash)
                          when (and binding (<= (gethash coord counts-hash 0) 0))
                          do (setq chosen-coord coord)
                          (cl-return))))
             when chosen-coord do (cl-return))
    ;; fallback if none free via letter-guided
    (unless chosen-coord
      (let ((free (daselt-abv--free-candidates preferred-hash counts-hash)))
        (setq chosen-coord
              (if free
                  (car (cl-sort free #'<
                                :key (lambda (cc) (daselt-abv--erg-score cc))))
                ;; multi-fill: everything taken once; pick min-count then ergo
                (daselt-abv--choose-fallback preferred-hash counts-hash)))))
    ;; book-keeping and return coord
    (cl-incf (gethash chosen-coord counts-hash 0))
    chosen-coord))

(defun daselt-abv--complement-hash (universe-hash subset-hash)
  \"Return a hash of coords present in UNIVERSE-HASH but not in SUBSET-HASH.\"
  (let ((ht (make-hash-table :test 'equal)))
    (maphash
     (lambda (coord binding)
       (unless (gethash coord subset-hash)
         (puthash coord binding ht)))
     universe-hash)
    ht))

(defun daselt-abv--sorted-affixes-by-frequency (affixes freq-alist)
  \"Return AFFIXES sorted by descending frequency using FREQ-ALIST; default 0.
Ties are broken by shorter length first, then lexicographically.\"
  (let* ((freq (daselt-abv--alist->hash freq-alist))
         (pair (mapcar (lambda (a) (cons a (or (gethash a freq) 0))) affixes)))
    (mapcar #'car
            (cl-sort pair (lambda (a b)
                            (let ((fa (cdr a)) (fb (cdr b)))
                              (if (/= fa fb) (> fa fb)
                                ;; tie-break: shorter first, then lex
                                (let ((la (length (car a)))
                                      (lb (length (car b))))
                                  (if (/= la lb) (< la lb)
                                    (string< (car a) (car b)))))))))))

(defun daselt-abv--affix-kind-for-table (sym)
  \"Heuristically determine affix kind for table symbol SYM.
Return one of the symbols 'prefixes, 'suffixes, 'infixes, 'roots or nil.\"
  (let* ((name (symbol-name sym))
         (base (and (string-match \"^daselt-abv-\\(.*?\\)-table$\" name)
                    (match-string 1 name))))
    (cond
     ((and base (string-match-p \"^prefix\" base)) 'prefixes)
     ((and base (string-match-p \"^suffix\" base)) 'suffixes)
     ((and base (string-match-p \"^infix\" base)) 'infixes)
     ((and base (string-match-p \"^root\" base)) 'roots)
     (t nil))))

(defun daselt-abv--option-var-for-table (sym)
  \"Return the abv option variable symbol corresponding to table SYM.
Supported mappings are: prefixes, suffixes, infixes, roots.\"
  (pcase (daselt-abv--affix-kind-for-table sym)
    ('prefixes 'abv-prefixes)
    ('suffixes 'abv-suffixes)
    ('infixes  'abv-infixes)
    ('roots    'abv-roots)
    (_ nil)))

(defun daselt-abv--ensure-hash-table (sym)
  \"Ensure SYM is bound to a hash-table and return it.
If SYM is unbound or not a hash-table, bind it to a fresh hash-table.\"
  (unless (and (boundp sym) (hash-table-p (symbol-value sym)))
    (set sym (make-hash-table :test #'equal)))
  (symbol-value sym))

(defun daselt-abv--normalize-frequencies (freqs)
  \"Normalize FREQS into an alist of (AFFIX . COUNT).

Accepts the following shapes:
- alist of (string . integer)
- alist of ((string) . integer) as from `abv-corpus-reduced-monogram-frequencies'
- alist of (symbol . integer)
Return a list of conses with string keys. Entries with non-positive counts are
removed.\"
  (cl-loop for (k . v) in freqs
           for key = (cond
                      ((stringp k) k)
                      ((and (consp k) (stringp (car k))) (car k))
                      ((symbolp k) (symbol-name k))
                      (t nil))
           when (and key (integerp v) (> v 0))
           collect (cons key v)))

;;;###autoload
(defun daselt-abv-assign-affixes (affixes frequencies table-sym)
  \"Assign AFFIXES to Daselt coordinates by FREQUENCIES into TABLE-SYM.

AFFIXES is a list of affix strings (e.g., `abv-prefixes').  FREQUENCIES is an
alist mapping affix to count, or a list of ((affix) . count) pairs as returned
by `abv-corpus-reduced-monogram-frequencies'.  TABLE-SYM is the symbol of the
hash-table to populate (e.g., 'daselt-abv-prefix-table).

If TABLE-SYM is already bound to a hash-table, extend it without overwriting
existing entries.  The value for each key is the coordinate tuple assigned to
that affix.  Coordinates are chosen from a preferred set first (paired lower
layers), then from its complement.  Affixes are processed in descending
frequency order.

This function assigns by coordinates, not by the symbols they house.\"
  (interactive
   (let* ((table (intern (completing-read \"Affix table symbol: \" obarray
                                          (lambda (s)
                                            (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s)))
                                          nil nil nil \"daselt-abv-prefix-table\")))
          (opt (or (daselt-abv--option-var-for-table table) 'abv-prefixes))
          (affs (symbol-value opt))
          (freqs (mapcar (lambda (a) (cons a 1)) affs)))
     (list affs freqs table)))
  (unless (and (listp affixes) (listp frequencies) (symbolp table-sym))
    (user-error \"Invalid inputs: expected (list list symbol)\"))
  (let* ((freqs (daselt-abv--normalize-frequencies frequencies))
         (table (daselt-abv--ensure-hash-table table-sym))
         ;; mark used coords from existing entries
         (used-coords (make-hash-table :test #'equal))
         (counts (make-hash-table :test #'equal)))
    (maphash (lambda (_a c)
               (puthash c t used-coords)
               (cl-incf (gethash c counts 0)))
             table)
    ;; Build placeval universe according to affix kind
    (pcase (daselt-abv--affix-kind-for-table table-sym)
      ('prefixes
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
              (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-prefix-placevals))
              (compl-h (daselt-abv--complement-hash univ-h paired-h))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             ;; try paired first
             (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
               (when (or (null coord) (gethash coord used-coords))
                 (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
               (when coord
                 (puthash coord t used-coords)
                 (puthash a coord table))))))
      ('suffixes
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-suffix-placevals))
              (paired-h (daselt-abv--placevals->coord-hash daselt-abv-valid-lower-paired-suffix-placevals))
              (compl-h (daselt-abv--complement-hash univ-h paired-h))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             (let* ((coord (daselt-abv--assign-one a paired-h counts letter-base nil)))
               (when (or (null coord) (gethash coord used-coords))
                 (setq coord (daselt-abv--assign-one a compl-h counts letter-base t)))
               (when coord
                 (puthash coord t used-coords)
                 (puthash a coord table))))))
      (_
       ;; generic: use prefix-classes as a reasonable default universe
       (let* ((univ-h (daselt-abv--placevals->coord-hash daselt-abv-valid-prefix-placevals))
              (sorted (daselt-abv--sorted-affixes-by-frequency affixes freqs))
              (letter-base (daselt-abv--collect-letter-bases
                            (append daselt-abv-valid-prefix-placevals
                                    daselt-abv-valid-suffix-placevals))))
         (dolist (a sorted)
           (unless (gethash a table)
             (let ((coord (daselt-abv--assign-one a univ-h counts letter-base nil)))
               (when (and coord (not (gethash coord used-coords)))
                 (puthash coord t used-coords)
                 (puthash a coord table))))))))
    table))

(defun daselt-abv--table->bindlist-var (table-sym)
  \"Return the symbol to use for the bindlist variable from TABLE-SYM.\"
  (intern (replace-regexp-in-string \"-table$\" \"-bindlist\" (symbol-name table-sym))))

(defun daselt-abv-table->bindlist (table-sym)
  \"Convert TABLE-SYM (an affix table) into a bindlist and define a variable.

The variable name is derived by replacing the trailing \"-table\" with
\"-bindlist\" in TABLE-SYM.  Each binding in the returned bindlist has the form
(COORDS . LABELSYM), where COORDS is the coordinate tuple from the table and
LABELSYM is an interned symbol mirroring the affix string (e.g., \\=\"pre\\=\" ->
\\='abv-pre).  The variable is created or replaced using `set'.  Return the
bindlist.\"
  (interactive
   (list (intern (completing-read \"Affix table: \" obarray
                                 (lambda (s)
                                   (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                                        (hash-table-p (symbol-value s))))
                                 t))))
  (unless (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
    (user-error \"Table %s is not bound to a hash-table\" table-sym))
  (let* ((tbl (symbol-value table-sym))
         (blist nil))
    (maphash (lambda (k v)
               (let* ((aff (if (stringp k) k (format \"%s\" k)))
                      (sym (intern (concat \"abv-\" aff)))
                      (coords v))
                 (push (cons coords sym) blist)))
             tbl)
    (setq blist (nreverse blist))
    (set (daselt-abv--table->bindlist-var table-sym) blist)
    blist))

;;; Visualization
;;;###autoload
(defun daselt-abv-draw-table-layer (table-sym laycoord &rest mods)
  \"Visualize one layer of TABLE-SYM by delegating to `daselt-bind-draw-bindlist-layer'.

Ensure a bindlist exists for TABLE-SYM by converting if necessary, then draw
layer LAYCOORD with optional MODS.\"
  (interactive
   (let* ((table (intern (completing-read \"Affix table symbol: \" obarray
                                         (lambda (s)
                                           (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                                                (hash-table-p (symbol-value s))))
                                         t)))
          (lay (read (read-from-minibuffer \"Layer coordinate (e.g., (0)): \"))))
     (list table lay)))
  (when (and (symbolp table-sym) (boundp table-sym) (hash-table-p (symbol-value table-sym)))
    ;; ensure bindlist
    (unless (and (boundp (daselt-abv--table->bindlist-var table-sym))
                 (daselt-bind-bindlist-p (symbol-value (daselt-abv--table->bindlist-var table-sym))))
      (daselt-abv-table->bindlist table-sym))
    (apply #'daselt-bind-draw-bindlist-layer (daselt-abv--table->bindlist-var table-sym) laycoord mods)))

(defun daselt-abv--table-symbols ()
  \"Return a list of symbols bound to affix tables named ^daselt-abv-.*-table$.\"
  (let (out)
    (mapatoms (lambda (s)
                (when (and (string-match-p \"^daselt-abv-.*-table$\" (symbol-name s))
                           (boundp s) (hash-table-p (symbol-value s)))
                  (push s out))))
    (nreverse out)))

;;; Query and save
;;;###autoload
(defun daselt-abv-look-up ()
  \"Look up an affix in a chosen table and maybe add it to the defining option.

This command prompts for a table among variables named
\\=\"daselt-abv-.*-table\\=\", then for an affix string.  If found, its
coordinates are displayed.  If no result is found, the user is asked whether
to add the query string to the variable that defined the table (e.g.,
`abv-prefixes', `abv-suffixes', `abv-infixes' or `abv-roots').  The new option
value is saved using `customize-push-and-save' when available.\"
  (interactive)
  (let* ((table (intern (completing-read \"Affix table: \" (daselt-abv--table-symbols) nil t)))
         (query (read-string (format \"Affix for %s: \" table)))
         (tbl (symbol-value table))
         (hit (and (hash-table-p tbl) (gethash query tbl))))
    (if hit
        (message \"%s => %S\" query hit)
      (let ((opt (daselt-abv--option-var-for-table table)))
        (if (not (and opt (boundp opt)))
            (message \"No matching option variable for %s\" table)
          (when (yes-or-no-p (format \"Add %S to %S and save? \" query opt))
            (if (fboundp 'customize-push-and-save)
                (customize-push-and-save opt query)
              (customize-set-variable opt (append (symbol-value opt) (list query)))
              (customize-save-customized))
            (message \"Added %S to %S\" query opt)))))))

;;;###autoload
(defun daselt-abv-save (&optional file)
  \"Save all tables named \\=\"daselt-abv-.*-table\\=\" into FILE.

FILE defaults to `daselt-abv-save-file'.  This overwrites existing contents.
Tables are serialized using `abv-corpus-print-table'.\"
  (interactive)
  (let ((outfile (or file daselt-abv-save-file)))
    (let ((dir (file-name-directory outfile)))
      (when dir (ignore-errors (make-directory dir t))))
    (with-temp-file outfile
      (insert \";;; Saved daselt-abv tables\\n\\n\")
      (dolist (s (daselt-abv--table-symbols))
        (insert (format \";;; %s\\n\" s))
        (abv-corpus-print-table s)
        (insert \"\\n\\n\")))
    (message \"Saved %d tables to %s\" (length (daselt-abv--table-symbols)) outfile)))


;;;; Legacy batch assignment (deprecated)
;; Keeping for reference; new `daselt-abv-assign-affixes' is table-oriented.
(defun daselt-abv-assign-prefixes-and-suffixes ()
  \"Compute and set `daselt-abv-prefix-table' and `daselt-abv-suffix-table'.
Deprecated: prefer calling `daselt-abv-assign-affixes' for each table.\"
  (interactive)
  (daselt-abv-assign-affixes abv-prefixes (mapcar (lambda (a) (cons a 1)) abv-prefixes) 'daselt-abv-prefix-table)
  (daselt-abv-assign-affixes abv-suffixes (mapcar (lambda (a) (cons a 1)) abv-suffixes) 'daselt-abv-suffix-table))

;;;; Hooks
(add-hook 'abv-mode-hook
          #'daselt-abv-add-to-temporary-word-constituents
          89)

(remove-hook 'daselt-mode-hook
             (lambda ()
               (unless daselt-mode
                 (remove-hook 'abv-mode-hook
                              #'daselt-abv-add-to-temporary-word-constituents))))

(provide 'daselt-abv)
;;; daselt-abv.del ends here
"))

nil
#+end_tool
- Review

- Summary
  - Implemented requested changes across abv-data.el, abv-corpus.el, and daselt-abv.del:
    - Converted global affix variables in abv-data.el to options with full documentation and added abv-infixes.
    - Added/expanded docstrings throughout abv-corpus.el and daselt-abv.del.
    - Reworked daselt-abv-assign-affixes into a reusable function that assigns affixes to coordinates by frequency, without overwriting existing table entries, and uses coordinate tuples as values.
    - Added table->bindlist conversion, visualization wrapper, interactive lookup with “add to option on miss,” and save command with daselt-abv-save-file option.
- Strengths
  - Modular design: clear separation of corpus helpers, default options, and Daselt-specific assignment/UX.
  - Extensible assignment pipeline: normalization of frequencies, layered coordinate supply, ergonomic proximity and letter-based heuristics.
  - Safe persistence with reproducible defconst forms using abv-corpus-print-table.
  - UX parity with abv-look-up plus “add and save” using customize-push-and-save.
- Issues
  - Depends on dash (-filter) and many daselt-/ utilities; ensure these are available when loading daselt-abv.del.
  - daselt-abv-draw-table-layer assumes bindlist structure; draws via daselt-bind-draw-bindlist-layer; failure modes depend on layout availability.
  - abv-corpus.el and abv-data.el both define defgroup abv; acceptable but slightly redundant.
- Recommendations
  - Consider adding ERT tests for:
    - daselt-abv--normalize-frequencies
    - daselt-abv-assign-affixes (no overwrite, coordinate uniqueness)
    - daselt-abv-table->bindlist
  - Optionally avoid dash by inlining simple filters to reduce deps.
  - Provide a “draw all layers” convenience and a “list missing affixes” report.

- Plan

- Objects/options
  - abv-data.el
    - =abv-roots=, =abv-prefixes=, =abv-suffixes=, =abv-infixes= as defcustoms (global defaults). Docstrings state: language-specific additions should be made on table symbol properties (:roots, :prefixes, :suffixes, :infixes).
  - abv-corpus.el
    - =abv-corpus-file-types=, =abv-corpus-file= as defcustoms.
  - daselt-abv.del
    - =daselt-abv-save-file= as defcustom for persistence.
    - Syntax-class and special symbol options retained with improved docs.

- Files/functions/commands
  - abv-data.el
    - Converts previous setq to defcustom; adds group =abv=; adds =abv-infixes= option.
  - abv-corpus.el
    - =daselt-abv-personal-corpus= to build word-frequency alist.
    - =abv-corpus--optional-letter-regex=
    - =abv-corpus-ngram-frequencies=
    - =abv-corpus-reduced-monogram-frequencies=
    - =daselt-abv-hash->def-form= and =abv-corpus-print-table= for defconst serialization.
    - All with expanded docstrings and sane defaults for =abv-corpus-file=.
  - daselt-abv.del
    - Assignment core
      - =daselt-abv-assign-affixes= (interactive): inputs (affixes list, frequency list, table symbol). Extends table without overwriting; values are coordinate tuples; coordinate choice guided by letter proximity, pairing, and ergo score.
      - Helpers: =daselt-abv--alist->hash=, =daselt-abv--placevals->coord-hash=, =daselt-abv--keys=, =daselt-abv--letter-p=, =daselt-abv--down=, =daselt-abv--erg-score=, =daselt-abv--coord-layers-in-order=, =daselt-abv--neighbors=, =daselt-abv--collect-letter-bases=, =daselt-abv--affix-letters=, =daselt-abv--letter-rarity=, =daselt-abv--free-candidates=, =daselt-abv--choose-fallback=, =daselt-abv--complement-hash=, =daselt-abv--sorted-affixes-by-frequency=, =daselt-abv--affix-kind-for-table=, =daselt-abv--option-var-for-table=, =daselt-abv--ensure-hash-table=, =daselt-abv--normalize-frequencies=.
    - Table -> bindlist
      - =daselt-abv-table->bindlist=, derives variable name by replacing “-table” with “-bindlist”; elements are (COORDS . SYMBOL) with SYMBOL like abv-pre.
    - Visualization
      - =daselt-abv-draw-table-layer= ensures bindlist exists and calls =daselt-bind-draw-bindlist-layer=.
    - Query UX
      - =daselt-abv-look-up=: choose a =daselt-abv-.*-table=, query it, and on miss ask to add the string to the defining option (abv-prefixes/suffixes/infixes/roots) and save via =customize-push-and-save=.
    - Save
      - =daselt-abv-save=: writes all =daselt-abv-.*-table= as defconst forms to =daselt-abv-save-file= using =abv-corpus-print-table=.

- Internal structure
  - Assignment respects:
    - existing table entries (no overwrite)
    - layer pairing preference (paired “lower” first, then complement)
    - letter-neighborhood and minimal ergonomic score
  - Bindlist variable naming convention: =^daselt-abv-.*-bindlist$= derived from table symbol.

- Code changes

- abv-data.el
  - Converted globals to options and added abv-infixes. Docstrings clarify “global defaults” and per-language via :roots, :prefixes, :suffixes, :infixes on table symbols.

- abv-corpus.el
  - Added docstrings and safe defaults. Rewrote/expanded function docs and improved interactive behavior for abv-corpus-print-table.

- daselt-abv.del
  - Added requires, options, docstrings and the following new or reworked entry points:
    - daselt-abv-assign-affixes
    - daselt-abv-table->bindlist
    - daselt-abv-draw-table-layer
    - daselt-abv-look-up
    - daselt-abv-save

Note: I kept a small “legacy” helper (daselt-abv-assign-prefixes-and-suffixes) which now delegates to the new function for convenience.

- Next steps / questions

- Is depending on dash acceptable for your build? If not, I can remove it by inlining simple filters.
- Should I add a “draw all layers” convenience? If yes, how many layers and which modifier sets do you want iterated?
- Do you want ERT tests committed now (and under which filename, e.g., daselt-abv-tests.el)?
- Do you want additional table kinds (roots/infixes) pre-registered (variables =daselt-abv-root-table=, =daselt-abv-infix-table=), or do you prefer the user to create/assign on demand?

Usage tips
- Assign for prefixes: M-x daselt-abv-assign-affixes RET (defaults provided in prompt) and choose table ‘daselt-abv-prefix-table’.
- Generate bindlist for a table: M-x daselt-abv-table->bindlist.
- Visualize a layer: M-x daselt-abv-draw-table-layer.
- Query with add-on-miss: M-x daselt-abv-look-up.
- Save all tables: M-x daselt-abv-save (writes to daselt-abv-save-file).

*** 
