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:
parent
3fc05c1405
commit
74a780ce95
18
ekp-utils.el
18
ekp-utils.el
@ -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
16
ekp.el
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 340–380 | 294 ms | 75 ms |
|
||||
| range-justify mix 280–320 | 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 340–380 | 529 ms | 106 ms |
|
||||
| range-justify mix 280–320 | 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.
|
||||
|
||||
344
ekp_c/ekp.c
344
ekp_c/ekp.c
@ -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\
|
||||
|
||||
@ -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;
|
||||
}
|
||||
293
ekp_c/ekp_kp.c
293
ekp_c/ekp_kp.c
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user