ekp/ekp_c
Kinneyzhang 64eb2f38d5 refactor!: per-gap break permissions — punctuation as first-class boxes
Kinsoku moves from merge-based box attachment to a general break-
permission layer, the foundation for no-break spans, NBSP, verbatim
atoms and punctuation protrusion:

- tokenizer: every CJK char (punctuation included) is its own box;
  the hold/attach machinery is gone.  Fixes two latent bugs: a second
  consecutive closer (字。」) could start a line, and an opener held
  across spaces reordered box content vs the original string
- ekp--str-type: cjk-punct splits into cjk-open (Ps/Pi) / cjk-close
- ekp-para gains breaks-allowed (bool-vector) + forbidden-positions
  (sparse, for C); glue at unbreakable punct gaps is nws, so
  punctuation hugs its content and justification stretch no longer
  opens gaps at 「x or x。
- halfwidth kinsoku: boxes consisting purely of .,;:!?)]}’”»›… may
  not start a line ((&[{‘“«‹ may not end one) — an ASCII comma after
  a CJK char no longer dangles at line start (old behavior violated
  CLREQ)
- DP (elisp 1D, elisp loose 2D, C): forbidden gaps are skipped as
  candidates while the line keeps extending; the emergency fallback
  generalizes from single boxes to atomic runs (no permitted break
  inside), recording real gap counts so multi-box emergency lines
  render correctly
- C module 1.2: ekp-c-break-with-arrays 11→12 args
  (forbidden-positions), batch vectors 12 elements, version gate bumped

Hyphenation improvement: 「Hello / Hello」 previously failed the
word regexp as merged boxes and were never hyphenated; as separate
boxes they hyphenate normally.

Tests: 48 ERT green (split tests migrated to the new box contract;
new unit test for break permissions and a rendered-output kinsoku
sweep across widths); 300-case fuzz 0 failures with C 1.2 parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 21:05:50 +08:00
..
ekp_hyphen.c chore: silence build warnings on non-macOS toolchains 2026-07-26 19:53:39 +08:00
ekp_kp.c refactor!: per-gap break permissions — punctuation as first-class boxes 2026-07-26 21:05:50 +08:00
ekp_module.h refactor!: per-gap break permissions — punctuation as first-class boxes 2026-07-26 21:05:50 +08:00
ekp_paragraph.c refactor: Linus-style code quality improvements across Elisp and C modules 2026-02-10 12:18:00 +00:00
ekp_thread_pool.c refactor: Linus-style code quality improvements across Elisp and C modules 2026-02-10 12:18:00 +00:00
ekp.c refactor!: per-gap break permissions — punctuation as first-class boxes 2026-07-26 21:05:50 +08:00
Makefile Makefile supports multiple plantform 2026-01-26 11:51:19 +08:00
README.md refactor!: overhaul KP core — correctness, C parity, performance, tests, docs 2026-07-26 18:44:02 +08:00

EKP C Dynamic Module

C implementation of the Knuth-Plass DP for emacs-kp (module version 1.1).

The division of labor: Elisp owns all font-dependent data (tokenization, pixel measurement, glue values, prefix sums); the C module runs only the O(n²) dynamic program. This keeps the two engines byte-identical in output while making the hot loop native.

Architecture

ekp_c/
├── ekp_module.h      # Core data structures and API declarations
├── ekp.c             # Emacs module entry point (emacs_module_init)
├── ekp_kp.c          # Knuth-Plass DP + two-pass emergency strategy
├── ekp_thread_pool.c # Thread pool (parallelism across paragraphs)
├── ekp_hyphen.c      # Liang hyphenation (experimental path only)
├── ekp_paragraph.c   # C-side tokenization (experimental path only)
└── Makefile

Parallelism model: the DP for one paragraph is sequential (each position depends on all earlier ones), so the thread pool parallelizes across paragraphs via ekp-c-break-batch — the correct granularity, with zero synchronization in the inner loop.

Building

cd ekp_c
make            # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows)

Requirements: C11 compiler, Emacs 27.1+ headers, pthreads.

make DEBUG=1    # Debug build with sanitizers
make clean
make info

API (as used by ekp.el)

(ekp-c-init)             ; init global state + thread pool
(ekp-c-version)          ; => "1.1" — checked by ekp-c-module-load
(ekp-c-thread-count)     ; => 8
(ekp-c-cleanup)

;; Synced automatically by ekp.el before every call:
(ekp-c-set-penalties LINE HYPHEN FITNESS LAST-RATIO
                     &optional CONSEC-HYPHEN LAST-SHORT)

;; Single paragraph (11 args):
(ekp-c-break-with-arrays IDEAL-PREFIX MIN-PREFIX MAX-PREFIX
                         GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES
                         HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH
                         LEAD-SPACES TRAIL-SPACES)
;; => (BREAKS . TOTAL-COST)

;; Many paragraphs in parallel: vector of 11-element vectors
(ekp-c-break-batch PARAGRAPHS)   ; => vector of (BREAKS . COST)

LEAD-SPACES / TRAIL-SPACES are the space-box run widths that the Elisp renderer strips from line edges; the DP excludes them from line metrics so both layers agree exactly (new in 1.1).

The DP uses the same two-pass strategy as the Elisp engine: a strict Knuth-Plass pass, then — only when the paragraph end is unreachable — a second pass permitting emergency single-box breaks, so overlong unbreakable tokens can never make the result empty. Badness saturates at 10000 exactly like the Elisp side.

Experimental: self-contained C path

ekp-c-break-lines tokenizes and hyphenates in C (ekp_paragraph.c, ekp_hyphen.c) with a measurement callback into Emacs. ekp.el does not use this path; its tokenizer is a simplified approximation of ekp-split-to-boxes. Kept for experimentation.

(ekp-c-load-hyphenator "/path/to/hyph_en_US.dic")   ; => index
(ekp-c-hyphenate 0 "hyphenation")                   ; => (2 5)
(ekp-c-break-lines "text..." 0 600 #'string-pixel-width)

Performance

Measured with tests/ekp-bench.el (batch Emacs 30.2, Apple Silicon, byte-compiled Elisp around the C calls, min of 3 cold-cache runs):

Case Elisp engine (compiled) C engine
justify text-zh.txt w=200 96 ms 57 ms
justify mixed text w=300 53 ms 23 ms
range-justify zh 340380 294 ms 75 ms
range-justify mix 280320 480 ms 34 ms
DP only, text-zh w=400 15 ms 1.3 ms

The pure-DP speedup is ~12× (1.3 ms vs 15 ms); end-to-end gains are smaller because tokenization, measurement and rendering stay in Elisp. The C engine matters most for range-justify (many widths per text) and multi-paragraph batches.