ekp/ekp_c/README.md
Kinneyzhang 112b3a0e52 refactor!: overhaul KP core — correctness, C parity, performance, tests, docs
- fix: ekp-param-set silently reset after first justify (now persists;
  ekp-param-reset added)
- fix: narrow-width CJK returned empty string (data loss); two-pass
  emergency-break strategy guarantees output for any input
- fix: K-P penalties never synced to C module; space-box metrics
  divergence between C and Elisp engines
- fix: para cache ignored ekp-latin-lang (stale hyphenation after
  language switch) and used collision-prone sxhash keys
- fix: fullwidth letters/digits misclassified as CJK punctuation
- fix: combining chars split from their base char in the tokenizer
- fix: punctuation-wrapped words (word!/(word)/word;) never hyphenated
- fix: renderer double-counted stripped space widths; negative glue
  clamped; batch/tty font detection no longer crashes
- feat: real looseness support via (position × line-count) DP
- perf: O(1) line metrics and gap counts via prefix arrays (inner loop
  previously allocated O(n) subsequences → O(n³) total); box measurement
  dedupe; eq fast-path para lookup; prebuilt per-para glue arrays
  → zh justify 7547ms → 96ms (compiled elisp) / 57ms (C);
    range-justify 68.5s → 0.48s / 34ms; C module itself 3–19× faster
- test: 36 batch-safe ERT tests + 300-case property fuzz (C/elisp
  byte-identical output, zero content loss) replacing ad-hoc suite
- docs: readme/readme_zh/DEVELOPER/DEVELOPER_ZH/ekp_c-README rewritten
  to match the implementation; phase handoff in .phrase/phases/

BREAKING: requires Emacs 29.1+; C module must be rebuilt (v1.1, new
arities); ekp-threshold-factor / ekp-flagged-penalty /
ekp-forced-break-penalty removed; Rust module stubs removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 18:44:02 +08:00

107 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```bash
cd ekp_c
make # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows)
```
Requirements: C11 compiler, Emacs 27.1+ headers, pthreads.
```bash
make DEBUG=1 # Debug build with sanitizers
make clean
make info
```
## API (as used by ekp.el)
```elisp
(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.
```elisp
(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.