ekp/ekp_c/ekp_module.h
Kinneyzhang 3fc05c1405 feat: typography wave — dictionary HYPHENMIN, JIS kinsoku, fast indent
Three quality gaps closed, one of them unlocking the C engine for
the most common Chinese configuration:

- first-line indent now runs on the 1D DP and the C engine (module
  1.5: ekp-c-break-with-arrays grows a 15th arg FIRST-LINE-WIDTH).
  A line starts at box 0 exactly when i = 0, so no (position x
  line-count) state is needed; only true parshape and looseness
  still take the 2D Elisp path.  Indented Chinese sample, w=400:
  C engine 82 ms (this configuration previously bypassed C entirely
  and ran the heaviest Elisp DP).  The C-result reconstruction
  mirrors the per-line width, and an equivalence test pins
  indent == the equivalent parshape across engines.
- LEFTHYPHENMIN / RIGHTHYPHENMIN are parsed from dictionaries and
  applied (en_US declares 2/3; the hardcoded 2/2 allowed breaks
  like "gen-cy" that the dictionary forbids).  Explicit overrides
  still win; partial overrides keep the dictionary's other side.
- JIS X 4051 line-start prohibition for small kana, the prolonged
  sound mark and iteration marks (new defcustom
  ekp-cjk-no-line-start-extra, char-table backed): っょー々 can no
  longer start a line.  Pure break-permission change; spacing and
  the DP are untouched.

Also fixed in passing:
- ekp--split-with-hyphen extracted regexp groups AFTER resolving
  the hyphenator; dictionary compilation clobbers the match data
  (latent since the resolver was hoisted; exposed by the HYPHENMIN
  parser) — groups are captured first now.
- ekp--first-indent-pixel goes through the width cache; it runs
  once per rendered line and was re-measuring the reference glyph
  every time (C path was 397 ms before, 82 ms after).

4 new ERT tests (92 total); fuzz 300/300 against C 1.5.
C module must be rebuilt: make -C ekp_c clean all

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:36:52 +08:00

333 lines
9.3 KiB
C

/*
* ekp_module.h - Emacs Knuth-Plass dynamic module
*
* 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.
*
* Core data structures and API declarations.
* Design principle: Data structures are the code. Get them right,
* and the rest writes itself. -- Linus Torvalds
*/
#ifndef EKP_MODULE_H
#define EKP_MODULE_H
#include <emacs-module.h>
#include <stdint.h>
#include <stdbool.h>
#include <pthread.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
/* 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;
/*
* Line break result
*/
typedef struct {
int32_t *breaks; /* break positions */
size_t break_count;
int32_t *rest_pixels; /* remaining space per line */
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];
size_t thread_count;
pthread_mutex_t queue_lock;
pthread_cond_t queue_cond;
pthread_cond_t done_cond;
struct {
void (*func)(void *);
void *arg;
} *queue;
size_t queue_size;
size_t queue_head;
size_t queue_tail;
size_t active_count;
bool shutdown;
} ekp_thread_pool_t;
/*
* 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;
/* K-P parameters */
int line_penalty;
int hyphen_penalty;
int fitness_penalty;
double last_line_ratio;
int consec_hyphen_penalty; /* multiplier for consecutive hyphens */
double last_line_short_penalty; /* multiplier for short last lines */
int32_t extra_stretch; /* per-line flexibility for non-justify
* alignment (0 = justify) */
} ekp_state_t;
/* Global state instance */
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);
/*
* API: Pure DP with pre-computed prefix arrays (for Elisp integration)
*
* This is the preferred API when Elisp has already computed everything.
* Elisp does: tokenization, width measurement, glue computation, prefix sums.
* C module only does: O(n²) DP computation.
*
* ideal_prefix, min_prefix, max_prefix: prefix sum arrays (n+1 elements)
* glue_ideals, glue_stretches, glue_shrinks: per-box glue values (n elements)
* hyphen_positions: sorted array of hyphenable box indices
* hyphen_count: length of hyphen_positions
* hyphen_width: pixel width of hyphen character
* line_width: target line width in pixels
* lead_spaces: (n+1 elements, nullable) width of the space-box run
* starting at box i; entry 0 must be 0 (indentation kept)
* trail_spaces: (n+1 elements, nullable) width of the space-box run
* ending at box k-1
* forbidden_positions: sorted array of gap indices where a line may
* NOT end (kinsoku, no-break spans); nullable
* forbidden_count: length of forbidden_positions
*/
ekp_result_t *ekp_break_with_prefixes(
const int32_t *ideal_prefix,
const int32_t *min_prefix,
const int32_t *max_prefix,
const int32_t *glue_ideals,
const int32_t *glue_shrinks,
const int32_t *glue_stretches,
size_t n,
const int32_t *hyphen_positions,
size_t hyphen_count,
int32_t hyphen_width,
int32_t line_width,
const int32_t *lead_spaces,
const int32_t *trail_spaces,
const int32_t *forbidden_positions,
size_t forbidden_count,
const int32_t *tail_protrudes,
int32_t hyphen_protrude,
int32_t first_line_width);
/*
* Batch input for parallel processing
*/
typedef struct {
const int32_t *ideal_prefix;
const int32_t *min_prefix;
const int32_t *max_prefix;
const int32_t *glue_ideals;
const int32_t *glue_shrinks;
const int32_t *glue_stretches;
size_t n;
const int32_t *hyphen_positions;
size_t hyphen_count;
int32_t hyphen_width;
int32_t line_width;
const int32_t *lead_spaces; /* nullable, n+1 elements */
const int32_t *trail_spaces; /* nullable, n+1 elements */
const int32_t *forbidden_positions; /* nullable, sorted gap indices */
size_t forbidden_count;
const int32_t *tail_protrudes; /* nullable, n+1 elements */
int32_t hyphen_protrude;
int32_t first_line_width; /* width of line 0; <=0 = line_width */
} ekp_batch_input_t;
/*
* API: Batch line breaking (parallel across paragraphs)
*
* Processes multiple paragraphs concurrently using the thread pool.
* This is the correct parallelization granularity - paragraphs are
* independent, so no synchronization overhead.
*
* Returns array of results (caller must free each result and the array).
*/
ekp_result_t **ekp_break_batch(
ekp_batch_input_t *inputs,
size_t count);
/*
* API: Thread pool
*/
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);
void ekp_pool_wait(ekp_thread_pool_t *pool);
/*
* API: Initialization
*/
int ekp_init(void);
void ekp_cleanup(void);
/*
* Emacs module interface
*/
int emacs_module_init(struct emacs_runtime *runtime);
#endif /* EKP_MODULE_H */