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-init "ext:ekp")
(declare-function ekp-c-version "ext:ekp") (declare-function ekp-c-version "ext:ekp")
(declare-function ekp-c-thread-count "ext:ekp") (declare-function ekp-c-thread-count "ext:ekp")
(declare-function ekp-c-load-hyphenator "ext:ekp")
(defvar ekp-c-module-loaded nil (defvar ekp-c-module-loaded nil
"Non-nil if C module is loaded.") "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 () (defun ekp-c-module-dir ()
"Return the C module directory." "Return the C module directory."
(when-let ((root-dir (ekp-root-dir))) (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))))) (ekp-c-version) (ekp-c-thread-count)))))
(message "C module not found. Run 'make' in ekp_c/ directory.")))) (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 ;;;###autoload
(defun ekp-c-module-build () (defun ekp-c-module-build ()
"Build the C module using make." "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 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." runs the pure DP. Falls back to Elisp when the C call fails."
(ekp--c-sync-params) (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-ideal-prefixs para)
(ekp-para-min-prefixs para) (ekp-para-min-prefixs para)
(ekp-para-max-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-forbidden-positions para)
(ekp-para-tail-protrudes para) (ekp-para-tail-protrudes para)
(ekp-para-hyphen-protrude 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-breaks (car result))
(c-cost (cdr result))) (c-cost (cdr result)))
(if (null c-breaks) (if (null c-breaks)
@ -1723,12 +1727,16 @@ Only computes strings that aren't already cached."
(mapcar (lambda (ip) (mapcar (lambda (ip)
(ekp--prepare-para-for-c (cdr ip) line-pixel)) (ekp--prepare-para-for-c (cdr ip) line-pixel))
needs-compute))) 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 (cl-loop for ip in needs-compute
for j from 0 for j from 0
for idx = (car ip) for idx = (car ip)
for para = (cdr 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 breaks = (car res)
for cost = (cdr res) for cost = (cdr res)
do (aset results idx do (aset results idx

View File

@ -67,7 +67,7 @@ else
endif endif
# Source files # 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) OBJS := $(SRCS:.c=.o)
# Output # Output

View File

@ -1,6 +1,6 @@
# EKP C Dynamic Module # 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** The division of labor: **Elisp owns all font-dependent data**
(tokenization, pixel measurement, glue values, prefix sums); the C (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.c # Emacs module entry point (emacs_module_init)
├── ekp_kp.c # Knuth-Plass DP + two-pass emergency strategy ├── ekp_kp.c # Knuth-Plass DP + two-pass emergency strategy
├── ekp_thread_pool.c # Thread pool (parallelism across paragraphs) ├── 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 └── Makefile
``` ```
Parallelism model: the DP for one paragraph is sequential (each Parallelism model: the DP for one paragraph is sequential (each
position depends on all earlier ones), so the thread pool parallelizes position depends on all earlier ones), so the thread pool parallelizes
across **paragraphs** via `ekp-c-break-batch` — the correct granularity, 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 ## Building
@ -32,10 +33,12 @@ cd ekp_c
make # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows) 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 ```bash
make DEBUG=1 # Debug build with sanitizers make DEBUG=1 # Debug build with ASan/UBSan
make clean make clean
make info make info
``` ```
@ -43,29 +46,36 @@ make info
## API (as used by ekp.el) ## API (as used by ekp.el)
```elisp ```elisp
(ekp-c-init) ; init global state + thread pool (ekp-c-init) ; init global state
(ekp-c-version) ; => "1.4" — checked by ekp-c-module-load (ekp-c-version) ; => "1.5" — checked by ekp-c-module-load
(ekp-c-thread-count) ; => 8 (ekp-c-thread-count) ; worker count (created lazily on first batch)
(ekp-c-cleanup) (ekp-c-cleanup)
;; Synced automatically by ekp.el before every call: ;; Synced automatically by ekp.el before every call:
(ekp-c-set-penalties LINE HYPHEN FITNESS LAST-RATIO (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 (ekp-c-break-with-arrays IDEAL-PREFIX MIN-PREFIX MAX-PREFIX
GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH 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) ;; => (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) (ekp-c-break-batch PARAGRAPHS) ; => vector of (BREAKS . COST)
``` ```
`LEAD-SPACES` / `TRAIL-SPACES` are the space-box run widths that the `LEAD-SPACES` / `TRAIL-SPACES` are the space-box run widths that the
Elisp renderer strips from line edges; the DP excludes them from line 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 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 — 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 unbreakable tokens can never make the result empty. Badness saturates
at 10000 exactly like the Elisp side. at 10000 exactly like the Elisp side.
### Experimental: self-contained C path Failure behavior: any allocation failure or bad argument makes the
call return nil, and ekp.el falls back to the Elisp engine — the C
`ekp-c-break-lines` tokenizes and hyphenates in C module never silently degrades to a subtly different layout.
(`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 ## Performance
@ -94,13 +94,14 @@ byte-compiled Elisp around the C calls, min of 3 cold-cache runs):
| Case | Elisp engine (compiled) | C engine | | Case | Elisp engine (compiled) | C engine |
|:----------------------------|------------------------:|---------:| |:----------------------------|------------------------:|---------:|
| justify text-zh.txt w=200 | 96 ms | 57 ms | | justify text-zh.txt w=200 | 150 ms | 41 ms |
| justify mixed text w=300 | 53 ms | 23 ms | | justify mixed text w=300 | 82 ms | 31 ms |
| range-justify zh 340380 | 294 ms | 75 ms | | range-justify zh 340380 | 529 ms | 106 ms |
| range-justify mix 280320 | 480 ms | 34 ms | | range-justify mix 280320 | 762 ms | 52 ms |
| DP only, text-zh w=400 | 15 ms | 1.3 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 The pure-DP speedup is ~12×; end-to-end gains are smaller because
smaller because tokenization, measurement and rendering stay in Elisp. tokenization, measurement and rendering stay in Elisp. The C engine
The C engine matters most for `range-justify` (many widths per text) matters most for `range-justify` (many widths per text) and
and multi-paragraph batches. 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 "ekp_module.h"
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
@ -27,25 +28,13 @@
/* Required for Emacs modules */ /* Required for Emacs modules */
int plugin_is_GPL_compatible; int plugin_is_GPL_compatible;
/* Cached Emacs environment for callbacks */ /* All pixel quantities travel as int32; clamp instead of silently
static emacs_env *current_env = NULL; * wrapping if Elisp ever hands us something absurd. */
static emacs_value measure_func = NULL; static inline int32_t clamp32(intmax_t v)
/*
* Pixel measurement callback that calls back into Emacs
*/
static int32_t emacs_measure_string(const char *text, size_t len)
{ {
if (!current_env || !measure_func) if (v > INT32_MAX) return INT32_MAX;
return len * 7; /* fallback: ~7 pixels per char */ if (v < INT32_MIN) return INT32_MIN;
return (int32_t)v;
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);
} }
/* /*
@ -77,72 +66,6 @@ static emacs_value Fekp_c_cleanup(emacs_env *env, ptrdiff_t nargs,
return env->intern(env, "t"); 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 * 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) if (!ekp_global || nargs < 4)
return env->intern(env, "nil"); return env->intern(env, "nil");
ekp_global->line_penalty = env->extract_integer(env, args[0]); ekp_global->line_penalty = clamp32(env->extract_integer(env, args[0]));
ekp_global->hyphen_penalty = env->extract_integer(env, args[1]); ekp_global->hyphen_penalty = clamp32(env->extract_integer(env, args[1]));
ekp_global->fitness_penalty = env->extract_integer(env, args[2]); ekp_global->fitness_penalty = clamp32(env->extract_integer(env, args[2]));
ekp_global->last_line_ratio = env->extract_float(env, args[3]); ekp_global->last_line_ratio = env->extract_float(env, args[3]);
if (nargs > 4) 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) if (nargs > 5)
ekp_global->last_line_short_penalty = env->extract_float(env, args[5]); ekp_global->last_line_short_penalty = env->extract_float(env, args[5]);
/* Per-line extra stretch for non-justify alignment; reset to 0 /* Per-line extra stretch for non-justify alignment; reset to 0
* when the caller omits it so stale values never leak. */ * when the caller omits it so stale values never leak. */
ekp_global->extra_stretch = 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"); 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 * 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) emacs_value *args, void *data)
{ {
(void)nargs; (void)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 */ /* Extract prefix arrays */
for (ptrdiff_t i = 0; i < prefix_len; i++) { for (ptrdiff_t i = 0; i < prefix_len; i++) {
ideal_prefix[i] = env->extract_integer(env, env->vec_get(env, args[0], i)); ideal_prefix[i] = clamp32(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)); min_prefix[i] = clamp32(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)); max_prefix[i] = clamp32(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)); lead_spaces[i] = clamp32(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)); trail_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i)));
} }
/* Extract glue arrays */ /* Extract glue arrays */
for (size_t i = 0; i < n; i++) { for (size_t i = 0; i < n; i++) {
glue_ideals[i] = env->extract_integer(env, env->vec_get(env, args[3], i)); glue_ideals[i] = clamp32(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_shrinks[i] = clamp32(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_stretches[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i)));
} }
/* Get hyphen positions vector */ /* 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)); hyph_pos = malloc(hyph_count * sizeof(int32_t));
if (hyph_pos) { if (hyph_pos) {
for (ptrdiff_t i = 0; i < hyph_count; i++) { 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 hyph_width = clamp32(env->extract_integer(env, args[7]));
int32_t line_width = env->extract_integer(env, args[8]); int32_t line_width = clamp32(env->extract_integer(env, args[8]));
/* Right-edge protrusion: per-gap array (n+1) and hyphen scalar */ /* Right-edge protrusion: per-gap array (n+1) and hyphen scalar */
int32_t *tail_pro = malloc(prefix_len * sizeof(int32_t)); int32_t *tail_pro = malloc(prefix_len * sizeof(int32_t));
if (tail_pro) { if (tail_pro) {
for (ptrdiff_t i = 0; i < prefix_len; i++) { 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 hyphen_protrude = clamp32(env->extract_integer(env, args[13]));
int32_t first_line_width = env->extract_integer(env, args[14]); int32_t first_line_width = clamp32(env->extract_integer(env, args[14]));
/* Forbidden break positions (sorted gap indices, may be empty) */ /* Forbidden break positions (sorted gap indices, may be empty) */
ptrdiff_t forb_count = env->vec_size(env, args[11]); 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)); forb_pos = malloc(forb_count * sizeof(int32_t));
if (forb_pos) { if (forb_pos) {
for (ptrdiff_t i = 0; i < forb_count; i++) { 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 */ /* Call the pure DP function */
ekp_result_t *result = ekp_break_with_prefixes( ekp_result_t *result = ekp_break_with_prefixes(
ideal_prefix, min_prefix, max_prefix, ideal_prefix, min_prefix, max_prefix,
@ -500,17 +321,17 @@ static bool extract_paragraph_data(
} }
for (ptrdiff_t i = 0; i < prefix_len; i++) { for (ptrdiff_t i = 0; i < prefix_len; i++) {
(*ideal_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[0], i)); (*ideal_prefix)[i] = clamp32(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)); (*min_prefix)[i] = clamp32(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)); (*max_prefix)[i] = clamp32(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)); (*lead_spaces)[i] = clamp32(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)); (*trail_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i)));
} }
for (size_t i = 0; i < *n; i++) { for (size_t i = 0; i < *n; i++) {
(*glue_ideals)[i] = env->extract_integer(env, env->vec_get(env, args[3], i)); (*glue_ideals)[i] = clamp32(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_shrinks)[i] = clamp32(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_stretches)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i)));
} }
*hyph_count = env->vec_size(env, args[6]); *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)); *hyph_pos = malloc(*hyph_count * sizeof(int32_t));
if (*hyph_pos) { if (*hyph_pos) {
for (ptrdiff_t i = 0; i < *hyph_count; i++) { 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]); *hyph_width = clamp32(env->extract_integer(env, args[7]));
*line_width = env->extract_integer(env, args[8]); *line_width = clamp32(env->extract_integer(env, args[8]));
*forb_count = env->vec_size(env, args[11]); *forb_count = env->vec_size(env, args[11]);
*forb_pos = NULL; *forb_pos = NULL;
@ -533,7 +354,7 @@ static bool extract_paragraph_data(
*forb_pos = malloc(*forb_count * sizeof(int32_t)); *forb_pos = malloc(*forb_count * sizeof(int32_t));
if (*forb_pos) { if (*forb_pos) {
for (ptrdiff_t i = 0; i < *forb_count; i++) { 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)); *tail_pro = malloc(prefix_len * sizeof(int32_t));
if (*tail_pro) { if (*tail_pro) {
for (ptrdiff_t i = 0; i < prefix_len; i++) { 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]); *hyphen_protrude = clamp32(env->extract_integer(env, args[13]));
*first_line_width = env->extract_integer(env, args[14]); *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; return true;
} }
@ -740,18 +576,6 @@ int emacs_module_init(struct emacs_runtime *runtime)
defun(env, "ekp-c-cleanup", 0, 0, Fekp_c_cleanup, defun(env, "ekp-c-cleanup", 0, 0, Fekp_c_cleanup,
"Cleanup EKP C module resources."); "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, defun(env, "ekp-c-set-penalties", 4, 7, Fekp_c_set_penalties,
"Set Knuth-Plass algorithm penalties.\n\n\ "Set Knuth-Plass algorithm penalties.\n\n\
LINE-PENALTY: base penalty per line break (default 10)\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 \ (fn LINE-PENALTY HYPHEN-PENALTY FITNESS-PENALTY LAST-LINE-RATIO \
&optional CONSEC-HYPHEN-PENALTY LAST-LINE-SHORT-PENALTY)"); &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, 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\ "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\ 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 * Unified DP input structure for the array-based DP core.
*/
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.
*/ */
typedef struct { typedef struct {
/* Prefix sum arrays */ /* 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) void ekp_result_destroy(ekp_result_t *r)
{ {
if (!r) if (!r)
@ -877,6 +626,10 @@ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count)
if (!results) if (!results)
return NULL; 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 */ /* Single paragraph: no point using threads */
if (count == 1 || !ekp_global || !ekp_global->pool) { if (count == 1 || !ekp_global || !ekp_global->pool) {
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
@ -946,16 +699,6 @@ int ekp_init(void)
if (!ekp_global) if (!ekp_global)
return -1; 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 */ /* Default K-P parameters */
ekp_global->line_penalty = 10; ekp_global->line_penalty = 10;
@ -965,15 +708,9 @@ int ekp_init(void)
ekp_global->consec_hyphen_penalty = 100; ekp_global->consec_hyphen_penalty = 100;
ekp_global->last_line_short_penalty = 50.0; ekp_global->last_line_short_penalty = 50.0;
/* Create thread pool */ /* The thread pool is created lazily by the first batch call:
ekp_global->pool = ekp_pool_create(EKP_THREAD_POOL_SIZE); * plain single-paragraph use never starts worker threads. */
if (!ekp_global->pool) { ekp_global->pool = NULL;
free(ekp_global);
ekp_global = NULL;
return -1;
}
pthread_mutex_init(&ekp_global->cache_lock, NULL);
return 0; return 0;
} }
@ -983,20 +720,6 @@ void ekp_cleanup(void)
if (!ekp_global) if (!ekp_global)
return; 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); ekp_pool_destroy(ekp_global->pool);
free(ekp_global); free(ekp_global);
ekp_global = NULL; ekp_global = NULL;

View File

@ -23,123 +23,17 @@
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <pthread.h> #include <pthread.h>
#include <math.h>
/* Version */ /* Version */
#define EKP_VERSION_MAJOR 1 #define EKP_VERSION_MAJOR 1
#define EKP_VERSION_MINOR 5 #define EKP_VERSION_MINOR 5
/* Limits */ /* Limits */
#define EKP_MAX_PATTERN_LEN 64 #define EKP_THREAD_POOL_MAX 64
#define EKP_MAX_WORD_LEN 256
#define EKP_CACHE_SIZE 4096
#define EKP_THREAD_POOL_SIZE 8
/* Infinity for impossible breaks */ /* Infinity for impossible breaks */
#define EKP_INFINITY 1e10 #define EKP_INFINITY HUGE_VAL /* unreachable sentinel */
/*
* 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;
/* /*
* Line break result * Line break result
@ -151,20 +45,11 @@ typedef struct {
double total_cost; double total_cost;
} ekp_result_t; } 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 * Thread pool for parallel computation
*/ */
typedef struct { typedef struct {
pthread_t threads[EKP_THREAD_POOL_SIZE]; pthread_t threads[EKP_THREAD_POOL_MAX];
size_t thread_count; size_t thread_count;
pthread_mutex_t queue_lock; pthread_mutex_t queue_lock;
pthread_cond_t queue_cond; pthread_cond_t queue_cond;
@ -186,15 +71,7 @@ typedef struct {
* Global state * Global state
*/ */
typedef struct { typedef struct {
ekp_hyphenator_t *hyphenators[32]; /* by language */ ekp_thread_pool_t *pool; /* created lazily on first batch */
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;
/* K-P parameters */ /* K-P parameters */
int line_penalty; int line_penalty;
@ -213,23 +90,14 @@ extern ekp_state_t *ekp_global;
/* /*
* API: Hyphenation * 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 * 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) * 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); void ekp_result_destroy(ekp_result_t *r);
/* /*
@ -313,6 +181,7 @@ ekp_result_t **ekp_break_batch(
/* /*
* API: Thread pool * API: Thread pool
*/ */
size_t ekp_pool_default_threads(void);
ekp_thread_pool_t *ekp_pool_create(size_t num_threads); ekp_thread_pool_t *ekp_pool_create(size_t num_threads);
void ekp_pool_destroy(ekp_thread_pool_t *pool); void ekp_pool_destroy(ekp_thread_pool_t *pool);
void ekp_pool_submit(ekp_thread_pool_t *pool, void (*func)(void *), void *arg); 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 "ekp_module.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#define QUEUE_CAPACITY 1024 #define QUEUE_CAPACITY 1024
@ -41,8 +44,12 @@ static void *worker_thread(void *arg)
/* Dequeue work */ /* Dequeue work */
void (*func)(void *) = pool->queue[pool->queue_head].func; void (*func)(void *) = pool->queue[pool->queue_head].func;
void *work_arg = pool->queue[pool->queue_head].arg; 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->queue_head = (pool->queue_head + 1) % pool->queue_size;
pool->active_count++; pool->active_count++;
if (was_full)
pthread_cond_broadcast(&pool->done_cond);
pthread_mutex_unlock(&pool->queue_lock); pthread_mutex_unlock(&pool->queue_lock);
@ -62,12 +69,25 @@ static void *worker_thread(void *arg)
return NULL; 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) ekp_thread_pool_t *ekp_pool_create(size_t num_threads)
{ {
if (num_threads == 0) if (num_threads == 0)
num_threads = EKP_THREAD_POOL_SIZE; num_threads = ekp_pool_default_threads();
if (num_threads > EKP_THREAD_POOL_SIZE) if (num_threads > EKP_THREAD_POOL_MAX)
num_threads = EKP_THREAD_POOL_SIZE; num_threads = EKP_THREAD_POOL_MAX;
ekp_thread_pool_t *pool = calloc(1, sizeof(*pool)); ekp_thread_pool_t *pool = calloc(1, sizeof(*pool));
if (!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); pthread_mutex_lock(&pool->queue_lock);
size_t next_tail = (pool->queue_tail + 1) % pool->queue_size; /* Queue full: wait for a worker to make room. Dropping the task
* here used to silently degrade the batch to the Elisp fallback
/* Queue full - drop task (shouldn't happen with proper sizing) */ * exactly when parallelism mattered most. */
if (next_tail == pool->queue_head) { 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); pthread_mutex_unlock(&pool->queue_lock);
return; return;
} }
pool->queue[pool->queue_tail].func = func; pool->queue[pool->queue_tail].func = func;
pool->queue[pool->queue_tail].arg = arg; 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_cond_signal(&pool->queue_cond);
pthread_mutex_unlock(&pool->queue_lock); pthread_mutex_unlock(&pool->queue_lock);