refactor!: remove the experimental C tokenization path; harden the module

The self-contained C path (ekp-c-break-lines, ekp-c-load-hyphenator,
ekp-c-hyphenate, ekp-c-set-spacing, ekp_paragraph.c, ekp_hyphen.c,
~1500 lines) was never used by ekp.el, diverged semantically from the
real pipeline (no kinsoku, no protrusion, no two-pass emergency), and
contained an exploitable heap overflow reachable from Lisp:
ekp_para_create sized its box array as box_count * 2, but a long word
hyphenates into arbitrarily many syllable boxes, overflowing the
calloc'd buffer.  Deleting the path deletes the bug class.

Hardening of the live path:

- thread pool: sized from the machine's core count instead of a
  hardcoded 8; created lazily on the first multi-paragraph batch
  (single-paragraph users never start worker threads); a full queue
  now blocks the submitter until a worker makes room — tasks were
  silently dropped before, degrading the batch to the Elisp fallback
  exactly when parallelism mattered most.
- unified failure gate: a partial allocation used to silently drop
  kinsoku, hyphenation or protrusion data and continue with a subtly
  different layout; any allocation failure or pending Lisp signal
  (non-local exit from a bad element type) now fails the whole call,
  and ekp.el falls back to the Elisp engine.  The Elisp bridge wraps
  both C entry points in condition-case, and a whole-batch nil no
  longer crashes the per-paragraph loop.
- integer safety: every extracted pixel value is clamped to int32
  instead of silently wrapping.
- EKP_INFINITY (the unreachable-state sentinel) is now a real
  infinity: extremely degenerate paragraphs could legitimately
  accumulate demerits past the old 1e10 constant, making C consider
  reachable states dead and diverge from the Elisp engine.

BREAKING: the four experimental module functions are gone; rebuild
with make -C ekp_c clean all (version gate unchanged at 1.5).

92 ERT green; fuzz 300/300 byte-identical across engines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kinneyzhang 2026-07-27 01:49:25 +08:00
parent 3fc05c1405
commit 74a780ce95
10 changed files with 174 additions and 1484 deletions

View File

@ -339,14 +339,10 @@ after CALLBACK returns."
(declare-function ekp-c-init "ext:ekp")
(declare-function ekp-c-version "ext:ekp")
(declare-function ekp-c-thread-count "ext:ekp")
(declare-function ekp-c-load-hyphenator "ext:ekp")
(defvar ekp-c-module-loaded nil
"Non-nil if C module is loaded.")
(defvar ekp-c-hyphenator-index nil
"Index of the loaded hyphenator in C module.")
(defun ekp-c-module-dir ()
"Return the C module directory."
(when-let ((root-dir (ekp-root-dir)))
@ -389,20 +385,6 @@ Run 'make' in ekp_c/ to rebuild; falling back to Elisp."
(ekp-c-version) (ekp-c-thread-count)))))
(message "C module not found. Run 'make' in ekp_c/ directory."))))
(defun ekp-c-load-dictionary (lang)
"Load hyphenation dictionary for LANG into C module."
(when ekp-c-module-loaded
(let* ((root-dir (ekp-root-dir))
(dict-file (expand-file-name
(format "dictionaries/hyph_%s.dic" lang)
root-dir)))
(when (file-exists-p dict-file)
(setq ekp-c-hyphenator-index
(ekp-c-load-hyphenator dict-file))
(when ekp-c-hyphenator-index
(message "Loaded hyphenator for %s (index %d)"
lang ekp-c-hyphenator-index))))))
;;;###autoload
(defun ekp-c-module-build ()
"Build the C module using make."

16
ekp.el
View File

@ -1680,7 +1680,8 @@ the reconstructed rests overfill the indented line."
The C module receives all font-dependent data from Elisp; it only
runs the pure DP. Falls back to Elisp when the C call fails."
(ekp--c-sync-params)
(let* ((result (ekp-c-break-with-arrays
(let* ((result (condition-case nil
(ekp-c-break-with-arrays
(ekp-para-ideal-prefixs para)
(ekp-para-min-prefixs para)
(ekp-para-max-prefixs para)
@ -1695,7 +1696,10 @@ runs the pure DP. Falls back to Elisp when the C call fails."
(ekp-para-forbidden-positions para)
(ekp-para-tail-protrudes para)
(ekp-para-hyphen-protrude para)
(cdr (ekp--line-spec para 0 line-pixel))))
(cdr (ekp--line-spec para 0 line-pixel)))
;; A module-level signal must not escape: the
;; Elisp engine is the fallback for any C failure.
(error nil)))
(c-breaks (car result))
(c-cost (cdr result)))
(if (null c-breaks)
@ -1723,12 +1727,16 @@ Only computes strings that aren't already cached."
(mapcar (lambda (ip)
(ekp--prepare-para-for-c (cdr ip) line-pixel))
needs-compute)))
(batch-results (ekp-c-break-batch batch-input)))
;; nil (whole-batch failure or a signal) falls back to
;; the Elisp engine per paragraph below.
(batch-results (condition-case nil
(ekp-c-break-batch batch-input)
(error nil))))
(cl-loop for ip in needs-compute
for j from 0
for idx = (car ip)
for para = (cdr ip)
for res = (aref batch-results j)
for res = (and batch-results (aref batch-results j))
for breaks = (car res)
for cost = (cdr res)
do (aset results idx

View File

@ -67,7 +67,7 @@ else
endif
# Source files
SRCS := ekp.c ekp_kp.c ekp_hyphen.c ekp_paragraph.c ekp_thread_pool.c
SRCS := ekp.c ekp_kp.c ekp_thread_pool.c
OBJS := $(SRCS:.c=.o)
# Output

View File

@ -1,6 +1,6 @@
# EKP C Dynamic Module
C implementation of the Knuth-Plass DP for emacs-kp (module version 1.4).
C implementation of the Knuth-Plass DP for emacs-kp (module version 1.5).
The division of labor: **Elisp owns all font-dependent data**
(tokenization, pixel measurement, glue values, prefix sums); the C
@ -15,15 +15,16 @@ ekp_c/
├── 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.
with zero synchronization in the inner loop. The pool is created
lazily on the first multi-paragraph batch, sized to the machine's
core count; a full queue blocks the submitter instead of dropping
tasks.
## Building
@ -32,10 +33,12 @@ cd ekp_c
make # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows)
```
Requirements: C11 compiler, Emacs 27.1+ headers, pthreads.
Requirements: C11 compiler, Emacs module headers, pthreads.
Windows builds need MinGW-w64 (for pthreads) and
`make EMACS_ROOT=<path to your Emacs installation>`.
```bash
make DEBUG=1 # Debug build with sanitizers
make DEBUG=1 # Debug build with ASan/UBSan
make clean
make info
```
@ -43,29 +46,36 @@ make info
## API (as used by ekp.el)
```elisp
(ekp-c-init) ; init global state + thread pool
(ekp-c-version) ; => "1.4" — checked by ekp-c-module-load
(ekp-c-thread-count) ; => 8
(ekp-c-init) ; init global state
(ekp-c-version) ; => "1.5" — checked by ekp-c-module-load
(ekp-c-thread-count) ; worker count (created lazily on first batch)
(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)
&optional CONSEC-HYPHEN LAST-SHORT EXTRA-STRETCH)
;; Single paragraph (11 args):
;; Single paragraph (15 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)
LEAD-SPACES TRAIL-SPACES FORBIDDEN-POS
TAIL-PROTRUDES HYPHEN-PROTRUDE
FIRST-LINE-WIDTH)
;; => (BREAKS . TOTAL-COST)
;; Many paragraphs in parallel: vector of 11-element vectors
;; Many paragraphs in parallel: vector of 15-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 (since 1.1).
metrics so both layers agree exactly (since 1.1). `FORBIDDEN-POS`
carries the kinsoku / no-break gap indices (since 1.2),
`TAIL-PROTRUDES` / `HYPHEN-PROTRUDE` the right-edge protrusion
allowances (since 1.4), and `FIRST-LINE-WIDTH` the width of line 0
for first-line indentation (since 1.5; pass the line width or ≤0
when no indent is active).
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 —
@ -73,19 +83,9 @@ 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)
```
Failure behavior: any allocation failure or bad argument makes the
call return nil, and ekp.el falls back to the Elisp engine — the C
module never silently degrades to a subtly different layout.
## Performance
@ -94,13 +94,14 @@ 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 |
| justify text-zh.txt w=200 | 150 ms | 41 ms |
| justify mixed text w=300 | 82 ms | 31 ms |
| range-justify zh 340380 | 529 ms | 106 ms |
| range-justify mix 280320 | 762 ms | 52 ms |
| DP only, text-zh w=400 | 30 ms | 2.5 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.
The pure-DP speedup is ~12×; 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. Absolute numbers vary with the machine and
power state; regenerate them with the two commands in DEVELOPER.md §9.

View File

@ -20,6 +20,7 @@
*/
#include "ekp_module.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@ -27,25 +28,13 @@
/* Required for Emacs modules */
int plugin_is_GPL_compatible;
/* Cached Emacs environment for callbacks */
static emacs_env *current_env = NULL;
static emacs_value measure_func = NULL;
/*
* Pixel measurement callback that calls back into Emacs
*/
static int32_t emacs_measure_string(const char *text, size_t len)
/* All pixel quantities travel as int32; clamp instead of silently
* wrapping if Elisp ever hands us something absurd. */
static inline int32_t clamp32(intmax_t v)
{
if (!current_env || !measure_func)
return len * 7; /* fallback: ~7 pixels per char */
emacs_value str = current_env->make_string(current_env, text, len);
emacs_value result = current_env->funcall(current_env, measure_func, 1, &str);
if (current_env->non_local_exit_check(current_env) != emacs_funcall_exit_return)
return len * 7;
return (int32_t)current_env->extract_integer(current_env, result);
if (v > INT32_MAX) return INT32_MAX;
if (v < INT32_MIN) return INT32_MIN;
return (int32_t)v;
}
/*
@ -77,72 +66,6 @@ static emacs_value Fekp_c_cleanup(emacs_env *env, ptrdiff_t nargs,
return env->intern(env, "t");
}
/*
* ekp-c-load-hyphenator: Load hyphenation dictionary
*/
static emacs_value Fekp_c_load_hyphenator(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)nargs; (void)data;
if (!ekp_global) {
emacs_value signal = env->intern(env, "error");
emacs_value msg = env->make_string(env, "ekp-c not initialized", 21);
env->non_local_exit_signal(env, signal, msg);
return env->intern(env, "nil");
}
/* Get dictionary path */
ptrdiff_t size = 0;
env->copy_string_contents(env, args[0], NULL, &size);
char *path = malloc(size);
if (!path)
return env->intern(env, "nil");
env->copy_string_contents(env, args[0], path, &size);
/* Load hyphenator */
ekp_hyphenator_t *h = ekp_hyphen_create(path);
free(path);
if (!h)
return env->intern(env, "nil");
/* Store in global state */
if (ekp_global->hyphenator_count < 32) {
ekp_global->hyphenators[ekp_global->hyphenator_count++] = h;
return env->make_integer(env, ekp_global->hyphenator_count - 1);
}
ekp_hyphen_destroy(h);
return env->intern(env, "nil");
}
/*
* ekp-c-set-spacing: Set spacing parameters
*/
static emacs_value Fekp_c_set_spacing(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)data;
if (!ekp_global || nargs < 9) {
return env->intern(env, "nil");
}
ekp_global->spacing.lws_ideal = env->extract_integer(env, args[0]);
ekp_global->spacing.lws_stretch = env->extract_integer(env, args[1]);
ekp_global->spacing.lws_shrink = env->extract_integer(env, args[2]);
ekp_global->spacing.mws_ideal = env->extract_integer(env, args[3]);
ekp_global->spacing.mws_stretch = env->extract_integer(env, args[4]);
ekp_global->spacing.mws_shrink = env->extract_integer(env, args[5]);
ekp_global->spacing.cws_ideal = env->extract_integer(env, args[6]);
ekp_global->spacing.cws_stretch = env->extract_integer(env, args[7]);
ekp_global->spacing.cws_shrink = env->extract_integer(env, args[8]);
return env->intern(env, "t");
}
/*
* ekp-c-set-penalties: Set K-P parameters
*/
@ -154,144 +77,22 @@ static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs,
if (!ekp_global || nargs < 4)
return env->intern(env, "nil");
ekp_global->line_penalty = env->extract_integer(env, args[0]);
ekp_global->hyphen_penalty = env->extract_integer(env, args[1]);
ekp_global->fitness_penalty = env->extract_integer(env, args[2]);
ekp_global->line_penalty = clamp32(env->extract_integer(env, args[0]));
ekp_global->hyphen_penalty = clamp32(env->extract_integer(env, args[1]));
ekp_global->fitness_penalty = clamp32(env->extract_integer(env, args[2]));
ekp_global->last_line_ratio = env->extract_float(env, args[3]);
if (nargs > 4)
ekp_global->consec_hyphen_penalty = env->extract_integer(env, args[4]);
ekp_global->consec_hyphen_penalty = clamp32(env->extract_integer(env, args[4]));
if (nargs > 5)
ekp_global->last_line_short_penalty = env->extract_float(env, args[5]);
/* Per-line extra stretch for non-justify alignment; reset to 0
* when the caller omits it so stale values never leak. */
ekp_global->extra_stretch =
(nargs > 6) ? (int32_t)env->extract_integer(env, args[6]) : 0;
(nargs > 6) ? (int32_t)clamp32(env->extract_integer(env, args[6])) : 0;
return env->intern(env, "t");
}
/*
* ekp-c-hyphenate: Get hyphenation positions for a word
*/
static emacs_value Fekp_c_hyphenate(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)data;
if (!ekp_global || nargs < 2)
return env->intern(env, "nil");
intmax_t h_idx = env->extract_integer(env, args[0]);
if (h_idx < 0 || (size_t)h_idx >= ekp_global->hyphenator_count)
return env->intern(env, "nil");
ekp_hyphenator_t *h = ekp_global->hyphenators[h_idx];
/* Get word */
ptrdiff_t size = 0;
env->copy_string_contents(env, args[1], NULL, &size);
char *word = malloc(size);
if (!word)
return env->intern(env, "nil");
env->copy_string_contents(env, args[1], word, &size);
/* Hyphenate */
int8_t positions[EKP_MAX_WORD_LEN];
int count = ekp_hyphen_word(h, word, size - 1, positions, EKP_MAX_WORD_LEN);
free(word);
/* Build result list */
emacs_value result = env->intern(env, "nil");
emacs_value cons_sym = env->intern(env, "cons");
for (int i = count - 1; i >= 0; i--) {
emacs_value pos = env->make_integer(env, positions[i]);
emacs_value args2[2] = {pos, result};
result = env->funcall(env, cons_sym, 2, args2);
}
return result;
}
/*
* ekp-c-break-lines: Core line breaking function
*
* Args: (string hyphenator-index line-width measure-func)
* Returns: (breaks . total-cost) where breaks is a list
*/
static emacs_value Fekp_c_break_lines(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)data;
if (!ekp_global || nargs < 4)
return env->intern(env, "nil");
/* Get string */
ptrdiff_t size = 0;
env->copy_string_contents(env, args[0], NULL, &size);
char *text = malloc(size);
if (!text)
return env->intern(env, "nil");
env->copy_string_contents(env, args[0], text, &size);
size_t text_len = size - 1;
/* Get hyphenator */
intmax_t h_idx = env->extract_integer(env, args[1]);
ekp_hyphenator_t *h = NULL;
if (h_idx >= 0 && (size_t)h_idx < ekp_global->hyphenator_count)
h = ekp_global->hyphenators[h_idx];
/* Get line width */
int32_t line_width = env->extract_integer(env, args[2]);
/* Get measure function */
current_env = env;
measure_func = args[3];
/* Create paragraph */
ekp_paragraph_t *para = ekp_para_create(text, text_len, h, emacs_measure_string);
free(text);
if (!para) {
current_env = NULL;
measure_func = NULL;
return env->intern(env, "nil");
}
/* Break lines */
ekp_result_t *result = ekp_break_lines(para, line_width);
current_env = NULL;
measure_func = NULL;
if (!result) {
ekp_para_destroy(para);
return env->intern(env, "nil");
}
/* Build result: ((breaks...) . cost) */
emacs_value breaks_list = env->intern(env, "nil");
emacs_value cons_sym = env->intern(env, "cons");
for (size_t i = result->break_count; i > 0; i--) {
emacs_value brk = env->make_integer(env, result->breaks[i - 1]);
emacs_value args2[2] = {brk, breaks_list};
breaks_list = env->funcall(env, cons_sym, 2, args2);
}
emacs_value cost = env->make_float(env, result->total_cost);
emacs_value args2[2] = {breaks_list, cost};
emacs_value final = env->funcall(env, cons_sym, 2, args2);
ekp_result_destroy(result);
ekp_para_destroy(para);
return final;
}
/*
* ekp-c-version: Return module version
*/
@ -314,7 +115,11 @@ static emacs_value Fekp_c_thread_count(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)nargs; (void)args; (void)data;
return env->make_integer(env, EKP_THREAD_POOL_SIZE);
/* Pool is created lazily; report its actual size once it exists,
* else the size it will get. */
if (ekp_global && ekp_global->pool)
return env->make_integer(env, (intmax_t)ekp_global->pool->thread_count);
return env->make_integer(env, (intmax_t)ekp_pool_default_threads());
}
/*
@ -370,18 +175,18 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
/* Extract prefix arrays */
for (ptrdiff_t i = 0; i < prefix_len; i++) {
ideal_prefix[i] = env->extract_integer(env, env->vec_get(env, args[0], i));
min_prefix[i] = env->extract_integer(env, env->vec_get(env, args[1], i));
max_prefix[i] = env->extract_integer(env, env->vec_get(env, args[2], i));
lead_spaces[i] = env->extract_integer(env, env->vec_get(env, args[9], i));
trail_spaces[i] = env->extract_integer(env, env->vec_get(env, args[10], i));
ideal_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[0], i)));
min_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[1], i)));
max_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[2], i)));
lead_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[9], i)));
trail_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i)));
}
/* Extract glue arrays */
for (size_t i = 0; i < n; i++) {
glue_ideals[i] = env->extract_integer(env, env->vec_get(env, args[3], i));
glue_shrinks[i] = env->extract_integer(env, env->vec_get(env, args[4], i));
glue_stretches[i] = env->extract_integer(env, env->vec_get(env, args[5], i));
glue_ideals[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[3], i)));
glue_shrinks[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[4], i)));
glue_stretches[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i)));
}
/* Get hyphen positions vector */
@ -391,23 +196,23 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
hyph_pos = malloc(hyph_count * sizeof(int32_t));
if (hyph_pos) {
for (ptrdiff_t i = 0; i < hyph_count; i++) {
hyph_pos[i] = env->extract_integer(env, env->vec_get(env, args[6], i));
hyph_pos[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i)));
}
}
}
int32_t hyph_width = env->extract_integer(env, args[7]);
int32_t line_width = env->extract_integer(env, args[8]);
int32_t hyph_width = clamp32(env->extract_integer(env, args[7]));
int32_t line_width = clamp32(env->extract_integer(env, args[8]));
/* Right-edge protrusion: per-gap array (n+1) and hyphen scalar */
int32_t *tail_pro = malloc(prefix_len * sizeof(int32_t));
if (tail_pro) {
for (ptrdiff_t i = 0; i < prefix_len; i++) {
tail_pro[i] = env->extract_integer(env, env->vec_get(env, args[12], i));
tail_pro[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[12], i)));
}
}
int32_t hyphen_protrude = env->extract_integer(env, args[13]);
int32_t first_line_width = env->extract_integer(env, args[14]);
int32_t hyphen_protrude = clamp32(env->extract_integer(env, args[13]));
int32_t first_line_width = clamp32(env->extract_integer(env, args[14]));
/* Forbidden break positions (sorted gap indices, may be empty) */
ptrdiff_t forb_count = env->vec_size(env, args[11]);
@ -416,11 +221,27 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
forb_pos = malloc(forb_count * sizeof(int32_t));
if (forb_pos) {
for (ptrdiff_t i = 0; i < forb_count; i++) {
forb_pos[i] = env->extract_integer(env, env->vec_get(env, args[11], i));
forb_pos[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[11], i)));
}
}
}
/* One consolidated gate: any partial allocation above (silent
* "no kinsoku / no hyphenation" degradation) or a pending Lisp
* signal from a bad element type must fail the whole call the
* Elisp engine is the correct fallback, not a subtly different
* layout. */
if ((hyph_count > 0 && !hyph_pos) ||
(forb_count > 0 && !forb_pos) ||
!tail_pro ||
env->non_local_exit_check(env) != emacs_funcall_exit_return) {
free(ideal_prefix); free(min_prefix); free(max_prefix);
free(glue_ideals); free(glue_shrinks); free(glue_stretches);
free(lead_spaces); free(trail_spaces);
free(hyph_pos); free(forb_pos); free(tail_pro);
return env->intern(env, "nil");
}
/* Call the pure DP function */
ekp_result_t *result = ekp_break_with_prefixes(
ideal_prefix, min_prefix, max_prefix,
@ -500,17 +321,17 @@ static bool extract_paragraph_data(
}
for (ptrdiff_t i = 0; i < prefix_len; i++) {
(*ideal_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[0], i));
(*min_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[1], i));
(*max_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[2], i));
(*lead_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[9], i));
(*trail_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[10], i));
(*ideal_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[0], i)));
(*min_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[1], i)));
(*max_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[2], i)));
(*lead_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[9], i)));
(*trail_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i)));
}
for (size_t i = 0; i < *n; i++) {
(*glue_ideals)[i] = env->extract_integer(env, env->vec_get(env, args[3], i));
(*glue_shrinks)[i] = env->extract_integer(env, env->vec_get(env, args[4], i));
(*glue_stretches)[i] = env->extract_integer(env, env->vec_get(env, args[5], i));
(*glue_ideals)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[3], i)));
(*glue_shrinks)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[4], i)));
(*glue_stretches)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i)));
}
*hyph_count = env->vec_size(env, args[6]);
@ -519,13 +340,13 @@ static bool extract_paragraph_data(
*hyph_pos = malloc(*hyph_count * sizeof(int32_t));
if (*hyph_pos) {
for (ptrdiff_t i = 0; i < *hyph_count; i++) {
(*hyph_pos)[i] = env->extract_integer(env, env->vec_get(env, args[6], i));
(*hyph_pos)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i)));
}
}
}
*hyph_width = env->extract_integer(env, args[7]);
*line_width = env->extract_integer(env, args[8]);
*hyph_width = clamp32(env->extract_integer(env, args[7]));
*line_width = clamp32(env->extract_integer(env, args[8]));
*forb_count = env->vec_size(env, args[11]);
*forb_pos = NULL;
@ -533,7 +354,7 @@ static bool extract_paragraph_data(
*forb_pos = malloc(*forb_count * sizeof(int32_t));
if (*forb_pos) {
for (ptrdiff_t i = 0; i < *forb_count; i++) {
(*forb_pos)[i] = env->extract_integer(env, env->vec_get(env, args[11], i));
(*forb_pos)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[11], i)));
}
}
}
@ -541,11 +362,26 @@ static bool extract_paragraph_data(
*tail_pro = malloc(prefix_len * sizeof(int32_t));
if (*tail_pro) {
for (ptrdiff_t i = 0; i < prefix_len; i++) {
(*tail_pro)[i] = env->extract_integer(env, env->vec_get(env, args[12], i));
(*tail_pro)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[12], i)));
}
}
*hyphen_protrude = env->extract_integer(env, args[13]);
*first_line_width = env->extract_integer(env, args[14]);
*hyphen_protrude = clamp32(env->extract_integer(env, args[13]));
*first_line_width = clamp32(env->extract_integer(env, args[14]));
if ((*hyph_count > 0 && !*hyph_pos) ||
(*forb_count > 0 && !*forb_pos) ||
!*tail_pro ||
env->non_local_exit_check(env) != emacs_funcall_exit_return) {
free(*ideal_prefix); free(*min_prefix); free(*max_prefix);
free(*glue_ideals); free(*glue_shrinks); free(*glue_stretches);
free(*lead_spaces); free(*trail_spaces);
free(*hyph_pos); free(*forb_pos); free(*tail_pro);
*ideal_prefix = *min_prefix = *max_prefix = NULL;
*glue_ideals = *glue_shrinks = *glue_stretches = NULL;
*lead_spaces = *trail_spaces = NULL;
*hyph_pos = *forb_pos = *tail_pro = NULL;
return false;
}
return true;
}
@ -740,18 +576,6 @@ int emacs_module_init(struct emacs_runtime *runtime)
defun(env, "ekp-c-cleanup", 0, 0, Fekp_c_cleanup,
"Cleanup EKP C module resources.");
defun(env, "ekp-c-load-hyphenator", 1, 1, Fekp_c_load_hyphenator,
"Load hyphenation dictionary from PATH.\n\
Returns hyphenator index or nil on failure.\n\n(fn PATH)");
defun(env, "ekp-c-set-spacing", 9, 9, Fekp_c_set_spacing,
"Set spacing parameters (in pixels).\n\n\
Arguments are: LWS-IDEAL LWS-STRETCH LWS-SHRINK\n\
MWS-IDEAL MWS-STRETCH MWS-SHRINK\n\
CWS-IDEAL CWS-STRETCH CWS-SHRINK\n\n\
LWS = Latin Word Space, MWS = Mixed, CWS = CJK.\n\n\
(fn LWS-I LWS-+ LWS-- MWS-I MWS-+ MWS-- CWS-I CWS-+ CWS--)");
defun(env, "ekp-c-set-penalties", 4, 7, Fekp_c_set_penalties,
"Set Knuth-Plass algorithm penalties.\n\n\
LINE-PENALTY: base penalty per line break (default 10)\n\
@ -763,18 +587,6 @@ LAST-LINE-SHORT-PENALTY: multiplier for short last lines (default 50.0)\n\n\
(fn LINE-PENALTY HYPHEN-PENALTY FITNESS-PENALTY LAST-LINE-RATIO \
&optional CONSEC-HYPHEN-PENALTY LAST-LINE-SHORT-PENALTY)");
defun(env, "ekp-c-hyphenate", 2, 2, Fekp_c_hyphenate,
"Get hyphenation positions for WORD using HYPHENATOR-INDEX.\n\
Returns list of positions where word can be hyphenated.\n\n(fn HYPHENATOR-INDEX WORD)");
defun(env, "ekp-c-break-lines", 4, 4, Fekp_c_break_lines,
"Break STRING into lines of LINE-WIDTH pixels.\n\n\
Uses Knuth-Plass optimal line breaking with hyphenation.\n\
HYPHENATOR-INDEX: index from `ekp-c-load-hyphenator', or -1 for none\n\
MEASURE-FUNC: function that takes a string and returns pixel width\n\n\
Returns (BREAKS . TOTAL-COST) where BREAKS is list of break positions.\n\n\
(fn STRING HYPHENATOR-INDEX LINE-WIDTH MEASURE-FUNC)");
defun(env, "ekp-c-break-with-arrays", 15, 15, Fekp_c_break_with_arrays,
"Break lines using Elisp's pre-computed prefix arrays (preferred API).\n\n\
IDEAL-PREFIX: vector of ideal width prefix sums (n+1 elements)\n\

View File

@ -1,321 +0,0 @@
/*
* ekp_hyphen.c - Liang hyphenation algorithm implementation
*
* Copyright (C) 2024-2026 Kinney Zhang
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of emacs-kp, which 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.
* It is distributed WITHOUT ANY WARRANTY; see the GNU General Public
* License (COPYING) for details.
*
* Fast, thread-safe hyphenation with pattern caching.
* Uses FNV-1a hash for O(1) pattern lookup.
*/
#include "ekp_module.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* FNV-1a hash constants */
#define FNV_OFFSET 14695981039346656037ULL
#define FNV_PRIME 1099511628211ULL
static uint64_t fnv1a_hash(const char *data, size_t len)
{
uint64_t hash = FNV_OFFSET;
for (size_t i = 0; i < len; i++) {
hash ^= (uint8_t)data[i];
hash *= FNV_PRIME;
}
return hash;
}
/*
* Parse a pattern like "hy3ph" into letters and values.
* Returns true on success.
*/
static bool parse_pattern(const char *pat, ekp_pattern_t *out)
{
size_t pat_len = strlen(pat);
if (pat_len == 0 || pat_len >= EKP_MAX_PATTERN_LEN * 2)
return false;
size_t pos = 0;
size_t letter_idx = 0;
size_t value_idx = 0;
memset(out->values, 0, sizeof(out->values));
memset(out->letters, 0, sizeof(out->letters));
while (pos < pat_len) {
/* Read optional digit */
uint8_t digit = 0;
if (isdigit((unsigned char)pat[pos])) {
digit = pat[pos] - '0';
pos++;
}
out->values[value_idx++] = digit;
/* Read letter if present */
if (pos < pat_len && !isdigit((unsigned char)pat[pos])) {
out->letters[letter_idx++] = pat[pos];
pos++;
}
}
out->len = letter_idx;
/* Find non-zero range */
size_t start = 0, end = value_idx;
while (start < end && out->values[start] == 0) start++;
while (end > start && out->values[end - 1] == 0) end--;
out->offset = start;
/* Shift values to start */
if (start > 0) {
memmove(out->values, out->values + start, end - start);
memset(out->values + (end - start), 0, start);
}
return letter_idx > 0;
}
/*
* Load patterns from .dic file
*/
ekp_hyphenator_t *ekp_hyphen_create(const char *dict_path)
{
FILE *fp = fopen(dict_path, "r");
if (!fp)
return NULL;
ekp_hyphenator_t *h = calloc(1, sizeof(*h));
if (!h) {
fclose(fp);
return NULL;
}
pthread_rwlock_init(&h->lock, NULL);
h->left_min = 2;
h->right_min = 2;
/* First pass: count patterns */
char line[256];
size_t count = 0;
if (!fgets(line, sizeof(line), fp)) {
/* empty file: no encoding line to skip; count loop sees EOF */
}
while (fgets(line, sizeof(line), fp)) {
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
line[--len] = '\0';
/* Skip empty, comments, HYPHENMIN, patterns with / */
if (len == 0 || line[0] == '%' || line[0] == '#')
continue;
if (strstr(line, "HYPHENMIN") || strchr(line, '/'))
continue;
count++;
}
/* Allocate patterns */
h->patterns = calloc(count, sizeof(ekp_pattern_t));
h->hash_size = count * 2; /* load factor 0.5 */
h->hash_table = calloc(h->hash_size, sizeof(uint32_t));
if (!h->patterns || !h->hash_table) {
ekp_hyphen_destroy(h);
fclose(fp);
return NULL;
}
/* Second pass: parse patterns */
rewind(fp);
if (!fgets(line, sizeof(line), fp)) {
/* empty file: no encoding line to skip; parse loop sees EOF */
}
size_t idx = 0;
while (fgets(line, sizeof(line), fp)) {
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
line[--len] = '\0';
if (len == 0 || line[0] == '%' || line[0] == '#')
continue;
if (strstr(line, "HYPHENMIN") || strchr(line, '/'))
continue;
/* Handle ^^XX hex escapes */
char decoded[256];
char *dst = decoded;
const char *src = line;
while (*src) {
if (src[0] == '^' && src[1] == '^' &&
isxdigit((unsigned char)src[2]) &&
isxdigit((unsigned char)src[3])) {
char hex[3] = {src[2], src[3], 0};
*dst++ = (char)strtol(hex, NULL, 16);
src += 4;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
if (parse_pattern(decoded, &h->patterns[idx])) {
/* Insert into hash table */
uint64_t hash = fnv1a_hash(h->patterns[idx].letters,
h->patterns[idx].len);
size_t slot = hash % h->hash_size;
while (h->hash_table[slot] != 0) {
slot = (slot + 1) % h->hash_size;
}
h->hash_table[slot] = idx + 1; /* 1-indexed */
if (h->patterns[idx].len > h->max_pattern_len)
h->max_pattern_len = h->patterns[idx].len;
idx++;
}
}
h->pattern_count = idx;
fclose(fp);
return h;
}
void ekp_hyphen_destroy(ekp_hyphenator_t *h)
{
if (!h) return;
pthread_rwlock_destroy(&h->lock);
free(h->patterns);
free(h->hash_table);
free(h);
}
/*
* Find pattern by letters (hash table lookup)
*/
static ekp_pattern_t *find_pattern(ekp_hyphenator_t *h,
const char *letters, size_t len)
{
if (len == 0 || len > h->max_pattern_len)
return NULL;
uint64_t hash = fnv1a_hash(letters, len);
size_t slot = hash % h->hash_size;
for (size_t i = 0; i < h->hash_size; i++) {
uint32_t idx = h->hash_table[slot];
if (idx == 0)
return NULL;
ekp_pattern_t *p = &h->patterns[idx - 1];
if (p->len == len && memcmp(p->letters, letters, len) == 0)
return p;
slot = (slot + 1) % h->hash_size;
}
return NULL;
}
/*
* Compute hyphenation positions for a word
* Thread-safe (read lock)
*/
int ekp_hyphen_word(ekp_hyphenator_t *h, const char *word, size_t len,
int8_t *positions, size_t max_pos)
{
if (!h || !word || len == 0 || len > EKP_MAX_WORD_LEN - 2)
return 0;
/* Check cache first */
uint64_t word_hash = fnv1a_hash(word, len);
size_t cache_slot = word_hash % EKP_CACHE_SIZE;
pthread_rwlock_rdlock(&h->lock);
if (h->cache[cache_slot].hash == word_hash &&
strncmp(h->cache[cache_slot].word, word, len) == 0) {
int count = h->cache[cache_slot].pos_count;
if (count <= (int)max_pos) {
memcpy(positions, h->cache[cache_slot].positions,
count * sizeof(int8_t));
}
pthread_rwlock_unlock(&h->lock);
return count;
}
pthread_rwlock_unlock(&h->lock);
/* Compute hyphenation */
char padded[EKP_MAX_WORD_LEN + 2];
padded[0] = '.';
for (size_t i = 0; i < len; i++)
padded[i + 1] = tolower((unsigned char)word[i]);
padded[len + 1] = '.';
size_t padded_len = len + 2;
uint8_t prio[EKP_MAX_WORD_LEN + 3];
memset(prio, 0, sizeof(prio));
/* Apply matching patterns */
pthread_rwlock_rdlock(&h->lock);
for (size_t i = 0; i < padded_len - 1; i++) {
for (size_t j = i + 1; j <= padded_len && j <= i + h->max_pattern_len; j++) {
ekp_pattern_t *pat = find_pattern(h, padded + i, j - i);
if (pat) {
size_t val_len = pat->len + 1 - pat->offset;
for (size_t k = 0; k < val_len && k < sizeof(pat->values); k++) {
size_t pos = i + pat->offset + k;
if (pos < sizeof(prio) && pat->values[k] > prio[pos])
prio[pos] = pat->values[k];
}
}
}
}
pthread_rwlock_unlock(&h->lock);
/* Collect odd positions (subtract 1 for padding offset) */
int8_t result[EKP_MAX_WORD_LEN];
int count = 0;
for (size_t i = 1; i < padded_len && count < EKP_MAX_WORD_LEN; i++) {
if (prio[i] & 1) { /* odd = break allowed */
int pos = (int)i - 1; /* adjust for leading '.' */
/* Apply margin constraints */
if (pos >= h->left_min && pos <= (int)len - h->right_min) {
result[count++] = pos;
}
}
}
/* Update cache */
pthread_rwlock_wrlock(&h->lock);
h->cache[cache_slot].hash = word_hash;
strncpy(h->cache[cache_slot].word, word, len);
h->cache[cache_slot].word[len] = '\0';
memcpy(h->cache[cache_slot].positions, result, count * sizeof(int8_t));
h->cache[cache_slot].pos_count = count;
pthread_rwlock_unlock(&h->lock);
/* Copy to output */
int out_count = count < (int)max_pos ? count : (int)max_pos;
memcpy(positions, result, out_count * sizeof(int8_t));
return out_count;
}

View File

@ -98,39 +98,7 @@ static inline double compute_demerits(double badness, int32_t penalty,
}
/*
* Parallel work item for demerits computation
*/
typedef struct {
ekp_paragraph_t *para;
int32_t line_width;
size_t start;
size_t end;
/* Output arrays (pre-allocated) */
double *demerits;
int32_t *backptrs;
int32_t *rest_pixels;
uint8_t *fitness;
int32_t *hyphen_counts;
int32_t *line_counts;
/* Shared read-only input */
const double *prev_demerits;
const uint8_t *prev_fitness;
const int32_t *prev_hyphen_counts;
const int32_t *prev_line_counts;
/* Parameters */
int line_penalty;
int hyphen_penalty;
int fitness_penalty;
double last_line_ratio;
} dp_work_t;
/*
* Unified DP input structure for shared core algorithm
* This allows both ekp_paragraph_t-based and array-based inputs
* to use the same DP core logic.
* Unified DP input structure for the array-based DP core.
*/
typedef struct {
/* Prefix sum arrays */
@ -418,225 +386,6 @@ static void dp_process_position(
}
}
/*
* Process a range of candidate breakpoints (for parallel execution)
* Now uses shared dp_process_position() core.
*/
static void process_dp_range(void *arg)
{
dp_work_t *work = (dp_work_t *)arg;
ekp_paragraph_t *p = work->para;
size_t n = p->box_count;
/* Build temporary glue arrays from paragraph structure */
int32_t *glue_ideals = malloc(n * sizeof(int32_t));
int32_t *glue_shrinks = malloc(n * sizeof(int32_t));
int32_t *glue_stretches = malloc(n * sizeof(int32_t));
if (!glue_ideals || !glue_shrinks || !glue_stretches) {
free(glue_ideals); free(glue_shrinks); free(glue_stretches);
return;
}
for (size_t i = 0; i < n; i++) {
glue_ideals[i] = p->glues[i].ideal;
glue_shrinks[i] = p->glues[i].shrink;
glue_stretches[i] = p->glues[i].stretch;
}
/* Create unified input structure */
dp_input_t in = {
.ideal_prefix = p->ideal_prefix,
.min_prefix = p->min_prefix,
.max_prefix = p->max_prefix,
.glue_ideals = glue_ideals,
.glue_shrinks = glue_shrinks,
.glue_stretches = glue_stretches,
.hyphen_positions = p->hyphen_positions,
.hyphen_count = p->hyphen_count,
.hyphen_width = p->hyphen_width,
.lead_spaces = NULL,
.trail_spaces = NULL,
.n = n,
.line_width = work->line_width,
.line_penalty = work->line_penalty,
.hyphen_penalty = work->hyphen_penalty,
.fitness_penalty = work->fitness_penalty,
.last_line_ratio = work->last_line_ratio,
.consec_hyphen_penalty =
ekp_global ? ekp_global->consec_hyphen_penalty : 100,
.last_line_short_penalty =
ekp_global ? ekp_global->last_line_short_penalty : 50.0,
.allow_emergency = true
};
/* Process each position in range */
for (size_t i = work->start; i < work->end; i++) {
if (work->prev_demerits[i] >= EKP_INFINITY)
continue;
dp_process_position(&in, i,
work->prev_demerits[i],
work->prev_fitness[i],
work->prev_hyphen_counts[i],
work->prev_line_counts[i],
work->demerits,
work->backptrs,
work->rest_pixels,
work->fitness,
work->hyphen_counts,
work->line_counts);
}
free(glue_ideals);
free(glue_shrinks);
free(glue_stretches);
}
/*
* Main line breaking function
*/
ekp_result_t *ekp_break_lines(ekp_paragraph_t *p, int32_t line_width)
{
if (!p || p->box_count == 0 || line_width <= 0)
return NULL;
size_t n = p->box_count;
/* Allocate DP arrays */
double *demerits = malloc((n + 1) * sizeof(double));
int32_t *backptrs = malloc((n + 1) * sizeof(int32_t));
int32_t *rest_pixels = malloc((n + 1) * sizeof(int32_t));
uint8_t *fitness = malloc((n + 1) * sizeof(uint8_t));
int32_t *hyphen_counts = malloc((n + 1) * sizeof(int32_t));
int32_t *line_counts = malloc((n + 1) * sizeof(int32_t));
if (!demerits || !backptrs || !rest_pixels ||
!fitness || !hyphen_counts || !line_counts) {
free(demerits);
free(backptrs);
free(rest_pixels);
free(fitness);
free(hyphen_counts);
free(line_counts);
return NULL;
}
/* Initialize */
for (size_t i = 0; i <= n; i++) {
demerits[i] = EKP_INFINITY;
backptrs[i] = -1;
rest_pixels[i] = 0;
fitness[i] = FITNESS_DECENT;
hyphen_counts[i] = 0;
line_counts[i] = 0;
}
demerits[0] = 0.0;
/* Get parameters */
int line_penalty = ekp_global ? ekp_global->line_penalty : 10;
int hyphen_penalty = ekp_global ? ekp_global->hyphen_penalty : 50;
int fitness_penalty = ekp_global ? ekp_global->fitness_penalty : 100;
double last_ratio = ekp_global ? ekp_global->last_line_ratio : 0.5;
/*
* Single-threaded DP: simple and correct.
*
* Note: Previous "parallel" implementation had data races - multiple
* threads writing to shared demerits[] array without synchronization.
* DP has inherent sequential dependencies (demerits[k] depends on all
* demerits[i] where i < k), making intra-paragraph parallelism complex.
*
* For real parallelism, use ekp_break_batch() to process multiple
* paragraphs concurrently - that's the correct granularity.
*/
dp_work_t work = {
.para = p,
.line_width = line_width,
.start = 0,
.end = n,
.demerits = demerits,
.backptrs = backptrs,
.rest_pixels = rest_pixels,
.fitness = fitness,
.hyphen_counts = hyphen_counts,
.line_counts = line_counts,
.prev_demerits = demerits,
.prev_fitness = fitness,
.prev_hyphen_counts = hyphen_counts,
.prev_line_counts = line_counts,
.line_penalty = line_penalty,
.hyphen_penalty = hyphen_penalty,
.fitness_penalty = fitness_penalty,
.last_line_ratio = last_ratio,
};
/* Iterative DP: O(n²) worst case, typically O(n·m) with early termination */
for (size_t i = 0; i < n; i++) {
if (demerits[i] >= EKP_INFINITY)
continue;
work.start = i;
work.end = i + 1;
process_dp_range(&work);
}
/* Trace back optimal path */
ekp_result_t *result = calloc(1, sizeof(*result));
if (!result) {
free(demerits);
free(backptrs);
free(rest_pixels);
free(fitness);
free(hyphen_counts);
free(line_counts);
return NULL;
}
/* Count breaks */
size_t break_count = 0;
int32_t idx = n;
while (idx > 0) {
break_count++;
idx = backptrs[idx];
if (idx < 0)
break;
}
result->breaks = malloc(break_count * sizeof(int32_t));
result->rest_pixels = malloc(break_count * sizeof(int32_t));
if (!result->breaks || !result->rest_pixels) {
ekp_result_destroy(result);
free(demerits);
free(backptrs);
free(rest_pixels);
free(fitness);
free(hyphen_counts);
free(line_counts);
return NULL;
}
result->break_count = break_count;
result->total_cost = demerits[n];
/* Fill in reverse order */
idx = n;
for (size_t i = break_count; i > 0; i--) {
result->breaks[i - 1] = idx;
result->rest_pixels[i - 1] = rest_pixels[idx];
idx = backptrs[idx];
}
free(demerits);
free(backptrs);
free(rest_pixels);
free(fitness);
free(hyphen_counts);
free(line_counts);
return result;
}
void ekp_result_destroy(ekp_result_t *r)
{
if (!r)
@ -877,6 +626,10 @@ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count)
if (!results)
return NULL;
/* Create the worker pool on first parallel use */
if (count > 1 && ekp_global && !ekp_global->pool)
ekp_global->pool = ekp_pool_create(0);
/* Single paragraph: no point using threads */
if (count == 1 || !ekp_global || !ekp_global->pool) {
for (size_t i = 0; i < count; i++) {
@ -946,16 +699,6 @@ int ekp_init(void)
if (!ekp_global)
return -1;
/* Default spacing */
ekp_global->spacing.lws_ideal = 7;
ekp_global->spacing.lws_stretch = 3;
ekp_global->spacing.lws_shrink = 2;
ekp_global->spacing.mws_ideal = 5;
ekp_global->spacing.mws_stretch = 2;
ekp_global->spacing.mws_shrink = 1;
ekp_global->spacing.cws_ideal = 0;
ekp_global->spacing.cws_stretch = 2;
ekp_global->spacing.cws_shrink = 0;
/* Default K-P parameters */
ekp_global->line_penalty = 10;
@ -965,15 +708,9 @@ int ekp_init(void)
ekp_global->consec_hyphen_penalty = 100;
ekp_global->last_line_short_penalty = 50.0;
/* Create thread pool */
ekp_global->pool = ekp_pool_create(EKP_THREAD_POOL_SIZE);
if (!ekp_global->pool) {
free(ekp_global);
ekp_global = NULL;
return -1;
}
pthread_mutex_init(&ekp_global->cache_lock, NULL);
/* The thread pool is created lazily by the first batch call:
* plain single-paragraph use never starts worker threads. */
ekp_global->pool = NULL;
return 0;
}
@ -983,20 +720,6 @@ void ekp_cleanup(void)
if (!ekp_global)
return;
/* Destroy hyphenators */
for (size_t i = 0; i < ekp_global->hyphenator_count; i++) {
ekp_hyphen_destroy(ekp_global->hyphenators[i]);
}
/* Destroy paragraph cache */
if (ekp_global->para_cache) {
for (size_t i = 0; i < ekp_global->para_cache_size; i++) {
ekp_para_destroy(ekp_global->para_cache[i]);
}
free(ekp_global->para_cache);
}
pthread_mutex_destroy(&ekp_global->cache_lock);
ekp_pool_destroy(ekp_global->pool);
free(ekp_global);
ekp_global = NULL;

View File

@ -23,123 +23,17 @@
#include <stdint.h>
#include <stdbool.h>
#include <pthread.h>
#include <math.h>
/* Version */
#define EKP_VERSION_MAJOR 1
#define EKP_VERSION_MINOR 5
/* Limits */
#define EKP_MAX_PATTERN_LEN 64
#define EKP_MAX_WORD_LEN 256
#define EKP_CACHE_SIZE 4096
#define EKP_THREAD_POOL_SIZE 8
#define EKP_THREAD_POOL_MAX 64
/* Infinity for impossible breaks */
#define EKP_INFINITY 1e10
/*
* Box: indivisible content with fixed width
* Keep it small - we'll have thousands of these
*/
typedef struct {
const char *text; /* UTF-8 string, NOT owned */
int32_t text_len; /* byte length */
int32_t pixel_width; /* rendered width in pixels */
uint8_t box_type; /* 0=latin, 1=cjk, 2=cjk_punct, 3=space */
uint8_t start_type; /* first char type */
uint8_t end_type; /* last char type */
} ekp_box_t;
/*
* Glue: flexible space between boxes
* The heart of Knuth-Plass: ideal ± stretch/shrink
*/
typedef struct {
int16_t ideal; /* natural width */
int16_t stretch; /* max stretch */
int16_t shrink; /* max shrink */
uint8_t type; /* 0=none, 1=lws, 2=mws, 3=cws */
} ekp_glue_t;
/*
* Breakpoint candidate for DP
*/
typedef struct {
int32_t index; /* box index */
int32_t prev; /* previous breakpoint index */
double demerits; /* accumulated demerits */
int32_t line_count; /* lines so far */
uint8_t fitness; /* 0-3: tight to very-loose */
uint8_t hyphen_count; /* consecutive hyphens */
bool is_hyphen; /* ends with hyphen? */
} ekp_breakpoint_t;
/*
* Hyphenation pattern (Liang's algorithm)
* Compact representation: letters + priority values
*/
typedef struct {
char letters[EKP_MAX_PATTERN_LEN];
uint8_t values[EKP_MAX_PATTERN_LEN + 1];
uint8_t len;
uint8_t offset; /* where values start */
} ekp_pattern_t;
/*
* Hyphenator: compiled patterns + cache
* Thread-safe with read-write lock
*/
typedef struct {
ekp_pattern_t *patterns;
size_t pattern_count;
size_t max_pattern_len;
/* Hash table for O(1) pattern lookup */
uint32_t *hash_table;
size_t hash_size;
/* Word cache (LRU) */
struct {
uint64_t hash;
char word[EKP_MAX_WORD_LEN];
int8_t positions[EKP_MAX_WORD_LEN];
int pos_count;
} cache[EKP_CACHE_SIZE];
size_t cache_head;
pthread_rwlock_t lock;
/* Margin constraints */
int left_min;
int right_min;
} ekp_hyphenator_t;
/*
* Paragraph: preprocessed text ready for line breaking
* All arrays are parallel: boxes[i] has glues[i], widths[i], etc.
*/
typedef struct {
ekp_box_t *boxes;
ekp_glue_t *glues;
size_t box_count;
/* Prefix sums for O(1) range queries */
int32_t *ideal_prefix;
int32_t *min_prefix;
int32_t *max_prefix;
/* Hyphenation data */
int32_t *hyphen_positions;
size_t hyphen_count;
int32_t hyphen_width;
/* Original string (owned) */
char *text;
size_t text_len;
/* Hash for cache lookup */
uint64_t hash;
} ekp_paragraph_t;
#define EKP_INFINITY HUGE_VAL /* unreachable sentinel */
/*
* Line break result
@ -151,20 +45,11 @@ typedef struct {
double total_cost;
} ekp_result_t;
/*
* Global spacing parameters
*/
typedef struct {
int16_t lws_ideal, lws_stretch, lws_shrink;
int16_t mws_ideal, mws_stretch, mws_shrink;
int16_t cws_ideal, cws_stretch, cws_shrink;
} ekp_spacing_t;
/*
* Thread pool for parallel computation
*/
typedef struct {
pthread_t threads[EKP_THREAD_POOL_SIZE];
pthread_t threads[EKP_THREAD_POOL_MAX];
size_t thread_count;
pthread_mutex_t queue_lock;
pthread_cond_t queue_cond;
@ -186,15 +71,7 @@ typedef struct {
* Global state
*/
typedef struct {
ekp_hyphenator_t *hyphenators[32]; /* by language */
size_t hyphenator_count;
ekp_paragraph_t **para_cache;
size_t para_cache_size;
pthread_mutex_t cache_lock;
ekp_spacing_t spacing;
ekp_thread_pool_t *pool;
ekp_thread_pool_t *pool; /* created lazily on first batch */
/* K-P parameters */
int line_penalty;
@ -213,23 +90,14 @@ extern ekp_state_t *ekp_global;
/*
* API: Hyphenation
*/
ekp_hyphenator_t *ekp_hyphen_create(const char *dict_path);
void ekp_hyphen_destroy(ekp_hyphenator_t *h);
int ekp_hyphen_word(ekp_hyphenator_t *h, const char *word, size_t len,
int8_t *positions, size_t max_pos);
/*
* API: Paragraph processing
*/
ekp_paragraph_t *ekp_para_create(const char *text, size_t len,
ekp_hyphenator_t *h,
int32_t (*measure_fn)(const char *, size_t));
void ekp_para_destroy(ekp_paragraph_t *p);
/*
* API: Line breaking (the main algorithm)
*/
ekp_result_t *ekp_break_lines(ekp_paragraph_t *p, int32_t line_width);
void ekp_result_destroy(ekp_result_t *r);
/*
@ -313,6 +181,7 @@ ekp_result_t **ekp_break_batch(
/*
* API: Thread pool
*/
size_t ekp_pool_default_threads(void);
ekp_thread_pool_t *ekp_pool_create(size_t num_threads);
void ekp_pool_destroy(ekp_thread_pool_t *pool);
void ekp_pool_submit(ekp_thread_pool_t *pool, void (*func)(void *), void *arg);

View File

@ -1,408 +0,0 @@
/*
* ekp_paragraph.c - Text preprocessing and box/glue construction
*
* Copyright (C) 2024-2026 Kinney Zhang
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of emacs-kp, which 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.
* It is distributed WITHOUT ANY WARRANTY; see the GNU General Public
* License (COPYING) for details.
*
* The boring part that makes everything else fast.
* Get the data layout right, and the algorithm sings.
*/
#include "ekp_module.h"
#include <stdlib.h>
#include <string.h>
/* Box types */
#define BOX_LATIN 0
#define BOX_CJK 1
#define BOX_CJK_PUNCT 2
#define BOX_SPACE 3
/* Glue types */
#define GLUE_NONE 0
#define GLUE_LWS 1 /* Latin word space */
#define GLUE_MWS 2 /* Mixed (Latin-CJK) */
#define GLUE_CWS 3 /* CJK character space */
/* UTF-8 helpers */
static inline uint32_t utf8_decode(const char *s, int *len)
{
unsigned char c = s[0];
*len = 1;
if ((c & 0x80) == 0)
return c;
if ((c & 0xE0) == 0xC0) {
*len = 2;
return ((c & 0x1F) << 6) | (s[1] & 0x3F);
}
if ((c & 0xF0) == 0xE0) {
*len = 3;
return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F);
}
if ((c & 0xF8) == 0xF0) {
*len = 4;
return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) |
((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
}
return c;
}
/* Character classification */
static inline bool is_cjk(uint32_t cp)
{
/* CJK Unified Ideographs and related blocks */
return (cp >= 0x4E00 && cp <= 0x9FFF) || /* CJK Unified */
(cp >= 0x3400 && cp <= 0x4DBF) || /* CJK Ext A */
(cp >= 0x20000 && cp <= 0x2A6DF) || /* CJK Ext B */
(cp >= 0x2A700 && cp <= 0x2B73F) || /* CJK Ext C */
(cp >= 0x2B740 && cp <= 0x2B81F) || /* CJK Ext D */
(cp >= 0xF900 && cp <= 0xFAFF) || /* CJK Compat */
(cp >= 0x3000 && cp <= 0x303F) || /* CJK Symbols */
(cp >= 0x3040 && cp <= 0x309F) || /* Hiragana */
(cp >= 0x30A0 && cp <= 0x30FF) || /* Katakana */
(cp >= 0xAC00 && cp <= 0xD7AF); /* Hangul */
}
static inline bool is_cjk_punct(uint32_t cp)
{
return (cp >= 0x3000 && cp <= 0x303F) || /* CJK Symbols */
(cp >= 0xFF00 && cp <= 0xFF60) || /* Fullwidth Forms */
cp == 0x201C || cp == 0x201D || /* " " */
cp == 0x2018 || cp == 0x2019; /* ' ' */
}
static inline bool is_whitespace(uint32_t cp)
{
return cp == ' ' || cp == '\t' || cp == '\n' || cp == '\r' ||
cp == 0x00A0 || cp == 0x3000; /* NBSP, ideographic space */
}
/*
* Determine box type from codepoint
*/
static uint8_t classify_char(uint32_t cp)
{
if (is_whitespace(cp))
return BOX_SPACE;
if (is_cjk_punct(cp))
return BOX_CJK_PUNCT;
if (is_cjk(cp))
return BOX_CJK;
return BOX_LATIN;
}
/*
* Determine glue type between two boxes
*/
static uint8_t glue_between(uint8_t prev_end, uint8_t curr_start)
{
if (prev_end == BOX_SPACE || curr_start == BOX_SPACE)
return GLUE_NONE;
bool prev_latin = (prev_end == BOX_LATIN);
bool curr_latin = (curr_start == BOX_LATIN);
if (prev_latin && curr_latin)
return GLUE_LWS;
if (!prev_latin && !curr_latin)
return GLUE_CWS;
return GLUE_MWS;
}
/*
* Split text into boxes with hyphenation
*/
ekp_paragraph_t *ekp_para_create(const char *text, size_t len,
ekp_hyphenator_t *h,
int32_t (*measure_fn)(const char *, size_t))
{
if (!text || len == 0)
return NULL;
ekp_paragraph_t *p = calloc(1, sizeof(*p));
if (!p)
return NULL;
/* Copy text */
p->text = malloc(len + 1);
if (!p->text) {
free(p);
return NULL;
}
memcpy(p->text, text, len);
p->text[len] = '\0';
p->text_len = len;
/* Compute hash for caching */
uint64_t hash = 14695981039346656037ULL;
for (size_t i = 0; i < len; i++) {
hash ^= (uint8_t)text[i];
hash *= 1099511628211ULL;
}
p->hash = hash;
/* First pass: count boxes (rough estimate) */
size_t max_boxes = len + 1;
/* Temporary arrays for first pass */
size_t *box_starts = malloc(max_boxes * sizeof(size_t));
size_t *box_lens = malloc(max_boxes * sizeof(size_t));
uint8_t *box_types = malloc(max_boxes * sizeof(uint8_t));
if (!box_starts || !box_lens || !box_types) {
free(box_starts);
free(box_lens);
free(box_types);
ekp_para_destroy(p);
return NULL;
}
/* Tokenize into boxes */
size_t box_count = 0;
size_t pos = 0;
size_t word_start = 0;
bool in_latin_word = false;
while (pos < len) {
int char_len;
uint32_t cp = utf8_decode(text + pos, &char_len);
uint8_t type = classify_char(cp);
if (in_latin_word) {
if (type != BOX_LATIN) {
/* End Latin word */
box_starts[box_count] = word_start;
box_lens[box_count] = pos - word_start;
box_types[box_count] = BOX_LATIN;
box_count++;
in_latin_word = false;
}
}
if (type == BOX_LATIN) {
if (!in_latin_word) {
word_start = pos;
in_latin_word = true;
}
} else {
/* Non-Latin: each character is its own box */
box_starts[box_count] = pos;
box_lens[box_count] = char_len;
box_types[box_count] = type;
box_count++;
}
pos += char_len;
}
/* Flush final Latin word */
if (in_latin_word) {
box_starts[box_count] = word_start;
box_lens[box_count] = pos - word_start;
box_types[box_count] = BOX_LATIN;
box_count++;
}
/* Hyphenation: expand Latin words */
size_t *hyphen_pos = malloc(max_boxes * sizeof(size_t));
size_t hyphen_count = 0;
/* Estimate expanded size */
size_t expanded_boxes = box_count * 2;
ekp_box_t *boxes = calloc(expanded_boxes, sizeof(ekp_box_t));
if (!boxes || !hyphen_pos) {
free(box_starts);
free(box_lens);
free(box_types);
free(hyphen_pos);
free(boxes);
ekp_para_destroy(p);
return NULL;
}
size_t final_count = 0;
for (size_t i = 0; i < box_count; i++) {
const char *box_text = text + box_starts[i];
size_t box_len = box_lens[i];
uint8_t type = box_types[i];
if (type == BOX_LATIN && h && box_len > 4) {
/* Try hyphenation */
int8_t positions[EKP_MAX_WORD_LEN];
int pos_count = ekp_hyphen_word(h, box_text, box_len,
positions, EKP_MAX_WORD_LEN);
if (pos_count > 0) {
/* Split at hyphenation points */
size_t prev_split = 0;
for (int j = 0; j < pos_count; j++) {
size_t split = positions[j];
if (split <= prev_split || split >= box_len)
continue;
boxes[final_count].text = box_text + prev_split;
boxes[final_count].text_len = split - prev_split;
boxes[final_count].box_type = BOX_LATIN;
boxes[final_count].start_type = BOX_LATIN;
boxes[final_count].end_type = BOX_LATIN;
boxes[final_count].pixel_width =
measure_fn ? measure_fn(boxes[final_count].text,
boxes[final_count].text_len) : 0;
hyphen_pos[hyphen_count++] = final_count;
final_count++;
prev_split = split;
}
/* Final segment */
if (prev_split < box_len) {
boxes[final_count].text = box_text + prev_split;
boxes[final_count].text_len = box_len - prev_split;
boxes[final_count].box_type = BOX_LATIN;
boxes[final_count].start_type = BOX_LATIN;
boxes[final_count].end_type = BOX_LATIN;
boxes[final_count].pixel_width =
measure_fn ? measure_fn(boxes[final_count].text,
boxes[final_count].text_len) : 0;
final_count++;
}
continue;
}
}
/* No hyphenation */
boxes[final_count].text = box_text;
boxes[final_count].text_len = box_len;
boxes[final_count].box_type = type;
boxes[final_count].start_type = type;
boxes[final_count].end_type = type;
boxes[final_count].pixel_width =
measure_fn ? measure_fn(box_text, box_len) : 0;
final_count++;
}
free(box_starts);
free(box_lens);
free(box_types);
/* Build final arrays */
p->boxes = boxes;
p->box_count = final_count;
/* Hyphenation positions */
p->hyphen_positions = malloc(hyphen_count * sizeof(int32_t));
if (p->hyphen_positions) {
for (size_t i = 0; i < hyphen_count; i++) {
p->hyphen_positions[i] = hyphen_pos[i];
}
p->hyphen_count = hyphen_count;
}
free(hyphen_pos);
/* Hyphen width */
p->hyphen_width = measure_fn ? measure_fn("-", 1) : 5;
/* Build glues */
p->glues = calloc(final_count, sizeof(ekp_glue_t));
if (!p->glues) {
ekp_para_destroy(p);
return NULL;
}
if (!ekp_global) {
ekp_para_destroy(p);
return NULL;
}
ekp_spacing_t *sp = &ekp_global->spacing;
for (size_t i = 0; i < final_count; i++) {
/* Check if after hyphenation point */
bool after_hyphen = false;
for (size_t j = 0; j < p->hyphen_count; j++) {
if ((size_t)(p->hyphen_positions[j] + 1) == i) {
after_hyphen = true;
break;
}
}
if (after_hyphen || i == 0) {
p->glues[i].type = GLUE_NONE;
continue;
}
uint8_t prev_end = boxes[i - 1].end_type;
uint8_t curr_start = boxes[i].start_type;
uint8_t gtype = glue_between(prev_end, curr_start);
p->glues[i].type = gtype;
switch (gtype) {
case GLUE_LWS:
p->glues[i].ideal = sp->lws_ideal;
p->glues[i].stretch = sp->lws_stretch;
p->glues[i].shrink = sp->lws_shrink;
break;
case GLUE_MWS:
p->glues[i].ideal = sp->mws_ideal;
p->glues[i].stretch = sp->mws_stretch;
p->glues[i].shrink = sp->mws_shrink;
break;
case GLUE_CWS:
p->glues[i].ideal = sp->cws_ideal;
p->glues[i].stretch = sp->cws_stretch;
p->glues[i].shrink = sp->cws_shrink;
break;
default:
break;
}
}
/* Build prefix sums for O(1) range queries */
p->ideal_prefix = calloc(final_count + 1, sizeof(int32_t));
p->min_prefix = calloc(final_count + 1, sizeof(int32_t));
p->max_prefix = calloc(final_count + 1, sizeof(int32_t));
if (!p->ideal_prefix || !p->min_prefix || !p->max_prefix) {
ekp_para_destroy(p);
return NULL;
}
for (size_t i = 0; i < final_count; i++) {
int32_t box_w = boxes[i].pixel_width;
int32_t glue_ideal = p->glues[i].ideal;
int32_t glue_stretch = p->glues[i].stretch;
int32_t glue_shrink = p->glues[i].shrink;
p->ideal_prefix[i + 1] = p->ideal_prefix[i] + box_w + glue_ideal;
p->min_prefix[i + 1] = p->min_prefix[i] + box_w + (glue_ideal - glue_shrink);
p->max_prefix[i + 1] = p->max_prefix[i] + box_w + (glue_ideal + glue_stretch);
}
return p;
}
void ekp_para_destroy(ekp_paragraph_t *p)
{
if (!p)
return;
free(p->text);
free(p->boxes);
free(p->glues);
free(p->hyphen_positions);
free(p->ideal_prefix);
free(p->min_prefix);
free(p->max_prefix);
free(p);
}

View File

@ -18,6 +18,9 @@
#include "ekp_module.h"
#include <stdlib.h>
#include <string.h>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#define QUEUE_CAPACITY 1024
@ -41,8 +44,12 @@ static void *worker_thread(void *arg)
/* Dequeue work */
void (*func)(void *) = pool->queue[pool->queue_head].func;
void *work_arg = pool->queue[pool->queue_head].arg;
bool was_full =
((pool->queue_tail + 1) % pool->queue_size) == pool->queue_head;
pool->queue_head = (pool->queue_head + 1) % pool->queue_size;
pool->active_count++;
if (was_full)
pthread_cond_broadcast(&pool->done_cond);
pthread_mutex_unlock(&pool->queue_lock);
@ -62,12 +69,25 @@ static void *worker_thread(void *arg)
return NULL;
}
size_t ekp_pool_default_threads(void)
{
long n = 0;
#if defined(_SC_NPROCESSORS_ONLN)
n = sysconf(_SC_NPROCESSORS_ONLN);
#endif
if (n <= 0)
n = 4;
if (n > EKP_THREAD_POOL_MAX)
n = EKP_THREAD_POOL_MAX;
return (size_t)n;
}
ekp_thread_pool_t *ekp_pool_create(size_t num_threads)
{
if (num_threads == 0)
num_threads = EKP_THREAD_POOL_SIZE;
if (num_threads > EKP_THREAD_POOL_SIZE)
num_threads = EKP_THREAD_POOL_SIZE;
num_threads = ekp_pool_default_threads();
if (num_threads > EKP_THREAD_POOL_MAX)
num_threads = EKP_THREAD_POOL_MAX;
ekp_thread_pool_t *pool = calloc(1, sizeof(*pool));
if (!pool)
@ -134,17 +154,21 @@ void ekp_pool_submit(ekp_thread_pool_t *pool, void (*func)(void *), void *arg)
pthread_mutex_lock(&pool->queue_lock);
size_t next_tail = (pool->queue_tail + 1) % pool->queue_size;
/* Queue full - drop task (shouldn't happen with proper sizing) */
if (next_tail == pool->queue_head) {
/* Queue full: wait for a worker to make room. Dropping the task
* here used to silently degrade the batch to the Elisp fallback
* exactly when parallelism mattered most. */
while (((pool->queue_tail + 1) % pool->queue_size) == pool->queue_head
&& !pool->shutdown) {
pthread_cond_wait(&pool->done_cond, &pool->queue_lock);
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->queue_lock);
return;
}
pool->queue[pool->queue_tail].func = func;
pool->queue[pool->queue_tail].arg = arg;
pool->queue_tail = next_tail;
pool->queue_tail = (pool->queue_tail + 1) % pool->queue_size;
pthread_cond_signal(&pool->queue_cond);
pthread_mutex_unlock(&pool->queue_lock);