perf: make live structural commits incremental
Reuse prepared paragraph data, safe Elisp DP states, common layout lines, and dirty-island source reconstruction for exact append commits. Simplify C int32 validation and add the frozen task030 evaluator, regressions, and decision records.
This commit is contained in:
parent
83352c4571
commit
665164911f
@ -78,6 +78,10 @@
|
|||||||
27.687/27.487 ms with exact layout parity (`task032` complete;
|
27.687/27.487 ms with exact layout parity (`task032` complete;
|
||||||
`issue020` awaits user confirmation):
|
`issue020` awaits user confirmation):
|
||||||
`.phrase/phases/phase-kp-overhaul-20260726/change_c_resize_latency_20260729.md`
|
`.phrase/phases/phase-kp-overhaul-20260726/change_c_resize_latency_20260729.md`
|
||||||
|
- 2026-07-30 implement exact incremental live-append preparation, DP, and
|
||||||
|
projection reuse (`task030` implementation verified; locked
|
||||||
|
source-instrumented 16 ms gate and `issue018` remain open):
|
||||||
|
`.phrase/phases/phase-kp-overhaul-20260726/change_narrow_live_append_latency_20260730.md`
|
||||||
|
|
||||||
## phase-doc-improvement-20260125
|
## phase-doc-improvement-20260125
|
||||||
- See `.phrase/phases/phase-doc-improvement-20260125/change_log.md`
|
- See `.phrase/phases/phase-doc-improvement-20260125/change_log.md`
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
# Change: Narrow Live-Append Latency 2026-07-30
|
||||||
|
|
||||||
|
## 2026-07-30 — Implement exact incremental live commits
|
||||||
|
|
||||||
|
- **Modify** `ekp.el`: retain prepared paragraph and 1D-DP state in layout
|
||||||
|
plans, retokenize only the last incomplete word plus appended suffix,
|
||||||
|
extend prefix/break/protrusion data from the first dirty box, resume Elisp
|
||||||
|
DP from the earliest state that can reach the new tail, and reuse unchanged
|
||||||
|
layout-line records.
|
||||||
|
- **Modify** `ekp-buffer.el`: reconstruct only the live dirty island, attempt
|
||||||
|
exact append planning before a full plan, preserve cache identity, and
|
||||||
|
release the old hard-line transaction before the next paragraph becomes
|
||||||
|
live.
|
||||||
|
- **Modify** `ekp_c/ekp.c`: validate signed 32-bit values with one
|
||||||
|
`extract_integer` call instead of three Lisp predicate/comparison calls.
|
||||||
|
- **Add** exact append/fallback, multi-engine, unsafe-context,
|
||||||
|
stable-boundary, dirty-island, and invalid-C-vector regressions, plus a
|
||||||
|
frozen baseline/candidate evaluator covering five widths, four paragraph
|
||||||
|
lengths, C/Elisp, and two GC modes.
|
||||||
|
- **Performance:** the four-round source-instrumented evaluator reduces
|
||||||
|
80-pixel C p95/p99 from 114.717/119.201 ms to 25.490/25.785 ms and Elisp
|
||||||
|
from 588.017/597.093 ms to 43.860/47.578 ms. Ordinary keys remain a
|
||||||
|
zero-work path at 0.595 ms p99 and all widths avoid regression.
|
||||||
|
- **Production check:** with production files byte-compiled, three repeated
|
||||||
|
public-command runs measure append p99 at 1.158–1.326 ms for C and
|
||||||
|
1.429–1.438 ms for pure Elisp; hard-boundary p99 is 1.251–1.363 ms and
|
||||||
|
1.457–1.470 ms respectively.
|
||||||
|
- **Decision:** retain the C module. Rust would cross the same Emacs module
|
||||||
|
ABI and cannot remove the Elisp-owned tokenization, measurement,
|
||||||
|
transaction, or publication work. Rewriting the already sub-millisecond C
|
||||||
|
layer would add Cargo and cross-platform release surface without a
|
||||||
|
measured end-to-end gain.
|
||||||
|
- **Open gate:** the locked source-instrumented evaluator still exceeds its
|
||||||
|
absolute 16 ms C/Elisp target, so `task030` and `issue018` remain open even
|
||||||
|
though the byte-compiled production path is within budget.
|
||||||
@ -2,27 +2,36 @@
|
|||||||
|
|
||||||
## issue018 [ ] Unique live appends miss the frame budget at very narrow widths
|
## issue018 [ ] Unique live appends miss the frame budget at very narrow widths
|
||||||
|
|
||||||
- **Status:** Re-profiled after `task031`; `task030` is unblocked.
|
- **Status:** Exact incremental implementation verified; the locked
|
||||||
- **Summary:** Stable transactions removed per-key whole-hard-line planning.
|
source-instrumented gate remains open.
|
||||||
The synthetic 80-pixel workload now performs zero planning on ordinary
|
- **Summary:** Stable transactions removed per-key whole-hard-line planning,
|
||||||
same-row edits, but structural row-crossing commits still make append p99
|
and task030 now incrementally extends paragraph preparation, Elisp DP
|
||||||
exceed the 16 ms frame budget.
|
state, and layout lines only at structural row crossings. Byte-compiled
|
||||||
|
production paths are within the 16 ms frame budget; the deliberately
|
||||||
|
source-loaded, fully instrumented evaluator still exceeds it.
|
||||||
- **Environment:** Emacs 30.2 on macOS, C backend 1.6, benchmark width
|
- **Environment:** Emacs 30.2 on macOS, C backend 1.6, benchmark width
|
||||||
fixed at 80 px, `gc-cons-threshold` bound to
|
fixed at 80 px, `gc-cons-threshold` bound to
|
||||||
`most-positive-fixnum` so mutator work is measured without GC pauses.
|
`most-positive-fixnum` so mutator work is measured without GC pauses.
|
||||||
- **Repro:**
|
- **Repro:**
|
||||||
1. Load the C backend.
|
1. For the production-shaped check, byte-compile the four production
|
||||||
2. Run `tests/ekp-buffer-live-bench.el` with GC excluded.
|
Elisp files into a temporary package root, put that root first on
|
||||||
3. Inspect the `append` row.
|
`load-path`, and run `tests/ekp-buffer-live-bench.el` once with the C
|
||||||
|
module loaded and once with `ekp-use-c-module` nil.
|
||||||
|
2. Confirm `ekp--dp-run-1d` is byte code and inspect the `append` and
|
||||||
|
`hard-boundary` rows.
|
||||||
|
3. For the locked source/instrumentation matrix, run
|
||||||
|
`tests/run-live-commit-evaluator.sh` and inspect
|
||||||
|
`.omx/goals/performance/narrow-live-commit/latest-report.json`.
|
||||||
- **Expected vs Actual:**
|
- **Expected vs Actual:**
|
||||||
- Expected: live append p99 remains below the 16 ms interaction budget,
|
- Expected: live append p99 remains below the 16 ms interaction budget,
|
||||||
including narrow windows.
|
including narrow windows.
|
||||||
- Actual: the final task031 GC-excluded runs record only 15 permitted
|
- Actual production path: three repeated byte-compiled public-command runs
|
||||||
structural plans across 291 appends. C measures median 2.177 ms and p99
|
measure append p99 at 1.158–1.326 ms for C and 1.429–1.438 ms for pure
|
||||||
51.170 ms; Elisp measures median 2.176 ms and p99 187.499 ms. Same-row
|
Elisp. Hard-boundary p99 is 1.251–1.363 ms and 1.457–1.470 ms.
|
||||||
cache-revisit work records zero plans (C p99 1.627 ms; Elisp p99
|
- Actual locked evaluator: four interleaved source-instrumented rounds
|
||||||
1.502 ms), while point motion records zero plan/cache calls (C p99
|
measure C at 25.490/25.785 ms p95/p99 and Elisp at
|
||||||
0.017 ms; Elisp p99 0.015 ms).
|
43.860/47.578 ms. These improve 77.78/78.37% and 92.54/92.03% over the
|
||||||
|
frozen baseline but still miss the absolute 16 ms stress target.
|
||||||
- **Investigation:**
|
- **Investigation:**
|
||||||
- The task029 point-motion change is not the cause. Navigation is now a
|
- The task029 point-motion change is not the cause. Navigation is now a
|
||||||
zero-work path and does not enter planning or projection publication.
|
zero-work path and does not enter planning or projection publication.
|
||||||
@ -38,6 +47,15 @@
|
|||||||
the current performance claim.
|
the current performance claim.
|
||||||
- Both backends still miss p99 at structural boundaries, so task030 must
|
- Both backends still miss p99 at structural boundaries, so task030 must
|
||||||
evaluate both rather than optimize only the C wrapper.
|
evaluate both rather than optimize only the C wrapper.
|
||||||
|
- The frozen C wrapper measured only 2.615/2.655 ms p95/p99 and the
|
||||||
|
candidate 0.697/0.701 ms. Paragraph preparation, DP state reconstruction,
|
||||||
|
plan construction, and publication owned the end-to-end cost.
|
||||||
|
- Byte compilation removes most of the remaining pure-Elisp interpreter
|
||||||
|
and closure overhead. This explains why the production path passes while
|
||||||
|
the frozen source-instrumented stress gate remains red; neither result is
|
||||||
|
substituted for the other.
|
||||||
|
- Rust is not selected. It would use the same `emacs_env` ABI and cannot
|
||||||
|
remove Elisp-owned font measurement, transaction, or projection work.
|
||||||
- **Required Outcome:**
|
- **Required Outcome:**
|
||||||
- Diagnose and reduce unique-state narrow-width append planning cost
|
- Diagnose and reduce unique-state narrow-width append planning cost
|
||||||
without changing KP output semantics, the core DP contract, C ABI, or
|
without changing KP output semantics, the core DP contract, C ABI, or
|
||||||
@ -47,13 +65,19 @@
|
|||||||
invalidation.
|
invalidation.
|
||||||
- Preserve the task029 invariant that point-only motion performs zero
|
- Preserve the task029 invariant that point-only motion performs zero
|
||||||
plan, cache, and text-property work.
|
plan, cache, and text-property work.
|
||||||
- **Fix:** Deferred to `task030`; the prerequisite correctness replacement
|
- **Fix:** `task030` retains exact prepared paragraph/context data, extends
|
||||||
is complete. Optimize only the surviving structural-commit path.
|
property-free append tails from the last complete-word boundary, resumes
|
||||||
- **Verification:** Future work must use a reproducible width matrix,
|
pure-Elisp DP from a safe reachable state, reuses common layout lines, and
|
||||||
profiler evidence, result-equivalence tests, default-GC and GC-excluded
|
reconstructs only the live dirty source island. Unsupported contexts take
|
||||||
runs, and GUI input evidence.
|
the unchanged full path. C int32 validation now performs one extraction.
|
||||||
- **User Confirmation:** Not applicable until a performance change is
|
- **Verification:** Exact baseline/C/Elisp hashes and append-chain
|
||||||
implemented.
|
equivalence pass across 64/80/96/128/160 px, 2/4/8/16-row fixtures,
|
||||||
|
default/excluded GC, unsafe fallbacks, and randomized chains. Normal and
|
||||||
|
random-order ERT pass 199/199; 300 fuzz cases, warning-clean production/C
|
||||||
|
compilation, 9/9 C tests, release checks, and reviewed temporal GUI
|
||||||
|
evidence pass.
|
||||||
|
- **User Confirmation:** Pending; the formal source-instrumented performance
|
||||||
|
target also remains open.
|
||||||
- **Resolved At:** Unresolved.
|
- **Resolved At:** Unresolved.
|
||||||
- **Resolved By:** Pending.
|
- **Resolved By:** Pending.
|
||||||
- **Commit:** Pending.
|
- **Commit:** Pending.
|
||||||
|
|||||||
@ -590,6 +590,36 @@
|
|||||||
- Dependency: `task031` is complete. Optimize only the surviving
|
- Dependency: `task031` is complete. Optimize only the surviving
|
||||||
structural-commit path; the obsolete per-keystroke whole-hard-line path
|
structural-commit path; the obsolete per-keystroke whole-hard-line path
|
||||||
no longer exists.
|
no longer exists.
|
||||||
|
- Implementation checkpoint: layout plans now retain prepared paragraph
|
||||||
|
and context data. Exact plain-text appends retokenize only from the last
|
||||||
|
complete-word boundary, rebuild derived vectors from the first dirty box,
|
||||||
|
resume pure-Elisp 1D DP from the earliest state that can reach the new
|
||||||
|
tail, and reuse unchanged line records. The buffer reconstructs only its
|
||||||
|
dirty source island and falls back to the complete planner for every
|
||||||
|
unsupported context. C signed-int validation now uses one module
|
||||||
|
extraction instead of three Lisp predicate/comparison calls.
|
||||||
|
- Architecture decision: keep the C module. The frozen baseline places C
|
||||||
|
DP at only 2.615/2.655 ms p95/p99, and the candidate at
|
||||||
|
0.697/0.701 ms. Rust would cross the same Emacs module ABI while leaving
|
||||||
|
Elisp tokenization, measurement, transactions, and property publication
|
||||||
|
unchanged.
|
||||||
|
- Performance checkpoint: four interleaved source-instrumented rounds
|
||||||
|
reduce 80-pixel C p95/p99 from 114.717/119.201 ms to
|
||||||
|
25.490/25.785 ms (77.78/78.37%) and Elisp from 588.017/597.093 ms to
|
||||||
|
43.860/47.578 ms (92.54/92.03%). Exact parity, zero-work ordinary keys,
|
||||||
|
valid GC exclusion, conflict freedom, and all-width non-regression pass.
|
||||||
|
- Production checkpoint: with production files byte-compiled, three
|
||||||
|
repeated public-command runs measure append p99 at 1.158–1.326 ms for C
|
||||||
|
and 1.429–1.438 ms for pure Elisp; hard-boundary p99 is
|
||||||
|
1.251–1.363 ms and 1.457–1.470 ms respectively.
|
||||||
|
- Verification checkpoint: normal and random-order ERT pass 199/199;
|
||||||
|
300-case C/Elisp fuzz, portable warning-clean C build and 9/9 C tests,
|
||||||
|
warning-as-error production compilation, release checks, exact
|
||||||
|
append-chain review, and the 11-action/33-checkpoint temporal GUI run
|
||||||
|
pass.
|
||||||
|
- Remaining gate: the locked source-instrumented evaluator still exceeds
|
||||||
|
its absolute 16 ms target for C and Elisp. Keep `task030` open rather
|
||||||
|
than redefining the evaluator after observing the result.
|
||||||
|
|
||||||
- task031 [x] Replace the overloaded live frontier with committed projection,
|
- task031 [x] Replace the overloaded live frontier with committed projection,
|
||||||
a dirty edit transaction, and atomic structural commits.
|
a dirty edit transaction, and atomic structural commits.
|
||||||
|
|||||||
23
CHANGELOG.md
23
CHANGELOG.md
@ -128,14 +128,17 @@ project aims to follow [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
- The live-path benchmark records append, historical-plan reuse,
|
- Live structural commits now extend prepared paragraph data from the last
|
||||||
point-motion, and hard-boundary latency with plan/cache counters.
|
complete-word boundary, resume pure-Elisp DP from a safe retained state,
|
||||||
At the synthetic 80-pixel width, same-row editing and point motion perform
|
reuse unchanged layout lines, and reconstruct only the buffer's dirty
|
||||||
zero planning; point-motion p99 is 0.017 ms on C and 0.015 ms on Elisp.
|
source island. Three repeated byte-compiled public-command runs at 80
|
||||||
Across 291 appends, only 15 visual-row crossings invoke the planner.
|
pixels measure append p99 at 1.158–1.326 ms for C and 1.429–1.438 ms for
|
||||||
Those structural commits still exceed the 16 ms p99 frame budget on both
|
pure Elisp; same-row editing and point motion still perform zero planning.
|
||||||
backends and are tracked as `issue018`/`task030`; no stale reuse, debounce,
|
The stricter frozen source-instrumented matrix improves C p99 by 78.37% to
|
||||||
skipped publication, or global GC workaround hides the miss.
|
25.785 ms and Elisp p99 by 92.03% to 47.578 ms, but remains openly red
|
||||||
|
against its locked 16 ms absolute stress target (`issue018`/`task030`).
|
||||||
|
No stale reuse, debounce, skipped publication, or global GC workaround
|
||||||
|
hides either result.
|
||||||
- Tokenization now accumulates fragments and joins once per emitted box;
|
- Tokenization now accumulates fragments and joins once per emitted box;
|
||||||
dense hyphen insertion likewise joins original word slices once. On the
|
dense hyphen insertion likewise joins original word slices once. On the
|
||||||
1,000–8,000-character adversarial benchmark, the 8,000-character cases
|
1,000–8,000-character adversarial benchmark, the 8,000-character cases
|
||||||
@ -157,6 +160,10 @@ project aims to follow [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
### C module (1.6)
|
### C module (1.6)
|
||||||
|
|
||||||
|
- Signed 32-bit module inputs now use one `extract_integer` validation
|
||||||
|
instead of calling Lisp `integerp`, `>=`, and `<=` for every value. The
|
||||||
|
frozen 80-pixel candidate module layer measures 0.697/0.701 ms p95/p99,
|
||||||
|
down from 2.615/2.655 ms, without changing the ABI or DP output.
|
||||||
- The 15-field single and batch APIs now preflight vector shape, lengths,
|
- The 15-field single and batch APIs now preflight vector shape, lengths,
|
||||||
scalar types, and signed 32-bit input range before extraction. Caller
|
scalar types, and signed 32-bit input range before extraction. Caller
|
||||||
errors signal `ekp-c-invalid-input`; allocation/no-result still returns
|
errors signal `ekp-c-invalid-input`; allocation/no-result still returns
|
||||||
|
|||||||
@ -146,6 +146,10 @@ command and is not limited by this value."
|
|||||||
'(ekp-justified ekp-buffer--display ekp-buffer--line-prefix)
|
'(ekp-justified ekp-buffer--display ekp-buffer--line-prefix)
|
||||||
"Text properties that identify EKP's buffer projection.")
|
"Text properties that identify EKP's buffer projection.")
|
||||||
|
|
||||||
|
(defconst ekp-buffer--non-ascii-whitespace-regexp
|
||||||
|
"[\t\n\r\v\f\u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]"
|
||||||
|
"Unicode whitespace that cannot use the ASCII-space projection path.")
|
||||||
|
|
||||||
(dolist (property ekp-buffer--owned-properties)
|
(dolist (property ekp-buffer--owned-properties)
|
||||||
(setf (alist-get property text-property-default-nonsticky) t))
|
(setf (alist-get property text-property-default-nonsticky) t))
|
||||||
|
|
||||||
@ -279,25 +283,26 @@ The narrowest live window wins because text properties are buffer-wide."
|
|||||||
(setq position (1+ position)))
|
(setq position (1+ position)))
|
||||||
(= position end))))
|
(= position end))))
|
||||||
|
|
||||||
(defun ekp-buffer--unsupported-gap-p (gap base)
|
(defun ekp-buffer--unsupported-gap (plan _base)
|
||||||
"Return non-nil when GAP cannot be projected at BASE."
|
"Return the first gap in PLAN that cannot be projected."
|
||||||
(let ((start (+ base (ekp-layout-gap-source-start gap)))
|
(let ((string (ekp-layout-plan-string plan)))
|
||||||
(end (+ base (ekp-layout-gap-source-end gap))))
|
(when (string-match-p ekp-buffer--non-ascii-whitespace-regexp string)
|
||||||
(and (< start end)
|
|
||||||
(not (ekp-buffer--ascii-space-range-p start end))
|
|
||||||
(< (ekp-layout-gap-target-pixel gap)
|
|
||||||
(ekp-layout-gap-natural-pixel gap)))))
|
|
||||||
|
|
||||||
(defun ekp-buffer--unsupported-gap (plan base)
|
|
||||||
"Return the first gap in PLAN that cannot be projected at BASE."
|
|
||||||
(catch 'unsupported
|
(catch 'unsupported
|
||||||
(cl-loop
|
(cl-loop
|
||||||
for line across (ekp-layout-plan-lines plan)
|
for line across (ekp-layout-plan-lines plan)
|
||||||
do
|
do
|
||||||
(cl-loop
|
(cl-loop
|
||||||
for gap across (ekp-layout-line-gaps line)
|
for gap across (ekp-layout-line-gaps line)
|
||||||
when (ekp-buffer--unsupported-gap-p gap base)
|
for start = (ekp-layout-gap-source-start gap)
|
||||||
do (throw 'unsupported gap)))))
|
for end = (ekp-layout-gap-source-end gap)
|
||||||
|
when (and (< start end)
|
||||||
|
(< (ekp-layout-gap-target-pixel gap)
|
||||||
|
(ekp-layout-gap-natural-pixel gap)))
|
||||||
|
do
|
||||||
|
(while (and (< start end) (= (aref string start) ?\s))
|
||||||
|
(setq start (1+ start)))
|
||||||
|
(when (< start end)
|
||||||
|
(throw 'unsupported gap))))))))
|
||||||
|
|
||||||
(defun ekp-buffer--record-conflict (beg end reason)
|
(defun ekp-buffer--record-conflict (beg end reason)
|
||||||
"Record that BEG through END stayed verbatim because of REASON."
|
"Record that BEG through END stayed verbatim because of REASON."
|
||||||
@ -1078,6 +1083,21 @@ LINE-END lets insertion at the hard-line end belong to its last span."
|
|||||||
"Return BEG through END with EKP projection properties removed."
|
"Return BEG through END with EKP projection properties removed."
|
||||||
(ekp-buffer--strip-owned-from-string (buffer-substring beg end)))
|
(ekp-buffer--strip-owned-from-string (buffer-substring beg end)))
|
||||||
|
|
||||||
|
(defun ekp-buffer--current-live-source ()
|
||||||
|
"Return the current logical source by replacing only the dirty island."
|
||||||
|
(let* ((state ekp-buffer--live-state)
|
||||||
|
(edit ekp-buffer--live-edit)
|
||||||
|
(source (ekp-buffer--live-state-source state))
|
||||||
|
(start (ekp-buffer--live-edit-dirty-start edit))
|
||||||
|
(finish (ekp-buffer--live-edit-dirty-finish edit))
|
||||||
|
(dirty-beg
|
||||||
|
(marker-position (ekp-buffer--live-edit-dirty-beg edit)))
|
||||||
|
(dirty-end
|
||||||
|
(marker-position (ekp-buffer--live-edit-dirty-end edit))))
|
||||||
|
(concat (substring source 0 start)
|
||||||
|
(buffer-substring dirty-beg dirty-end)
|
||||||
|
(substring source finish))))
|
||||||
|
|
||||||
(defun ekp-buffer--live-context (width)
|
(defun ekp-buffer--live-context (width)
|
||||||
"Return non-text layout context for a live plan at WIDTH."
|
"Return non-text layout context for a live plan at WIDTH."
|
||||||
(list (ekp--dp-key width)
|
(list (ekp--dp-key width)
|
||||||
@ -1121,13 +1141,26 @@ LINE-END lets insertion at the hard-line end belong to its last span."
|
|||||||
(setcdr (nthcdr 15 ekp-buffer--live-plan-cache) nil))
|
(setcdr (nthcdr 15 ekp-buffer--live-plan-cache) nil))
|
||||||
plan)
|
plan)
|
||||||
|
|
||||||
|
(defun ekp-buffer--live-append-plan (key text width)
|
||||||
|
"Extend the committed live plan for KEY to TEXT at WIDTH."
|
||||||
|
(let ((old-key (and ekp-buffer--live-state
|
||||||
|
(ekp-buffer--live-state-key
|
||||||
|
ekp-buffer--live-state)))
|
||||||
|
(old-plan (and ekp-buffer--live-state
|
||||||
|
(ekp-buffer--live-state-plan
|
||||||
|
ekp-buffer--live-state))))
|
||||||
|
(when (and old-key old-plan
|
||||||
|
(equal (cadr key) (cadr old-key)))
|
||||||
|
(ekp-layout-plan-append old-plan text width))))
|
||||||
|
|
||||||
(defun ekp-buffer--live-plan-entry (text width)
|
(defun ekp-buffer--live-plan-entry (text width)
|
||||||
"Return the cache entry for TEXT at WIDTH."
|
"Return the cache entry for TEXT at WIDTH."
|
||||||
(let ((key (list text (ekp-buffer--live-context width))))
|
(let ((key (list text (ekp-buffer--live-context width))))
|
||||||
(cons key
|
(cons key
|
||||||
(or (ekp-buffer--live-cache-get key)
|
(or (ekp-buffer--live-cache-get key)
|
||||||
(ekp-buffer--live-cache-put
|
(ekp-buffer--live-cache-put
|
||||||
key (ekp-layout-plan text width))))))
|
key (or (ekp-buffer--live-append-plan key text width)
|
||||||
|
(ekp-layout-plan text width)))))))
|
||||||
|
|
||||||
(defun ekp-buffer--single-line-live-p (text width)
|
(defun ekp-buffer--single-line-live-p (text width)
|
||||||
"Return non-nil if TEXT is conservatively known to fit WIDTH."
|
"Return non-nil if TEXT is conservatively known to fit WIDTH."
|
||||||
@ -1415,7 +1448,11 @@ Keep BOUNDARY's semantic row natural unless COMPLETE is non-nil."
|
|||||||
ekp-buffer--live-edit)))
|
ekp-buffer--live-edit)))
|
||||||
(point))
|
(point))
|
||||||
end)))
|
end)))
|
||||||
(text (and beg end (ekp-buffer--logical-substring beg end))))
|
(text
|
||||||
|
(and beg end
|
||||||
|
(if ekp-buffer--live-edit
|
||||||
|
(ekp-buffer--current-live-source)
|
||||||
|
(ekp-buffer--logical-substring beg end)))))
|
||||||
(when (and beg end width text)
|
(when (and beg end width text)
|
||||||
(condition-case err
|
(condition-case err
|
||||||
(ekp-buffer--publish-semantic-prefix
|
(ekp-buffer--publish-semantic-prefix
|
||||||
@ -1443,25 +1480,31 @@ Keep BOUNDARY's semantic row natural unless COMPLETE is non-nil."
|
|||||||
(dolist (range (ekp-buffer--paragraph-ranges old-start active-beg))
|
(dolist (range (ekp-buffer--paragraph-ranges old-start active-beg))
|
||||||
(ekp-buffer--layout-paragraph
|
(ekp-buffer--layout-paragraph
|
||||||
(car range) (cdr range) ekp-buffer--auto-width))))
|
(car range) (cdr range) ekp-buffer--auto-width))))
|
||||||
|
(ekp-buffer--release-live-edit)
|
||||||
(ekp-buffer--activate-live-paragraph active-beg active-end)
|
(ekp-buffer--activate-live-paragraph active-beg active-end)
|
||||||
(ekp-buffer--publish-live-prefix end)
|
(ekp-buffer--publish-live-prefix end)))
|
||||||
(ekp-buffer--release-live-edit)))
|
|
||||||
|
|
||||||
(defun ekp-buffer--baseline-logical-source ()
|
(defun ekp-buffer--baseline-logical-source (&optional start finish)
|
||||||
"Return the current transaction baseline without EKP projection."
|
"Return the transaction baseline without EKP projection.
|
||||||
|
When START and FINISH are non-nil, copy only that source interval."
|
||||||
|
(let ((source (ekp-buffer--live-edit-baseline-source
|
||||||
|
ekp-buffer--live-edit)))
|
||||||
(ekp-buffer--strip-owned-from-string
|
(ekp-buffer--strip-owned-from-string
|
||||||
(copy-sequence
|
(if start
|
||||||
(ekp-buffer--live-edit-baseline-source ekp-buffer--live-edit))))
|
(substring source start finish)
|
||||||
|
(copy-sequence source)))))
|
||||||
|
|
||||||
(defun ekp-buffer--live-baseline-restored-p ()
|
(defun ekp-buffer--live-baseline-restored-p ()
|
||||||
"Return non-nil when source and foreign properties match the baseline."
|
"Return non-nil when source and foreign properties match the baseline."
|
||||||
(let* ((state ekp-buffer--live-state)
|
(let* ((edit ekp-buffer--live-edit)
|
||||||
(beg (marker-position (ekp-buffer--live-state-beg state)))
|
(beg (marker-position (ekp-buffer--live-edit-dirty-beg edit)))
|
||||||
(end (marker-position (ekp-buffer--live-state-end state))))
|
(end (marker-position (ekp-buffer--live-edit-dirty-end edit)))
|
||||||
|
(start (ekp-buffer--live-edit-dirty-start edit))
|
||||||
|
(finish (ekp-buffer--live-edit-dirty-finish edit)))
|
||||||
(and beg end
|
(and beg end
|
||||||
(equal-including-properties
|
(equal-including-properties
|
||||||
(ekp-buffer--logical-substring beg end)
|
(buffer-substring beg end)
|
||||||
(ekp-buffer--baseline-logical-source)))))
|
(ekp-buffer--baseline-logical-source start finish)))))
|
||||||
|
|
||||||
(defun ekp-buffer--baseline-owned-properties (position)
|
(defun ekp-buffer--baseline-owned-properties (position)
|
||||||
"Return EKP-owned properties at baseline string POSITION."
|
"Return EKP-owned properties at baseline string POSITION."
|
||||||
|
|||||||
701
ekp.el
701
ekp.el
@ -260,7 +260,7 @@ when non-zero the C module is bypassed automatically."
|
|||||||
|
|
||||||
(cl-defstruct (ekp-layout-plan (:constructor ekp-layout-plan--create))
|
(cl-defstruct (ekp-layout-plan (:constructor ekp-layout-plan--create))
|
||||||
"Semantic KP layout independent of any output representation."
|
"Semantic KP layout independent of any output representation."
|
||||||
string line-pixel boxes offsets lines)
|
string line-pixel context para boxes offsets lines)
|
||||||
|
|
||||||
(defvar ekp--para-cache nil
|
(defvar ekp--para-cache nil
|
||||||
"Cache: equal-keyed table, content key → ekp-para struct.")
|
"Cache: equal-keyed table, content key → ekp-para struct.")
|
||||||
@ -834,9 +834,23 @@ MEASURE is the paragraph measure passed to the justify call.
|
|||||||
(cons 0 measure)))
|
(cons 0 measure)))
|
||||||
(t (cons 0 measure))))
|
(t (cons 0 measure))))
|
||||||
|
|
||||||
|
(defun ekp--glue-params-snapshot ()
|
||||||
|
"Return current paragraph glue parameters as a stable plist."
|
||||||
|
(let ((justify (eq ekp-alignment 'justify)))
|
||||||
|
(list :lws-ideal ekp-lws-ideal-pixel
|
||||||
|
:lws-stretch (if justify ekp-lws-stretch-pixel 0)
|
||||||
|
:lws-shrink (if justify ekp-lws-shrink-pixel 0)
|
||||||
|
:mws-ideal ekp-mws-ideal-pixel
|
||||||
|
:mws-stretch (if justify ekp-mws-stretch-pixel 0)
|
||||||
|
:mws-shrink (if justify ekp-mws-shrink-pixel 0)
|
||||||
|
:cws-ideal ekp-cws-ideal-pixel
|
||||||
|
:cws-stretch (if justify ekp-cws-stretch-pixel 0)
|
||||||
|
:cws-shrink (if justify ekp-cws-shrink-pixel 0)
|
||||||
|
:alignment ekp-alignment
|
||||||
|
:extra-stretch (if justify 0 (ekp--ragged-extra-stretch)))))
|
||||||
|
|
||||||
(defun ekp--make-para (string)
|
(defun ekp--make-para (string)
|
||||||
"Create and fully initialize `ekp-para' struct for STRING.
|
"Create and fully initialize `ekp-para' for STRING."
|
||||||
Computes ALL data in one pass: text, params, and prefix arrays."
|
|
||||||
;; Ensure params: explicit params persist; otherwise derive defaults
|
;; Ensure params: explicit params persist; otherwise derive defaults
|
||||||
;; from this string's font.
|
;; from this string's font.
|
||||||
(unless (and ekp--params-explicit (ekp--params-set-p))
|
(unless (and ekp--params-explicit (ekp--params-set-p))
|
||||||
@ -983,21 +997,286 @@ Computes ALL data in one pass: text, params, and prefix arrays."
|
|||||||
:forbidden-positions (vconcat (nreverse forbidden))
|
:forbidden-positions (vconcat (nreverse forbidden))
|
||||||
:tail-protrudes tail-protrudes
|
:tail-protrudes tail-protrudes
|
||||||
:hyphen-protrude hyphen-protrude
|
:hyphen-protrude hyphen-protrude
|
||||||
:glue-params (let ((justify (eq ekp-alignment 'justify)))
|
:glue-params (ekp--glue-params-snapshot)
|
||||||
(list :lws-ideal ekp-lws-ideal-pixel
|
|
||||||
:lws-stretch (if justify ekp-lws-stretch-pixel 0)
|
|
||||||
:lws-shrink (if justify ekp-lws-shrink-pixel 0)
|
|
||||||
:mws-ideal ekp-mws-ideal-pixel
|
|
||||||
:mws-stretch (if justify ekp-mws-stretch-pixel 0)
|
|
||||||
:mws-shrink (if justify ekp-mws-shrink-pixel 0)
|
|
||||||
:cws-ideal ekp-cws-ideal-pixel
|
|
||||||
:cws-stretch (if justify ekp-cws-stretch-pixel 0)
|
|
||||||
:cws-shrink (if justify ekp-cws-shrink-pixel 0)
|
|
||||||
:alignment ekp-alignment
|
|
||||||
:extra-stretch (if justify 0
|
|
||||||
(ekp--ragged-extra-stretch))))
|
|
||||||
:dp-cache (make-hash-table :test 'equal :size 20))))
|
:dp-cache (make-hash-table :test 'equal :size 20))))
|
||||||
|
|
||||||
|
(defun ekp--append-prefix-vector (prefix count suffix)
|
||||||
|
"Return PREFIX through COUNT followed by SUFFIX as a vector."
|
||||||
|
(vconcat (cl-subseq prefix 0 count) suffix))
|
||||||
|
|
||||||
|
(defun ekp--append-hyphen-positions (para stable tail-positions)
|
||||||
|
"Merge PARA hyphen positions before STABLE with TAIL-POSITIONS."
|
||||||
|
(vconcat
|
||||||
|
(seq-filter
|
||||||
|
(lambda (position) (< position stable))
|
||||||
|
(ekp-para-hyphen-positions para))
|
||||||
|
(mapcar
|
||||||
|
(lambda (position) (+ stable position))
|
||||||
|
(append tail-positions nil))))
|
||||||
|
|
||||||
|
(defun ekp--append-offsets (prefix stable tail-offsets cutoff)
|
||||||
|
"Merge PREFIX offsets before STABLE with TAIL-OFFSETS at CUTOFF."
|
||||||
|
(vconcat
|
||||||
|
(cl-subseq prefix 0 stable)
|
||||||
|
(mapcar
|
||||||
|
(lambda (range)
|
||||||
|
(cons (+ cutoff (car range)) (+ cutoff (cdr range))))
|
||||||
|
(append tail-offsets nil))))
|
||||||
|
|
||||||
|
(defun ekp--gap-natural-at (string boxes offsets right)
|
||||||
|
"Return STRING's natural gap width before box RIGHT in BOXES and OFFSETS."
|
||||||
|
(let* ((left (1- right))
|
||||||
|
(start (cdr (aref offsets left)))
|
||||||
|
(end (car (aref offsets right)))
|
||||||
|
(source (if (< start end)
|
||||||
|
(substring string start end)
|
||||||
|
(car (last (string-glyph-split
|
||||||
|
(aref boxes left)))))))
|
||||||
|
(ekp--measured-width source)))
|
||||||
|
|
||||||
|
(defun ekp--append-gap-naturals (para string boxes offsets stable)
|
||||||
|
"Reuse PARA gaps before STABLE and measure STRING's BOXES via OFFSETS."
|
||||||
|
(let* ((old (or (ekp-para-gap-naturals-memo para)
|
||||||
|
(ekp--gap-natural-pixels
|
||||||
|
para (ekp-para-box-offsets-memo para))))
|
||||||
|
(naturals
|
||||||
|
(vconcat (cl-subseq old 0 stable)
|
||||||
|
(make-vector (- (length boxes) stable) 0))))
|
||||||
|
(cl-loop for right from (max 1 stable) below (length boxes)
|
||||||
|
do (aset naturals right
|
||||||
|
(ekp--gap-natural-at string boxes offsets right)))
|
||||||
|
naturals))
|
||||||
|
|
||||||
|
(defun ekp--append-cutoff (old string)
|
||||||
|
"Return an append-safe source cutoff from OLD into STRING, or nil."
|
||||||
|
(when (and (< (length old) (length string))
|
||||||
|
(null (ekp--key-intervals old))
|
||||||
|
(null (ekp--key-intervals string))
|
||||||
|
(not (string-match-p "[\n\t]" string))
|
||||||
|
(string-prefix-p old string))
|
||||||
|
(let ((tail (1- (length old))))
|
||||||
|
(while (and (>= tail 0) (= (aref old tail) ?\s))
|
||||||
|
(setq tail (1- tail)))
|
||||||
|
(when (>= tail 0)
|
||||||
|
(when-let ((space
|
||||||
|
(cl-position ?\s old :from-end t :end (1+ tail))))
|
||||||
|
(1+ space))))))
|
||||||
|
|
||||||
|
(defun ekp--append-stable-box-count (offsets cutoff)
|
||||||
|
"Return the box index in OFFSETS beginning at CUTOFF."
|
||||||
|
(let ((position (1- (length offsets)))
|
||||||
|
found)
|
||||||
|
(while (and (>= position 0) (not found))
|
||||||
|
(if (= (car (aref offsets position)) cutoff)
|
||||||
|
(setq found position)
|
||||||
|
(setq position (1- position))))
|
||||||
|
found))
|
||||||
|
|
||||||
|
(defun ekp--copy-vector-prefix (source length count initial)
|
||||||
|
"Return LENGTH vector initialized from SOURCE's first COUNT entries."
|
||||||
|
(vconcat (cl-subseq (if (bool-vector-p source)
|
||||||
|
(vconcat source)
|
||||||
|
source)
|
||||||
|
0 count)
|
||||||
|
(make-vector (- length count) initial)))
|
||||||
|
|
||||||
|
(defun ekp--append-glue-types (para boxes types hyphens stable)
|
||||||
|
"Extend PARA glue types for BOXES from STABLE using TYPES and HYPHENS."
|
||||||
|
(let ((glues (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-glues-types para) (length boxes) stable nil)))
|
||||||
|
(dolist (position (append hyphens nil))
|
||||||
|
(when (>= position (1- stable))
|
||||||
|
(aset glues (1+ position) 'nws)))
|
||||||
|
(cl-loop for index from stable below (length boxes)
|
||||||
|
unless (aref glues index)
|
||||||
|
do (aset glues index
|
||||||
|
(ekp--glue-type
|
||||||
|
(and (> index 0) (aref types (1- index)))
|
||||||
|
(aref types index))))
|
||||||
|
(cl-loop for position from stable below (length boxes)
|
||||||
|
when (ekp--append-break-forbidden-p boxes types position)
|
||||||
|
do (aset glues position 'nws))
|
||||||
|
glues))
|
||||||
|
|
||||||
|
(defun ekp--append-prefix-data (para stable widths types glues)
|
||||||
|
"Extend PARA prefix data from STABLE using WIDTHS, TYPES, and GLUES."
|
||||||
|
(let* ((n (length widths))
|
||||||
|
(prefix-count (1+ stable))
|
||||||
|
(ideal (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-ideal-prefixs para) (1+ n) prefix-count 0))
|
||||||
|
(minimum (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-min-prefixs para) (1+ n) prefix-count 0))
|
||||||
|
(maximum (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-max-prefixs para) (1+ n) prefix-count 0))
|
||||||
|
(g-ideal (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-glue-ideals para) n stable 0))
|
||||||
|
(g-shrink (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-glue-shrinks para) n stable 0))
|
||||||
|
(g-stretch (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-glue-stretches para) n stable 0))
|
||||||
|
(lws (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-lws-prefixs para) (1+ n) prefix-count 0))
|
||||||
|
(mws (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-mws-prefixs para) (1+ n) prefix-count 0))
|
||||||
|
(cws (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-cws-prefixs para) (1+ n) prefix-count 0))
|
||||||
|
(lead (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-lead-spaces para) (1+ n) stable 0))
|
||||||
|
(trail (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-trail-spaces para) (1+ n) prefix-count 0)))
|
||||||
|
(cl-loop for index from stable below n do
|
||||||
|
(let* ((width (aref widths index))
|
||||||
|
(type (aref glues index))
|
||||||
|
(gi (ekp-glue-ideal-pixel type))
|
||||||
|
(gmin (if (eq ekp-alignment 'justify)
|
||||||
|
(ekp-glue-min-pixel type) gi))
|
||||||
|
(gmax (if (eq ekp-alignment 'justify)
|
||||||
|
(ekp-glue-max-pixel type) gi)))
|
||||||
|
(aset g-ideal index gi)
|
||||||
|
(aset g-shrink index (- gi gmin))
|
||||||
|
(aset g-stretch index (- gmax gi))
|
||||||
|
(aset ideal (1+ index)
|
||||||
|
(+ (aref ideal index) width gi))
|
||||||
|
(aset minimum (1+ index)
|
||||||
|
(+ (aref minimum index) width gmin))
|
||||||
|
(aset maximum (1+ index)
|
||||||
|
(+ (aref maximum index) width gmax))
|
||||||
|
(aset lws (1+ index)
|
||||||
|
(+ (aref lws index) (if (eq type 'lws) 1 0)))
|
||||||
|
(aset mws (1+ index)
|
||||||
|
(+ (aref mws index) (if (eq type 'mws) 1 0)))
|
||||||
|
(aset cws (1+ index)
|
||||||
|
(+ (aref cws index) (if (eq type 'cws) 1 0)))
|
||||||
|
(aset trail (1+ index)
|
||||||
|
(if (ekp--space-box-type-p (aref types index))
|
||||||
|
(+ (aref trail index) width) 0))))
|
||||||
|
(cl-loop for index downfrom (1- n) to stable
|
||||||
|
do (aset lead index
|
||||||
|
(if (ekp--space-box-type-p (aref types index))
|
||||||
|
(+ (aref widths index) (aref lead (1+ index)))
|
||||||
|
0)))
|
||||||
|
(aset lead 0 0)
|
||||||
|
(vector ideal minimum maximum g-ideal g-shrink g-stretch
|
||||||
|
lws mws cws lead trail)))
|
||||||
|
|
||||||
|
(defun ekp--append-break-forbidden-p (boxes types position)
|
||||||
|
"Return non-nil when BOXES of TYPES may not break at POSITION."
|
||||||
|
(let* ((previous (aref boxes (1- position)))
|
||||||
|
(current (aref boxes position))
|
||||||
|
(previous-last (aref previous (1- (length previous))))
|
||||||
|
(current-first (aref current 0)))
|
||||||
|
(or (ekp--box-no-line-end-p previous (aref types (1- position)))
|
||||||
|
(ekp--box-no-line-start-p current (aref types position))
|
||||||
|
(memq previous-last ekp--no-break-joiner-chars)
|
||||||
|
(memq current-first ekp--no-break-joiner-chars))))
|
||||||
|
|
||||||
|
(defun ekp--append-break-data (para boxes types stable)
|
||||||
|
"Extend PARA break permissions for BOXES of TYPES from STABLE."
|
||||||
|
(let* ((n (length boxes))
|
||||||
|
(breaks (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-breaks-allowed para)
|
||||||
|
(1+ n) stable t))
|
||||||
|
(forbidden
|
||||||
|
(seq-filter
|
||||||
|
(lambda (position) (< position stable))
|
||||||
|
(ekp-para-forbidden-positions para))))
|
||||||
|
(cl-loop for position from stable below n
|
||||||
|
when (ekp--append-break-forbidden-p boxes types position)
|
||||||
|
do (aset breaks position nil)
|
||||||
|
and do (push position forbidden))
|
||||||
|
(cons breaks (vconcat (sort (append forbidden nil) #'<)))))
|
||||||
|
|
||||||
|
(defun ekp--append-tail-protrudes (para boxes types stable)
|
||||||
|
"Extend PARA tail protrusions for BOXES of TYPES from STABLE."
|
||||||
|
(let* ((n (length boxes))
|
||||||
|
(tail (ekp--copy-vector-prefix
|
||||||
|
(ekp-para-tail-protrudes para) (1+ n) (1+ stable) 0)))
|
||||||
|
(when ekp-protrusion
|
||||||
|
(cl-loop for position from (1+ stable) to n
|
||||||
|
for index = (1- position)
|
||||||
|
do (aset tail position
|
||||||
|
(if (ekp--space-box-type-p (aref types index))
|
||||||
|
(aref tail (1- position))
|
||||||
|
(ekp--tail-protrude-pixel
|
||||||
|
(aref boxes index) (aref types index))))))
|
||||||
|
tail))
|
||||||
|
|
||||||
|
(defun ekp--append-para-record
|
||||||
|
(para string stable boxes widths types glues hyphens offsets gaps)
|
||||||
|
"Extend PARA with STRING after STABLE.
|
||||||
|
Use BOXES, WIDTHS, TYPES, GLUES, HYPHENS, OFFSETS, and GAPS."
|
||||||
|
(let* ((prefix (ekp--append-prefix-data
|
||||||
|
para stable widths types glues))
|
||||||
|
(breaks (ekp--append-break-data para boxes types stable))
|
||||||
|
(extended (copy-ekp-para para)))
|
||||||
|
(setf (ekp-para-string extended) string
|
||||||
|
(ekp-para-boxes extended) boxes
|
||||||
|
(ekp-para-boxes-widths extended) widths
|
||||||
|
(ekp-para-boxes-types extended) types
|
||||||
|
(ekp-para-glues-types extended) glues
|
||||||
|
(ekp-para-hyphen-positions extended) hyphens
|
||||||
|
(ekp-para-ideal-prefixs extended) (aref prefix 0)
|
||||||
|
(ekp-para-min-prefixs extended) (aref prefix 1)
|
||||||
|
(ekp-para-max-prefixs extended) (aref prefix 2)
|
||||||
|
(ekp-para-glue-ideals extended) (aref prefix 3)
|
||||||
|
(ekp-para-glue-shrinks extended) (aref prefix 4)
|
||||||
|
(ekp-para-glue-stretches extended) (aref prefix 5)
|
||||||
|
(ekp-para-lws-prefixs extended) (aref prefix 6)
|
||||||
|
(ekp-para-mws-prefixs extended) (aref prefix 7)
|
||||||
|
(ekp-para-cws-prefixs extended) (aref prefix 8)
|
||||||
|
(ekp-para-lead-spaces extended) (aref prefix 9)
|
||||||
|
(ekp-para-trail-spaces extended) (aref prefix 10)
|
||||||
|
(ekp-para-breaks-allowed extended) (car breaks)
|
||||||
|
(ekp-para-forbidden-positions extended) (cdr breaks)
|
||||||
|
(ekp-para-tail-protrudes extended)
|
||||||
|
(ekp--append-tail-protrudes para boxes types stable)
|
||||||
|
(ekp-para-box-offsets-memo extended) offsets
|
||||||
|
(ekp-para-gap-naturals-memo extended) gaps
|
||||||
|
(ekp-para-dp-cache extended)
|
||||||
|
(make-hash-table :test 'equal :size 20))
|
||||||
|
extended))
|
||||||
|
|
||||||
|
(defun ekp--append-para (para string)
|
||||||
|
"Return (NEW-PARA . STABLE-BOXES) for plain STRING appended to PARA.
|
||||||
|
Return nil when the tokenizer prefix cannot be reused exactly."
|
||||||
|
(let* ((old (ekp-para-string para))
|
||||||
|
(cutoff (and (> (length old) 0)
|
||||||
|
(ekp--append-cutoff old string)))
|
||||||
|
(fonts-stable
|
||||||
|
(and cutoff
|
||||||
|
(equal (ekp-para-latin-font para)
|
||||||
|
(ekp-latin-font string))
|
||||||
|
(equal (ekp-para-cjk-font para)
|
||||||
|
(ekp-cjk-font string))))
|
||||||
|
(old-offsets (ekp-para-box-offsets-memo para))
|
||||||
|
(stable (and fonts-stable old-offsets
|
||||||
|
(ekp--append-stable-box-count old-offsets cutoff))))
|
||||||
|
(when (and stable (> stable 0))
|
||||||
|
(let* ((tail (substring string cutoff))
|
||||||
|
(split (ekp--split-with-hyphen tail))
|
||||||
|
(tail-boxes (car split))
|
||||||
|
(boxes (ekp--append-prefix-vector
|
||||||
|
(ekp-para-boxes para) stable tail-boxes))
|
||||||
|
(widths (ekp--append-prefix-vector
|
||||||
|
(ekp-para-boxes-widths para) stable
|
||||||
|
(ekp--measure-boxes tail-boxes t)))
|
||||||
|
(types (ekp--append-prefix-vector
|
||||||
|
(ekp-para-boxes-types para) stable
|
||||||
|
(vconcat (mapcar #'ekp--box-type tail-boxes))))
|
||||||
|
(hyphens (ekp--append-hyphen-positions para stable (cdr split)))
|
||||||
|
(glues (ekp--append-glue-types
|
||||||
|
para boxes types hyphens stable))
|
||||||
|
(offsets (ekp--append-offsets
|
||||||
|
old-offsets stable
|
||||||
|
(ekp--box-offsets tail (append tail-boxes nil))
|
||||||
|
cutoff))
|
||||||
|
(gaps (ekp--append-gap-naturals
|
||||||
|
para string boxes offsets stable)))
|
||||||
|
(cons (ekp--append-para-record
|
||||||
|
para string stable boxes widths types glues
|
||||||
|
hyphens offsets gaps)
|
||||||
|
stable)))))
|
||||||
|
|
||||||
(defun ekp--get-para (string)
|
(defun ekp--get-para (string)
|
||||||
"Get or create `ekp-para' struct for STRING.
|
"Get or create `ekp-para' struct for STRING.
|
||||||
This is the main entry point for cached paragraph data."
|
This is the main entry point for cached paragraph data."
|
||||||
@ -1180,12 +1459,84 @@ pass, where line 0 starts at box 0."
|
|||||||
(aset v (aref hyphen-positions j) t))
|
(aset v (aref hyphen-positions j) t))
|
||||||
v))
|
v))
|
||||||
|
|
||||||
(defun ekp--dp-run-1d (para line-pixel allow-emergency)
|
(defun ekp--dp-state-array (length previous stable index initial)
|
||||||
|
"Return LENGTH array reusing PREVIOUS INDEX through STABLE."
|
||||||
|
(if previous
|
||||||
|
(ekp--copy-vector-prefix
|
||||||
|
(aref previous index) length (1+ stable) initial)
|
||||||
|
(make-vector length initial)))
|
||||||
|
|
||||||
|
(defun ekp--dp-last-allowed-break (breaks end)
|
||||||
|
"Return the last permitted break in BREAKS at or before END."
|
||||||
|
(let ((position end))
|
||||||
|
(while (and (> position 0) (not (aref breaks position)))
|
||||||
|
(setq position (1- position)))
|
||||||
|
position))
|
||||||
|
|
||||||
|
(defun ekp--dp-first-new-break (para stable)
|
||||||
|
"Return PARA's first permitted break after STABLE."
|
||||||
|
(let ((breaks (ekp-para-breaks-allowed para))
|
||||||
|
(position (1+ stable))
|
||||||
|
(end (length (ekp-para-boxes para))))
|
||||||
|
(while (and (< position end) (not (aref breaks position)))
|
||||||
|
(setq position (1+ position)))
|
||||||
|
position))
|
||||||
|
|
||||||
|
(defun ekp--dp-line-too-long-p (para start end line-pixel)
|
||||||
|
"Return non-nil when PARA's START..END cannot fit LINE-PIXEL."
|
||||||
|
(let* ((hyphen-p (ekp--hyphenate-p
|
||||||
|
(ekp-para-hyphen-positions para) (1- end)))
|
||||||
|
(hyphen-width (if hyphen-p (ekp-para-hyphen-pixel para) 0))
|
||||||
|
(ideal (ekp--line-ideal-pixel para start end))
|
||||||
|
(minimum-prefix (ekp-para-min-prefixs para))
|
||||||
|
(glue-ideal (aref (ekp-para-glue-ideals para) start))
|
||||||
|
(glue-min (- glue-ideal
|
||||||
|
(aref (ekp-para-glue-shrinks para) start)))
|
||||||
|
(raw-ideal (- (aref (ekp-para-ideal-prefixs para) end)
|
||||||
|
(aref (ekp-para-ideal-prefixs para) start)
|
||||||
|
glue-ideal))
|
||||||
|
(space-width (ekp--line-stripped-space-pixel
|
||||||
|
raw-ideal start end
|
||||||
|
(ekp-para-lead-spaces para)
|
||||||
|
(ekp-para-trail-spaces para)))
|
||||||
|
(minimum (+ (- (aref minimum-prefix end)
|
||||||
|
(aref minimum-prefix start)
|
||||||
|
glue-min space-width)
|
||||||
|
hyphen-width))
|
||||||
|
(target (+ (if (= start 0)
|
||||||
|
(cdr (ekp--line-spec para 0 line-pixel))
|
||||||
|
line-pixel)
|
||||||
|
(if hyphen-p
|
||||||
|
(ekp-para-hyphen-protrude para)
|
||||||
|
(aref (ekp-para-tail-protrudes para) end)))))
|
||||||
|
(or (> minimum target)
|
||||||
|
(and (= end (length (ekp-para-boxes para)))
|
||||||
|
(> ideal target)))))
|
||||||
|
|
||||||
|
(defun ekp--dp-reused-start (para stable line-pixel)
|
||||||
|
"Return PARA's earliest state before STABLE reaching LINE-PIXEL's tail."
|
||||||
|
(let ((end (ekp--dp-first-new-break para stable))
|
||||||
|
(low 0)
|
||||||
|
(high stable))
|
||||||
|
(while (< low high)
|
||||||
|
(let ((middle (/ (+ low high) 2)))
|
||||||
|
(if (ekp--dp-line-too-long-p
|
||||||
|
para middle end line-pixel)
|
||||||
|
(setq low (1+ middle))
|
||||||
|
(setq high middle))))
|
||||||
|
low))
|
||||||
|
|
||||||
|
(defun ekp--dp-run-1d
|
||||||
|
(para line-pixel allow-emergency &optional previous-state stable-end)
|
||||||
"One strict (or emergency-permitting) K-P DP pass over PARA at LINE-PIXEL.
|
"One strict (or emergency-permitting) K-P DP pass over PARA at LINE-PIXEL.
|
||||||
Returns the dp-result plist, or nil when the paragraph end is
|
Returns the dp-result plist, or nil when the paragraph end is
|
||||||
unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
unreachable (only possible when ALLOW-EMERGENCY is nil).
|
||||||
|
PREVIOUS-STATE may reuse exact states through STABLE-END."
|
||||||
(let* ((boxes (ekp-para-boxes para))
|
(let* ((boxes (ekp-para-boxes para))
|
||||||
(n (length boxes))
|
(n (length boxes))
|
||||||
|
(reuse (and previous-state stable-end
|
||||||
|
(eq (aref previous-state 6) allow-emergency)))
|
||||||
|
(stable (if reuse (min stable-end n) 0))
|
||||||
(hyphen-pixel (ekp-para-hyphen-pixel para))
|
(hyphen-pixel (ekp-para-hyphen-pixel para))
|
||||||
(hyph-flags (ekp--hyphen-flags
|
(hyph-flags (ekp--hyphen-flags
|
||||||
(ekp-para-hyphen-positions para) n))
|
(ekp-para-hyphen-positions para) n))
|
||||||
@ -1201,6 +1552,8 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(lead-spaces (ekp-para-lead-spaces para))
|
(lead-spaces (ekp-para-lead-spaces para))
|
||||||
(trail-spaces (ekp-para-trail-spaces para))
|
(trail-spaces (ekp-para-trail-spaces para))
|
||||||
(breaks-ok (ekp-para-breaks-allowed para))
|
(breaks-ok (ekp-para-breaks-allowed para))
|
||||||
|
(last-reused-break
|
||||||
|
(and reuse (ekp--dp-last-allowed-break breaks-ok stable)))
|
||||||
(tail-protrudes (ekp-para-tail-protrudes para))
|
(tail-protrudes (ekp-para-tail-protrudes para))
|
||||||
(hyphen-protrude (ekp-para-hyphen-protrude para))
|
(hyphen-protrude (ekp-para-hyphen-protrude para))
|
||||||
;; First-line indent shrinks line 0 only; a line starts at
|
;; First-line indent shrinks line 0 only; a line starts at
|
||||||
@ -1215,14 +1568,25 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(mws-shrink (plist-get params :mws-shrink))
|
(mws-shrink (plist-get params :mws-shrink))
|
||||||
(cws-shrink (plist-get params :cws-shrink))
|
(cws-shrink (plist-get params :cws-shrink))
|
||||||
(extra-stretch (or (plist-get params :extra-stretch) 0))
|
(extra-stretch (or (plist-get params :extra-stretch) 0))
|
||||||
(backptrs (make-vector (1+ n) nil))
|
(reused-state (and reuse previous-state))
|
||||||
(demerits (make-vector (1+ n) nil))
|
(backptrs (ekp--dp-state-array
|
||||||
(rests (make-vector (1+ n) nil))
|
(1+ n) reused-state stable 0 nil))
|
||||||
(gaps (make-vector (1+ n) nil))
|
(demerits (ekp--dp-state-array
|
||||||
(hyphen-counts (make-vector (1+ n) 0))
|
(1+ n) reused-state stable 1 nil))
|
||||||
(fitness-classes (make-vector (1+ n) 1)))
|
(rests (ekp--dp-state-array
|
||||||
|
(1+ n) reused-state stable 2 nil))
|
||||||
|
(gaps (ekp--dp-state-array
|
||||||
|
(1+ n) reused-state stable 3 nil))
|
||||||
|
(hyphen-counts (ekp--dp-state-array
|
||||||
|
(1+ n) reused-state stable 4 0))
|
||||||
|
(fitness-classes (ekp--dp-state-array
|
||||||
|
(1+ n) reused-state stable 5 1)))
|
||||||
(aset demerits 0 0.0)
|
(aset demerits 0 0.0)
|
||||||
(dotimes (i n)
|
(let ((iteration-start
|
||||||
|
(if reuse
|
||||||
|
(ekp--dp-reused-start para stable line-pixel)
|
||||||
|
0)))
|
||||||
|
(cl-loop for i from iteration-start below n do
|
||||||
(when (aref demerits i)
|
(when (aref demerits i)
|
||||||
(let* ((prev-dem (aref demerits i))
|
(let* ((prev-dem (aref demerits i))
|
||||||
(prev-hyphen-count (aref hyphen-counts i))
|
(prev-hyphen-count (aref hyphen-counts i))
|
||||||
@ -1233,8 +1597,12 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(lead-glue-ideal (aref glue-ideals i))
|
(lead-glue-ideal (aref glue-ideals i))
|
||||||
(lead-glue-min (- lead-glue-ideal (aref glue-shrinks i)))
|
(lead-glue-min (- lead-glue-ideal (aref glue-shrinks i)))
|
||||||
(lead-glue-max (+ lead-glue-ideal (aref glue-stretches i)))
|
(lead-glue-max (+ lead-glue-ideal (aref glue-stretches i)))
|
||||||
(saw-allowed nil)
|
(saw-allowed
|
||||||
(k (1+ i)))
|
(and reuse (< i stable)
|
||||||
|
(> last-reused-break i)))
|
||||||
|
(k (if (and reuse (< i stable))
|
||||||
|
(1+ stable)
|
||||||
|
(1+ i))))
|
||||||
(catch 'break
|
(catch 'break
|
||||||
(while (<= k n)
|
(while (<= k n)
|
||||||
(if (not (or (= k n) (aref breaks-ok k)))
|
(if (not (or (= k n) (aref breaks-ok k)))
|
||||||
@ -1355,7 +1723,7 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
prev-hyphen-count
|
prev-hyphen-count
|
||||||
(unless single-box (ekp--gaps-between para i k)))))
|
(unless single-box (ekp--gaps-between para i k)))))
|
||||||
(setq saw-allowed t)
|
(setq saw-allowed t)
|
||||||
(setq k (1+ k)))))))))
|
(setq k (1+ k))))))))))
|
||||||
;; Extract solution (nil when end unreachable in the strict pass)
|
;; Extract solution (nil when end unreachable in the strict pass)
|
||||||
(when (aref demerits n)
|
(when (aref demerits n)
|
||||||
(let ((breaks (ekp--dp-trace-breaks backptrs n)))
|
(let ((breaks (ekp--dp-trace-breaks backptrs n)))
|
||||||
@ -1363,7 +1731,10 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
:gaps (mapcar (lambda (b) (aref gaps b)) breaks)
|
:gaps (mapcar (lambda (b) (aref gaps b)) breaks)
|
||||||
:breaks breaks
|
:breaks breaks
|
||||||
:cost (aref demerits n)
|
:cost (aref demerits n)
|
||||||
:line-count (length breaks))))))
|
:line-count (length breaks)
|
||||||
|
:state (vector backptrs demerits rests gaps
|
||||||
|
hyphen-counts fitness-classes
|
||||||
|
allow-emergency))))))
|
||||||
|
|
||||||
(defun ekp--dp-relax-emergency (demerits backptrs rests gaps hyphen-counts
|
(defun ekp--dp-relax-emergency (demerits backptrs rests gaps hyphen-counts
|
||||||
fitness-classes i k prev-dem rest
|
fitness-classes i k prev-dem rest
|
||||||
@ -1666,6 +2037,22 @@ HYPHEN-COUNT)."
|
|||||||
(ekp--dp-cache-via-c para line-pixel)
|
(ekp--dp-cache-via-c para line-pixel)
|
||||||
(ekp--dp-cache-elisp para line-pixel))))
|
(ekp--dp-cache-elisp para line-pixel))))
|
||||||
|
|
||||||
|
(defun ekp--dp-cache-append (para previous stable line-pixel)
|
||||||
|
"Compute PARA at LINE-PIXEL reusing PREVIOUS states through STABLE."
|
||||||
|
(if (ekp--c-available-p)
|
||||||
|
(ekp--dp-cache-via-c para line-pixel)
|
||||||
|
(let* ((old (ekp--dp-get-cached previous line-pixel))
|
||||||
|
(state (and old (plist-get old :state)))
|
||||||
|
(result
|
||||||
|
(if (and state (not (aref state 6)))
|
||||||
|
(or (ekp--dp-run-1d
|
||||||
|
para line-pixel nil state stable)
|
||||||
|
(ekp--dp-run-1d para line-pixel t))
|
||||||
|
(or (ekp--dp-run-1d para line-pixel nil)
|
||||||
|
(ekp--dp-run-1d para line-pixel t)))))
|
||||||
|
(puthash (ekp--dp-key line-pixel) result (ekp-para-dp-cache para))
|
||||||
|
result)))
|
||||||
|
|
||||||
(defun ekp-dp-cache (string line-pixel)
|
(defun ekp-dp-cache (string line-pixel)
|
||||||
"Compute optimal line breaks for STRING at LINE-PIXEL width.
|
"Compute optimal line breaks for STRING at LINE-PIXEL width.
|
||||||
Uses Knuth-Plass dynamic programming with demerits.
|
Uses Knuth-Plass dynamic programming with demerits.
|
||||||
@ -1867,64 +2254,70 @@ Returns ((latin-adj . latin-extra) (mix-adj . mix-extra) (cjk-adj . cjk-extra)).
|
|||||||
(cons mix-adj mix-extra)
|
(cons mix-adj mix-extra)
|
||||||
(cons cjk-adj cjk-extra))))
|
(cons cjk-adj cjk-extra))))
|
||||||
|
|
||||||
(defun ekp--compute-glue-pixels (para glues-types gaps-distribution stretch-p)
|
(defun ekp--fixed-line-glues (para types start end maximum trailing)
|
||||||
"Compute actual glue pixels from GLUES-TYPES and GAPS-DISTRIBUTION.
|
"Return fixed glue pixels for PARA TYPES from START to END.
|
||||||
Return the pixel list for each glue using PARA's stored glue params.
|
Use maximum widths when MAXIMUM is non-nil and finish with TRAILING."
|
||||||
STRETCH-P selects stretch (t) or shrink (nil)."
|
(let* ((params (ekp-para-glue-params para))
|
||||||
(let ((latin-adj (car (nth 0 gaps-distribution)))
|
(pixels (make-vector (1+ (- end start)) 0)))
|
||||||
(latin-extra (cdr (nth 0 gaps-distribution)))
|
(cl-loop for position from (1+ start) below end
|
||||||
(mix-adj (car (nth 1 gaps-distribution)))
|
for output from 1
|
||||||
(mix-extra (cdr (nth 1 gaps-distribution)))
|
for type = (aref types position)
|
||||||
(cjk-adj (car (nth 2 gaps-distribution)))
|
for ideal = (pcase type
|
||||||
(cjk-extra (cdr (nth 2 gaps-distribution)))
|
('lws (plist-get params :lws-ideal))
|
||||||
(latin-idx -1) (mix-idx -1) (cjk-idx -1))
|
('mws (plist-get params :mws-ideal))
|
||||||
(mapcar
|
('cws (plist-get params :cws-ideal))
|
||||||
(lambda (type)
|
(_ 0))
|
||||||
(let* ((base (ekp--para-glue-ideal para type))
|
for stretch = (if maximum
|
||||||
(adj (pcase type
|
(pcase type
|
||||||
('lws (cl-incf latin-idx)
|
('lws (plist-get params :lws-stretch))
|
||||||
(+ latin-adj (if (< latin-idx latin-extra) 1 0)))
|
('mws (plist-get params :mws-stretch))
|
||||||
('mws (cl-incf mix-idx)
|
('cws (plist-get params :cws-stretch))
|
||||||
(+ mix-adj (if (< mix-idx mix-extra) 1 0)))
|
(_ 0))
|
||||||
('cws (cl-incf cjk-idx)
|
0)
|
||||||
(+ cjk-adj (if (< cjk-idx cjk-extra) 1 0)))
|
do (aset pixels output (+ ideal stretch)))
|
||||||
(_ 0))))
|
(aset pixels (1- (length pixels)) trailing)
|
||||||
(max 0 (if stretch-p (+ base adj) (- base adj)))))
|
pixels))
|
||||||
glues-types)))
|
|
||||||
|
|
||||||
(defun ekp--line-glue-single-box (line-pixel box-width hyphen-p hyphen-pixel)
|
(defun ekp--adjusted-line-glues
|
||||||
"Compute glues for a single-box line of width LINE-PIXEL.
|
(para types start end rest-pixel gaps-list)
|
||||||
BOX-WIDTH is the box width; HYPHEN-P adds HYPHEN-PIXEL when the box
|
"Distribute REST-PIXEL over GAPS-LIST for PARA TYPES from START to END."
|
||||||
hyphenates. The trailing filler is clamped at 0 for overfull boxes."
|
|
||||||
(let ((trailing (- line-pixel box-width (if hyphen-p hyphen-pixel 0))))
|
|
||||||
(list 0 (max 0 trailing))))
|
|
||||||
|
|
||||||
(defun ekp--line-glue-last-line (para glues-types ideal-pixel line-pixel)
|
|
||||||
"Compute glues for the last line (ragged right) of PARA at LINE-PIXEL.
|
|
||||||
GLUES-TYPES are the per-glue types and IDEAL-PIXEL the line's ideal
|
|
||||||
width; PARA supplies the stored glue params."
|
|
||||||
(append '(0)
|
|
||||||
(mapcar (lambda (type) (ekp--para-glue-ideal para type)) glues-types)
|
|
||||||
(list (max 0 (- line-pixel ideal-pixel)))))
|
|
||||||
|
|
||||||
(defun ekp--line-glue-normal (para glues-types rest-pixel gaps-list)
|
|
||||||
"Compute glues for a normal (justified) line from PARA.
|
|
||||||
GLUES-TYPES are the per-glue types; REST-PIXEL is the surplus (or
|
|
||||||
deficit) spread across GAPS-LIST using PARA's stored glue params."
|
|
||||||
(if (= rest-pixel 0)
|
(if (= rest-pixel 0)
|
||||||
(append '(0) (mapcar (lambda (type) (ekp--para-glue-ideal para type))
|
(ekp--fixed-line-glues para types start end nil 0)
|
||||||
glues-types)
|
|
||||||
'(0))
|
|
||||||
(let* ((stretch-p (> rest-pixel 0))
|
(let* ((stretch-p (> rest-pixel 0))
|
||||||
(distribution (ekp--distribute-gap-adjustment
|
(distribution (ekp--distribute-gap-adjustment
|
||||||
para (abs rest-pixel) gaps-list stretch-p))
|
para (abs rest-pixel) gaps-list stretch-p))
|
||||||
(glue-pixels (ekp--compute-glue-pixels
|
(shares (vconcat distribution))
|
||||||
para glues-types distribution stretch-p)))
|
(params (ekp-para-glue-params para))
|
||||||
(append '(0) glue-pixels '(0)))))
|
(pixels (make-vector (1+ (- end start)) 0))
|
||||||
|
(indices (vector -1 -1 -1)))
|
||||||
|
(cl-loop for position from (1+ start) below end
|
||||||
|
for output from 1
|
||||||
|
for type = (aref types position)
|
||||||
|
for slot = (pcase type ('lws 0) ('mws 1) ('cws 2) (_ nil))
|
||||||
|
for ideal = (pcase type
|
||||||
|
('lws (plist-get params :lws-ideal))
|
||||||
|
('mws (plist-get params :mws-ideal))
|
||||||
|
('cws (plist-get params :cws-ideal))
|
||||||
|
(_ 0))
|
||||||
|
do
|
||||||
|
(let ((adjustment 0))
|
||||||
|
(when slot
|
||||||
|
(cl-incf (aref indices slot))
|
||||||
|
(let ((share (aref shares slot)))
|
||||||
|
(setq adjustment
|
||||||
|
(+ (car share)
|
||||||
|
(if (< (aref indices slot) (cdr share))
|
||||||
|
1 0)))))
|
||||||
|
(aset pixels output
|
||||||
|
(max 0 (if stretch-p
|
||||||
|
(+ ideal adjustment)
|
||||||
|
(- ideal adjustment))))))
|
||||||
|
pixels)))
|
||||||
|
|
||||||
(defun ekp--line-glues-from-data (para line-pixel dp)
|
(defun ekp--line-glues-from-data
|
||||||
|
(para line-pixel dp &optional previous-lines common)
|
||||||
"Compute glue vectors from prepared PARA at LINE-PIXEL using DP.
|
"Compute glue vectors from prepared PARA at LINE-PIXEL using DP.
|
||||||
Each line's glues are [0 glue1 glue2 ... trailing-space]."
|
Reuse COMMON entries from PREVIOUS-LINES when provided."
|
||||||
(let* ((boxes-num (length (ekp-para-boxes para)))
|
(let* ((boxes-num (length (ekp-para-boxes para)))
|
||||||
(glues-types (ekp-para-glues-types para))
|
(glues-types (ekp-para-glues-types para))
|
||||||
(alignment (or (plist-get (ekp-para-glue-params para) :alignment)
|
(alignment (or (plist-get (ekp-para-glue-params para) :alignment)
|
||||||
@ -1936,11 +2329,14 @@ Each line's glues are [0 glue1 glue2 ... trailing-space]."
|
|||||||
(lines-gaps (plist-get dp :gaps))
|
(lines-gaps (plist-get dp :gaps))
|
||||||
(hyphen-pixel (ekp-para-hyphen-pixel para))
|
(hyphen-pixel (ekp-para-hyphen-pixel para))
|
||||||
(line-glues (make-vector (length breaks) nil))
|
(line-glues (make-vector (length breaks) nil))
|
||||||
(start 0))
|
(start (if (> (or common 0) 0)
|
||||||
(dotimes (i (length breaks))
|
(nth (1- common) breaks)
|
||||||
|
0)))
|
||||||
|
(dotimes (index (or common 0))
|
||||||
|
(aset line-glues index
|
||||||
|
(ekp-layout-line-glues (aref previous-lines index))))
|
||||||
|
(cl-loop for i from (or common 0) below (length breaks) do
|
||||||
(let* ((end (nth i breaks))
|
(let* ((end (nth i breaks))
|
||||||
(line-glues-types (append (cl-subseq glues-types (1+ start) end)
|
|
||||||
nil))
|
|
||||||
(is-last (>= end boxes-num))
|
(is-last (>= end boxes-num))
|
||||||
(hyphen-p (ekp--hyphenate-p hyphen-positions (1- end)))
|
(hyphen-p (ekp--hyphenate-p hyphen-positions (1- end)))
|
||||||
;; per-line layout (parshape / first-line indent)
|
;; per-line layout (parshape / first-line indent)
|
||||||
@ -1968,53 +2364,48 @@ Each line's glues are [0 glue1 glue2 ... trailing-space]."
|
|||||||
(aref (ekp-para-glue-stretches para) start))
|
(aref (ekp-para-glue-stretches para) start))
|
||||||
space-w)
|
space-w)
|
||||||
(if hyphen-p hyphen-pixel 0))))
|
(if hyphen-p hyphen-pixel 0))))
|
||||||
glue-list)
|
glue-vector)
|
||||||
(setq glue-list
|
(setq glue-vector
|
||||||
(cond
|
(cond
|
||||||
;; Single box: just trailing space
|
;; Single box: just trailing space
|
||||||
((= 1 (- end start))
|
((= 1 (- end start))
|
||||||
(ekp--line-glue-single-box eff-pixel
|
(vector 0 (max 0 (- eff-pixel ideal-pixel))))
|
||||||
(- ideal-pixel
|
|
||||||
(if hyphen-p hyphen-pixel 0))
|
|
||||||
hyphen-p hyphen-pixel))
|
|
||||||
;; Last line, or any line under non-justify alignment:
|
;; Last line, or any line under non-justify alignment:
|
||||||
;; natural glue widths plus a trailing filler.
|
;; natural glue widths plus a trailing filler.
|
||||||
((or is-last ragged)
|
((or is-last ragged)
|
||||||
(ekp--line-glue-last-line
|
(ekp--fixed-line-glues
|
||||||
para line-glues-types ideal-pixel eff-pixel))
|
para glues-types start end nil
|
||||||
|
(max 0 (- eff-pixel ideal-pixel))))
|
||||||
;; Emergency underfull line (can't stretch to width):
|
;; Emergency underfull line (can't stretch to width):
|
||||||
;; set glues to max and pad with trailing filler.
|
;; set glues to max and pad with trailing filler.
|
||||||
((< max-pixel eff-pixel)
|
((< max-pixel eff-pixel)
|
||||||
(append '(0)
|
(ekp--fixed-line-glues
|
||||||
(mapcar (lambda (type)
|
para glues-types start end t
|
||||||
(ekp--para-glue-max para type))
|
(max 0 (- eff-pixel max-pixel))))
|
||||||
line-glues-types)
|
|
||||||
(list (max 0 (- eff-pixel max-pixel)))))
|
|
||||||
;; Normal justified line
|
;; Normal justified line
|
||||||
(t
|
(t
|
||||||
(ekp--line-glue-normal para line-glues-types
|
(ekp--adjusted-line-glues
|
||||||
(nth i lines-rests)
|
para glues-types start end
|
||||||
(nth i lines-gaps)))))
|
(nth i lines-rests) (nth i lines-gaps)))))
|
||||||
;; Non-justify alignment: place the leftover per mode
|
;; Non-justify alignment: place the leftover per mode
|
||||||
;; (ragged-right keeps it trailing; center splits it; ragged-left
|
;; (ragged-right keeps it trailing; center splits it; ragged-left
|
||||||
;; moves it to the head).
|
;; moves it to the head).
|
||||||
(when (and ragged (>= (length glue-list) 2)
|
(when (and ragged (>= (length glue-vector) 2)
|
||||||
(memq alignment '(center ragged-left)))
|
(memq alignment '(center ragged-left)))
|
||||||
(let ((filler (car (last glue-list))))
|
(let* ((last (1- (length glue-vector)))
|
||||||
(setq glue-list
|
(filler (aref glue-vector last))
|
||||||
|
(lead (if (eq alignment 'center)
|
||||||
|
(/ filler 2)
|
||||||
|
filler)))
|
||||||
|
(aset glue-vector 0 lead)
|
||||||
|
(aset glue-vector last
|
||||||
(if (eq alignment 'center)
|
(if (eq alignment 'center)
|
||||||
(let ((lead (/ filler 2)))
|
(- filler lead)
|
||||||
(append (list lead)
|
0))))
|
||||||
(cdr (butlast glue-list))
|
|
||||||
(list (- filler lead))))
|
|
||||||
(append (list filler)
|
|
||||||
(cdr (butlast glue-list))
|
|
||||||
(list 0))))))
|
|
||||||
;; left indent renders as a leading spacer
|
;; left indent renders as a leading spacer
|
||||||
(when (> line-indent 0)
|
(when (> line-indent 0)
|
||||||
(setq glue-list (cons (+ (car glue-list) line-indent)
|
(aset glue-vector 0 (+ (aref glue-vector 0) line-indent)))
|
||||||
(cdr glue-list))))
|
(aset line-glues i glue-vector)
|
||||||
(aset line-glues i (vconcat glue-list))
|
|
||||||
(setq start end)))
|
(setq start end)))
|
||||||
line-glues))
|
line-glues))
|
||||||
|
|
||||||
@ -2191,23 +2582,53 @@ LAST-LINE-P suppresses a terminal discretionary hyphen."
|
|||||||
:trailing-pixel (aref glues (1- (length glues)))
|
:trailing-pixel (aref glues (1- (length glues)))
|
||||||
:hyphen-p hyphen-p))))
|
:hyphen-p hyphen-p))))
|
||||||
|
|
||||||
(defun ekp-layout-plan (string line-pixel)
|
(defun ekp--common-layout-line-count (previous breaks stable)
|
||||||
"Return a semantic KP layout plan for STRING at LINE-PIXEL.
|
"Return the break prefix shared by PREVIOUS and BREAKS before STABLE."
|
||||||
The plan records source offsets, glue targets, breaks, indentation,
|
(let ((lines (and previous (ekp-layout-plan-lines previous)))
|
||||||
and discretionary hyphens without choosing a string or buffer display
|
(count 0))
|
||||||
representation."
|
(while (and lines
|
||||||
(let* ((para (ekp--get-para string))
|
(< count (length lines))
|
||||||
(boxes (ekp-para-boxes para))
|
(< count (length breaks))
|
||||||
|
(<= (ekp-layout-line-box-end (aref lines count)) stable)
|
||||||
|
(= (ekp-layout-line-box-end (aref lines count))
|
||||||
|
(nth count breaks)))
|
||||||
|
(cl-incf count))
|
||||||
|
count))
|
||||||
|
|
||||||
|
(defun ekp--layout-context-snapshot (line-pixel)
|
||||||
|
"Return every non-text input to a plan at LINE-PIXEL."
|
||||||
|
(list (ekp--dp-key line-pixel)
|
||||||
|
(copy-tree (ekp--width-context))
|
||||||
|
ekp-latin-lang
|
||||||
|
ekp-alignment
|
||||||
|
ekp-ragged-stretch-pixel
|
||||||
|
(and ekp-protrusion (copy-tree ekp-protrusion-ratios))
|
||||||
|
(copy-tree ekp-parshape)
|
||||||
|
ekp-first-line-indent
|
||||||
|
ekp-cjk-no-line-start-extra
|
||||||
|
(ekp--spacing-signature)))
|
||||||
|
|
||||||
|
(defun ekp--layout-plan-from-para
|
||||||
|
(string line-pixel para dp &optional previous stable)
|
||||||
|
"Build STRING's LINE-PIXEL plan from PARA and DP.
|
||||||
|
Reuse PREVIOUS lines that end before STABLE when both are non-nil."
|
||||||
|
(let* ((boxes (ekp-para-boxes para))
|
||||||
(offsets (or (ekp-para-box-offsets-memo para)
|
(offsets (or (ekp-para-box-offsets-memo para)
|
||||||
(setf (ekp-para-box-offsets-memo para)
|
(setf (ekp-para-box-offsets-memo para)
|
||||||
(ekp--box-offsets string (append boxes nil)))))
|
(ekp--box-offsets string (append boxes nil)))))
|
||||||
(naturals (ekp--gap-natural-pixels para offsets))
|
(naturals (ekp--gap-natural-pixels para offsets))
|
||||||
(dp (ekp--dp-cache-para para line-pixel))
|
|
||||||
(breaks (plist-get dp :breaks))
|
(breaks (plist-get dp :breaks))
|
||||||
(line-glues (ekp--line-glues-from-data para line-pixel dp))
|
(previous-lines (and previous (ekp-layout-plan-lines previous)))
|
||||||
(start 0)
|
(common (if previous
|
||||||
lines)
|
(ekp--common-layout-line-count
|
||||||
(dotimes (i (length breaks))
|
previous breaks stable)
|
||||||
|
0))
|
||||||
|
(line-glues (ekp--line-glues-from-data
|
||||||
|
para line-pixel dp previous-lines common))
|
||||||
|
(start (if (> common 0) (nth (1- common) breaks) 0))
|
||||||
|
(lines (reverse
|
||||||
|
(cl-subseq (append previous-lines nil) 0 common))))
|
||||||
|
(cl-loop for i from common below (length breaks) do
|
||||||
(let* ((end (nth i breaks))
|
(let* ((end (nth i breaks))
|
||||||
(line (ekp--make-layout-line
|
(line (ekp--make-layout-line
|
||||||
para offsets naturals i start end (aref line-glues i)
|
para offsets naturals i start end (aref line-glues i)
|
||||||
@ -2218,7 +2639,35 @@ representation."
|
|||||||
string (vconcat (nreverse lines))))
|
string (vconcat (nreverse lines))))
|
||||||
(ekp-layout-plan--create
|
(ekp-layout-plan--create
|
||||||
:string string :line-pixel line-pixel
|
:string string :line-pixel line-pixel
|
||||||
:boxes boxes :offsets offsets :lines lines)))
|
:context (ekp--layout-context-snapshot line-pixel)
|
||||||
|
:para para :boxes boxes :offsets offsets :lines lines)))
|
||||||
|
|
||||||
|
(defun ekp-layout-plan (string line-pixel)
|
||||||
|
"Return a semantic KP layout plan for STRING at LINE-PIXEL.
|
||||||
|
The plan records source offsets, glue targets, breaks, indentation,
|
||||||
|
and discretionary hyphens without choosing a display representation."
|
||||||
|
(let ((para (ekp--get-para string)))
|
||||||
|
(ekp--layout-plan-from-para
|
||||||
|
string line-pixel para (ekp--dp-cache-para para line-pixel))))
|
||||||
|
|
||||||
|
(defun ekp-layout-plan-append (previous string line-pixel)
|
||||||
|
"Return STRING's exact append plan by extending PREVIOUS, or nil.
|
||||||
|
Only property-free, context-stable 1D layouts take this fast path."
|
||||||
|
(let ((old-para (and previous (ekp-layout-plan-para previous))))
|
||||||
|
(when (and old-para
|
||||||
|
(equal (ekp-layout-plan-context previous)
|
||||||
|
(ekp--layout-context-snapshot line-pixel))
|
||||||
|
(= ekp-looseness 0)
|
||||||
|
(not ekp-parshape)
|
||||||
|
(equal (ekp-para-glue-params old-para)
|
||||||
|
(ekp--glue-params-snapshot)))
|
||||||
|
(when-let* ((append (ekp--append-para old-para string))
|
||||||
|
(para (car append))
|
||||||
|
(stable (cdr append))
|
||||||
|
(dp (ekp--dp-cache-append
|
||||||
|
para old-para stable line-pixel)))
|
||||||
|
(ekp--layout-plan-from-para
|
||||||
|
string line-pixel para dp previous stable)))))
|
||||||
|
|
||||||
(defconst ekp--layout-marker-properties
|
(defconst ekp--layout-marker-properties
|
||||||
'(ekp-glue ekp-soft-break ekp-soft-hyphen ekp-hidden ekp-justified)
|
'(ekp-glue ekp-soft-break ekp-soft-hyphen ekp-hidden ekp-justified)
|
||||||
|
|||||||
@ -7,6 +7,12 @@ The division of labor: **Elisp owns all font-dependent data**
|
|||||||
module runs only the O(n²) dynamic program. This keeps the two engines
|
module runs only the O(n²) dynamic program. This keeps the two engines
|
||||||
byte-identical in output while making the hot loop native.
|
byte-identical in output while making the hot loop native.
|
||||||
|
|
||||||
|
The module remains C deliberately. A Rust implementation would still expose
|
||||||
|
the Emacs C module ABI and consume the same Elisp-prepared vectors. Current
|
||||||
|
live-commit profiles put the candidate module call below one millisecond, so
|
||||||
|
a Rust rewrite would add Cargo, target, and packaging obligations without
|
||||||
|
removing the measured end-to-end owners.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -100,6 +106,9 @@ the dispatcher never hides it or silently produces a different layout.
|
|||||||
Every public pixel/position integer must fit signed 32-bit range. The DP
|
Every public pixel/position integer must fit signed 32-bit range. The DP
|
||||||
uses 64-bit intermediates for sums and differences, so valid extreme inputs
|
uses 64-bit intermediates for sums and differences, so valid extreme inputs
|
||||||
cannot overflow when line width and protrusion are combined.
|
cannot overflow when line width and protrusion are combined.
|
||||||
|
Validation uses one `extract_integer` call per integer value and clears the
|
||||||
|
temporary non-local exit only when the value is not an integer; it does not
|
||||||
|
round-trip through Lisp predicates and comparisons.
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
@ -119,3 +128,10 @@ tokenization, measurement and rendering stay in Elisp. The C engine
|
|||||||
matters most for `range-justify` (many widths per text) and
|
matters most for `range-justify` (many widths per text) and
|
||||||
multi-paragraph batches. Absolute numbers vary with the machine and
|
multi-paragraph batches. Absolute numbers vary with the machine and
|
||||||
power state; regenerate them with the two commands in DEVELOPER.md §9.
|
power state; regenerate them with the two commands in DEVELOPER.md §9.
|
||||||
|
|
||||||
|
For task030's frozen 80-pixel structural-commit matrix, the C module layer
|
||||||
|
improved from 2.615/2.655 ms to 0.697/0.701 ms p95/p99 after one-pass integer
|
||||||
|
validation. With the production Elisp files byte-compiled, the complete
|
||||||
|
public append path measures 1.158–1.326 ms p99 for C and 1.429–1.438 ms for
|
||||||
|
pure Elisp on the same machine. These figures are separate from the
|
||||||
|
deliberately source-loaded, fully instrumented evaluator.
|
||||||
|
|||||||
16
ekp_c/ekp.c
16
ekp_c/ekp.c
@ -47,18 +47,12 @@ static bool lisp_predicate(emacs_env *env, const char *name, emacs_value value)
|
|||||||
|
|
||||||
static bool i32_value_p(emacs_env *env, emacs_value value)
|
static bool i32_value_p(emacs_env *env, emacs_value value)
|
||||||
{
|
{
|
||||||
if (!lisp_predicate(env, "integerp", value))
|
intmax_t integer = env->extract_integer(env, value);
|
||||||
|
if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
|
||||||
|
env->non_local_exit_clear(env);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
emacs_value min = env->make_integer(env, INT32_MIN);
|
return integer >= INT32_MIN && integer <= INT32_MAX;
|
||||||
emacs_value max = env->make_integer(env, INT32_MAX);
|
|
||||||
bool at_least_min = env->is_not_nil(
|
|
||||||
env, env->funcall(
|
|
||||||
env, env->intern(env, ">="), 2, (emacs_value[]){value, min}));
|
|
||||||
bool at_most_max = env->is_not_nil(
|
|
||||||
env, env->funcall(
|
|
||||||
env, env->intern(env, "<="), 2, (emacs_value[]){value, max}));
|
|
||||||
return at_least_min && at_most_max;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool finite_number_p(emacs_env *env, emacs_value value)
|
static bool finite_number_p(emacs_env *env, emacs_value value)
|
||||||
|
|||||||
95
postmortem/20260730-incremental-live-append-ownership.md
Normal file
95
postmortem/20260730-incremental-live-append-ownership.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# Incremental Live-Append Ownership
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
After stable live transactions removed per-key whole-paragraph planning,
|
||||||
|
only native visual-row crossings still paid the complete paragraph
|
||||||
|
preparation, dynamic program, plan construction, and projection cost.
|
||||||
|
At 80 pixels the old source benchmark attributed 51–187 ms p99 to these
|
||||||
|
permitted commits.
|
||||||
|
|
||||||
|
The C module itself was not the primary owner. In the frozen baseline its
|
||||||
|
80-pixel module call measured only 2.615/2.655 ms p95/p99, while paragraph
|
||||||
|
preparation and the surrounding plan dominated. A Rust module would still
|
||||||
|
enter through `emacs_env`, consume the same prepared vectors, and return the
|
||||||
|
same DP result.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Keep the C module and make the existing ownership model incremental:
|
||||||
|
|
||||||
|
- a layout plan retains its prepared paragraph and exact layout context;
|
||||||
|
- a property-free plain-text append retokenizes from the last complete-word
|
||||||
|
boundary instead of from paragraph start;
|
||||||
|
- derived prefix, break, protrusion, and gap data are copied only through the
|
||||||
|
proven-stable box boundary and recomputed from that boundary onward;
|
||||||
|
- pure Elisp DP retains its state and resumes from the earliest old state
|
||||||
|
that can still reach the first new break;
|
||||||
|
- C runs its full native DP over incrementally prepared vectors because its
|
||||||
|
sub-millisecond candidate call is already cheaper than adding another ABI;
|
||||||
|
- the buffer replaces only the dirty source island and reuses common layout
|
||||||
|
lines before publishing the changed suffix.
|
||||||
|
|
||||||
|
Any unsupported property, font, layout-context, tab/newline, parshape,
|
||||||
|
looseness, or token-boundary case returns nil and takes the unchanged full
|
||||||
|
planner. The fast path never approximates cache identity or layout output.
|
||||||
|
|
||||||
|
## Rejected alternatives
|
||||||
|
|
||||||
|
- **Rewrite the module in Rust:** it does not remove the Emacs C ABI or the
|
||||||
|
Elisp-owned work, while adding Cargo, Rust toolchain, target, packaging,
|
||||||
|
and Windows support obligations.
|
||||||
|
- **Move the whole planner behind the module boundary:** font measurement,
|
||||||
|
text properties, and editor transaction ownership belong to Emacs; moving
|
||||||
|
them would create a larger, less stable ABI.
|
||||||
|
- **Reuse stale prefix decisions:** a suffix can change the globally optimal
|
||||||
|
KP path, so only prepared data and reachable DP states may be reused.
|
||||||
|
- **Debounce, skip, or delay commits:** this hides latency by making display
|
||||||
|
state stale and violates the synchronous live-edit contract.
|
||||||
|
|
||||||
|
## Correctness trap
|
||||||
|
|
||||||
|
The first boundary box is dirty even when every earlier box is unchanged.
|
||||||
|
Its break permission depends on both neighboring boxes, so copying through
|
||||||
|
that position preserved a stale forbidden break. The final implementation
|
||||||
|
copies strictly before the stable boundary and recomputes break data from
|
||||||
|
the boundary. A regression fixes this ownership rule.
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
The four-round frozen source/instrumentation matrix preserves exact
|
||||||
|
baseline/C/Elisp source and projection hashes, zero conflicts, valid
|
||||||
|
GC-excluded samples, zero-work ordinary keys, and non-regression at
|
||||||
|
64/80/96/128/160 pixels. At 80 pixels:
|
||||||
|
|
||||||
|
- C improves 77.78% p95 and 78.37% p99 to 25.490/25.785 ms;
|
||||||
|
- Elisp improves 92.54% p95 and 92.03% p99 to 43.860/47.578 ms;
|
||||||
|
- the C module itself falls to 0.697/0.701 ms p95/p99.
|
||||||
|
|
||||||
|
Those source-instrumented values still miss the locked 16 ms absolute gate.
|
||||||
|
The production-shaped byte-compiled public path is materially different:
|
||||||
|
three repeated runs measure append p99 at 1.158–1.326 ms for C and
|
||||||
|
1.429–1.438 ms for Elisp, with no GC during the samples. Both measurements
|
||||||
|
remain documented; the stricter goal is not silently weakened.
|
||||||
|
|
||||||
|
## Known design debt
|
||||||
|
|
||||||
|
At a 558-pixel GUI width, deleting a suffix after a forward row crossing can
|
||||||
|
republish a backward layout and violate the existing “preserve typed
|
||||||
|
projection” visual invariant. The frozen baseline reproduces the same
|
||||||
|
behavior, and current ERT explicitly treats a backward wrap crossing as a
|
||||||
|
commit. This is not caused by incremental append work and needs a separate
|
||||||
|
semantic decision before a narrow tail-shrink predicate is changed.
|
||||||
|
|
||||||
|
## Verification and rollback
|
||||||
|
|
||||||
|
Exact append-chain equivalence is covered across C and Elisp, varied widths,
|
||||||
|
unsafe fallback contexts, and the stable-boundary regression. Normal and
|
||||||
|
permuted 199-test suites, 300 fuzz cases, warning-as-error compilation,
|
||||||
|
portable C warnings/tests, release checks, and reviewed temporal GUI
|
||||||
|
evidence pass.
|
||||||
|
|
||||||
|
Rollback is a direct revert of retained plan/paragraph state, incremental
|
||||||
|
append preparation/DP, dirty-island reconstruction, and one-pass C integer
|
||||||
|
validation. No data migration, compatibility shim, or cache conversion is
|
||||||
|
required.
|
||||||
@ -818,6 +818,38 @@
|
|||||||
(buffer-substring (point-min) (point-max))
|
(buffer-substring (point-min) (point-max))
|
||||||
baseline))))))
|
baseline))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-buffer-test-live-source-rebuilds-only-dirty-island ()
|
||||||
|
"A live source rebuild must not rescan the projected paragraph."
|
||||||
|
(let ((text
|
||||||
|
"alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu"))
|
||||||
|
(ekp-buffer-test--with-mode text 20
|
||||||
|
(let* ((middle
|
||||||
|
(nth 1
|
||||||
|
(ekp-buffer--live-state-spans
|
||||||
|
ekp-buffer--live-state)))
|
||||||
|
(beg (marker-position (ekp-buffer--span-beg middle)))
|
||||||
|
(end (marker-position (ekp-buffer--span-end middle)))
|
||||||
|
(space (save-excursion
|
||||||
|
(goto-char beg)
|
||||||
|
(search-forward " " end t)))
|
||||||
|
(logical-substring
|
||||||
|
(symbol-function 'ekp-buffer--logical-substring))
|
||||||
|
(rescans 0))
|
||||||
|
(should space)
|
||||||
|
(goto-char (1- space))
|
||||||
|
(delete-char 1)
|
||||||
|
(should ekp-buffer--live-edit)
|
||||||
|
(cl-letf (((symbol-function 'ekp-buffer--logical-substring)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(setq rescans (1+ rescans))
|
||||||
|
(apply logical-substring arguments))))
|
||||||
|
(should
|
||||||
|
(equal-including-properties
|
||||||
|
(ekp-buffer--current-live-source)
|
||||||
|
(ekp-buffer--logical-substring
|
||||||
|
(point-min) (point-max)))))
|
||||||
|
(should (= rescans 1))))))
|
||||||
|
|
||||||
(ert-deftest ekp-buffer-test-point-leaving-paragraph-is-zero-work ()
|
(ert-deftest ekp-buffer-test-point-leaving-paragraph-is-zero-work ()
|
||||||
"Cursor motion across hard lines cannot commit or rewrite live state."
|
"Cursor motion across hard lines cannot commit or rewrite live state."
|
||||||
(let ((text
|
(let ((text
|
||||||
|
|||||||
@ -54,6 +54,14 @@
|
|||||||
(should-error (apply #'ekp-c-break-with-arrays args)
|
(should-error (apply #'ekp-c-break-with-arrays args)
|
||||||
:type 'ekp-c-invalid-input)))
|
:type 'ekp-c-invalid-input)))
|
||||||
|
|
||||||
|
(ert-deftest ekp-c-test-rejects-non-integer-vector-value ()
|
||||||
|
"Vector values that are not integers use the explicit input condition."
|
||||||
|
(skip-unless (ekp-c-tests--available))
|
||||||
|
(let ((args (ekp-c-tests--valid-args)))
|
||||||
|
(setf (nth 3 args) ["not-an-integer"])
|
||||||
|
(should-error (apply #'ekp-c-break-with-arrays args)
|
||||||
|
:type 'ekp-c-invalid-input)))
|
||||||
|
|
||||||
(ert-deftest ekp-c-test-batch-rejects-short-paragraph-vector ()
|
(ert-deftest ekp-c-test-batch-rejects-short-paragraph-vector ()
|
||||||
"Batch preflight validates each paragraph before indexing 15 fields."
|
"Batch preflight validates each paragraph before indexing 15 fields."
|
||||||
(skip-unless (ekp-c-tests--available))
|
(skip-unless (ekp-c-tests--available))
|
||||||
|
|||||||
698
tests/ekp-live-commit-evaluator.el
Normal file
698
tests/ekp-live-commit-evaluator.el
Normal file
@ -0,0 +1,698 @@
|
|||||||
|
;;; ekp-live-commit-evaluator.el --- Structural live-commit gate -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; One process measures one frozen-baseline or candidate round. Compare mode
|
||||||
|
;; aggregates interleaved JSONL records and enforces the task030 contract.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'json)
|
||||||
|
|
||||||
|
(defvar ekp-use-c-module)
|
||||||
|
(defvar ekp-buffer--conflicts)
|
||||||
|
(defvar ekp-auto-justify-paragraph-limit)
|
||||||
|
(declare-function ekp-auto-justify-mode "ekp-buffer")
|
||||||
|
(declare-function ekp-buffer--logical-substring "ekp-buffer")
|
||||||
|
(declare-function ekp-buffer--window-pixel "ekp-buffer")
|
||||||
|
(declare-function ekp-c-module-load "ekp-utils")
|
||||||
|
(declare-function ekp-c-version "ext:ekp")
|
||||||
|
(declare-function ekp-clear-caches "ekp")
|
||||||
|
(declare-function ekp--c-available-p "ekp")
|
||||||
|
(declare-function ekp--measured-width "ekp")
|
||||||
|
|
||||||
|
(defconst ekp-live-commit-evaluator--widths '(64 80 96 128 160))
|
||||||
|
(defconst ekp-live-commit-evaluator--rows '(2 4 8 16))
|
||||||
|
(defconst ekp-live-commit-evaluator--target-ms 16.0)
|
||||||
|
(defconst ekp-live-commit-evaluator--minimum-improvement 20.0)
|
||||||
|
(defconst ekp-live-commit-evaluator--ordinary-target-ms 1.0)
|
||||||
|
(defconst ekp-live-commit-evaluator--maximum-regression 15.0)
|
||||||
|
(defconst ekp-live-commit-evaluator--default-gc-threshold
|
||||||
|
gc-cons-threshold)
|
||||||
|
(defconst ekp-live-commit-evaluator--corpus
|
||||||
|
(concat
|
||||||
|
" extraordinary editing continues smoothly 中文拉丁混排"
|
||||||
|
" while stable semantic rows remain exact and responsive"))
|
||||||
|
|
||||||
|
(defvar ekp-live-commit-evaluator--publish-calls 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--row-crossings 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--plan-calls 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--module-calls 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--append-calls 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--append-hits 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--cache-gets 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--cache-hits 0)
|
||||||
|
(defvar ekp-live-commit-evaluator--plan-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--para-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--dp-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--module-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--install-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--clear-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--transaction-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--signature-ms 0.0)
|
||||||
|
(defvar ekp-live-commit-evaluator--cache-ms 0.0)
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--env-number (name fallback)
|
||||||
|
"Return numeric environment variable NAME, or FALLBACK."
|
||||||
|
(if-let ((value (getenv name)))
|
||||||
|
(string-to-number value)
|
||||||
|
fallback))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--env-numbers (name fallback)
|
||||||
|
"Return comma-separated numeric environment variable NAME, or FALLBACK."
|
||||||
|
(if-let ((value (getenv name)))
|
||||||
|
(mapcar #'string-to-number (split-string value "," t "[ \t]+"))
|
||||||
|
fallback))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--env-strings (name fallback)
|
||||||
|
"Return comma-separated string environment variable NAME, or FALLBACK."
|
||||||
|
(if-let ((value (getenv name)))
|
||||||
|
(split-string value "," t "[ \t]+")
|
||||||
|
fallback))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--reset-profile ()
|
||||||
|
"Reset counters for one public command."
|
||||||
|
(setq ekp-live-commit-evaluator--publish-calls 0
|
||||||
|
ekp-live-commit-evaluator--row-crossings 0
|
||||||
|
ekp-live-commit-evaluator--plan-calls 0
|
||||||
|
ekp-live-commit-evaluator--module-calls 0
|
||||||
|
ekp-live-commit-evaluator--append-calls 0
|
||||||
|
ekp-live-commit-evaluator--append-hits 0
|
||||||
|
ekp-live-commit-evaluator--cache-gets 0
|
||||||
|
ekp-live-commit-evaluator--cache-hits 0
|
||||||
|
ekp-live-commit-evaluator--plan-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--para-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--dp-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--module-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--install-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--clear-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--transaction-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--signature-ms 0.0
|
||||||
|
ekp-live-commit-evaluator--cache-ms 0.0))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--timed-call (counter function arguments)
|
||||||
|
"Call FUNCTION with ARGUMENTS and add elapsed milliseconds to COUNTER."
|
||||||
|
(let ((started (float-time)))
|
||||||
|
(prog1 (apply function arguments)
|
||||||
|
(set counter
|
||||||
|
(+ (symbol-value counter)
|
||||||
|
(* 1000.0 (- (float-time) started)))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--counted-wrapper (counter function)
|
||||||
|
"Return a wrapper incrementing COUNTER before calling FUNCTION."
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(set counter (1+ (symbol-value counter)))
|
||||||
|
(apply function arguments)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
(time-counter call-counter function)
|
||||||
|
"Return a timed FUNCTION wrapper using TIME-COUNTER and CALL-COUNTER."
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(when call-counter
|
||||||
|
(set call-counter (1+ (symbol-value call-counter))))
|
||||||
|
(ekp-live-commit-evaluator--timed-call
|
||||||
|
time-counter function arguments)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--cache-wrapper (function)
|
||||||
|
"Return a live-cache FUNCTION wrapper that records hits."
|
||||||
|
(lambda (key)
|
||||||
|
(cl-incf ekp-live-commit-evaluator--cache-gets)
|
||||||
|
(let ((started (float-time)))
|
||||||
|
(prog1
|
||||||
|
(let ((plan (funcall function key)))
|
||||||
|
(when plan
|
||||||
|
(cl-incf ekp-live-commit-evaluator--cache-hits))
|
||||||
|
plan)
|
||||||
|
(cl-incf ekp-live-commit-evaluator--cache-ms
|
||||||
|
(* 1000.0 (- (float-time) started)))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--row-crossed-wrapper (function)
|
||||||
|
"Return a FUNCTION wrapper that counts true row crossings."
|
||||||
|
(lambda ()
|
||||||
|
(let ((crossed (funcall function)))
|
||||||
|
(when crossed
|
||||||
|
(cl-incf ekp-live-commit-evaluator--row-crossings))
|
||||||
|
crossed)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--append-wrapper (function)
|
||||||
|
"Return a FUNCTION wrapper that records append-plan hits."
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf ekp-live-commit-evaluator--append-calls)
|
||||||
|
(let ((plan (apply function arguments)))
|
||||||
|
(when plan
|
||||||
|
(cl-incf ekp-live-commit-evaluator--append-hits))
|
||||||
|
plan)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--instrument (function)
|
||||||
|
"Call FUNCTION with live-commit layer instrumentation installed."
|
||||||
|
(let ((publish (symbol-function 'ekp-buffer--publish-live-prefix))
|
||||||
|
(plan (symbol-function 'ekp-layout-plan))
|
||||||
|
(append-plan (symbol-function 'ekp-layout-plan-append))
|
||||||
|
(para (symbol-function 'ekp--get-para))
|
||||||
|
(dp (symbol-function 'ekp--dp-cache-para))
|
||||||
|
(module (symbol-function 'ekp-c-break-with-arrays))
|
||||||
|
(install (symbol-function 'ekp-buffer--install-live-prefix))
|
||||||
|
(suffix (symbol-function 'ekp-buffer--clear-live-suffix))
|
||||||
|
(projection (symbol-function 'ekp-buffer--clear-live-projection))
|
||||||
|
(transaction (symbol-function 'ekp-buffer--start-live-edit))
|
||||||
|
(signatures
|
||||||
|
(symbol-function 'ekp-buffer--live-prefix-signatures))
|
||||||
|
(row-crossed (symbol-function 'ekp-buffer--live-row-crossed-p))
|
||||||
|
(cache (symbol-function 'ekp-buffer--live-cache-get)))
|
||||||
|
(cl-letf
|
||||||
|
(((symbol-function 'ekp-buffer--publish-live-prefix)
|
||||||
|
(ekp-live-commit-evaluator--counted-wrapper
|
||||||
|
'ekp-live-commit-evaluator--publish-calls publish))
|
||||||
|
((symbol-function 'ekp-layout-plan)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--plan-ms
|
||||||
|
'ekp-live-commit-evaluator--plan-calls plan))
|
||||||
|
((symbol-function 'ekp-layout-plan-append)
|
||||||
|
(ekp-live-commit-evaluator--append-wrapper append-plan))
|
||||||
|
((symbol-function 'ekp--get-para)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--para-ms nil para))
|
||||||
|
((symbol-function 'ekp--dp-cache-para)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--dp-ms nil dp))
|
||||||
|
((symbol-function 'ekp-c-break-with-arrays)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--module-ms
|
||||||
|
'ekp-live-commit-evaluator--module-calls module))
|
||||||
|
((symbol-function 'ekp-buffer--install-live-prefix)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--install-ms nil install))
|
||||||
|
((symbol-function 'ekp-buffer--clear-live-suffix)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--clear-ms nil suffix))
|
||||||
|
((symbol-function 'ekp-buffer--clear-live-projection)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--clear-ms nil projection))
|
||||||
|
((symbol-function 'ekp-buffer--start-live-edit)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--transaction-ms nil transaction))
|
||||||
|
((symbol-function 'ekp-buffer--live-prefix-signatures)
|
||||||
|
(ekp-live-commit-evaluator--timed-wrapper
|
||||||
|
'ekp-live-commit-evaluator--signature-ms nil signatures))
|
||||||
|
((symbol-function 'ekp-buffer--live-row-crossed-p)
|
||||||
|
(ekp-live-commit-evaluator--row-crossed-wrapper row-crossed))
|
||||||
|
((symbol-function 'ekp-buffer--live-cache-get)
|
||||||
|
(ekp-live-commit-evaluator--cache-wrapper cache)))
|
||||||
|
(funcall function))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--self-insert (character)
|
||||||
|
"Insert CHARACTER through the public command path."
|
||||||
|
(let ((last-command-event character)
|
||||||
|
(this-command #'self-insert-command))
|
||||||
|
(call-interactively #'self-insert-command)
|
||||||
|
(run-hooks 'post-command-hook)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--event-kind ()
|
||||||
|
"Return the current command's measured event kind."
|
||||||
|
(if (> ekp-live-commit-evaluator--row-crossings 0)
|
||||||
|
"structural-commit"
|
||||||
|
"ordinary-key"))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--sample (character metadata)
|
||||||
|
"Measure one CHARACTER insertion and attach scenario METADATA."
|
||||||
|
(ekp-live-commit-evaluator--reset-profile)
|
||||||
|
(let ((gcs-before gcs-done)
|
||||||
|
(gc-before gc-elapsed)
|
||||||
|
(started (float-time)))
|
||||||
|
(ekp-live-commit-evaluator--self-insert character)
|
||||||
|
(let ((total-ms (* 1000.0 (- (float-time) started)))
|
||||||
|
(gc-events (- gcs-done gcs-before))
|
||||||
|
(gc-ms (* 1000.0 (- gc-elapsed gc-before))))
|
||||||
|
(append
|
||||||
|
metadata
|
||||||
|
`((event_kind . ,(ekp-live-commit-evaluator--event-kind))
|
||||||
|
(char . ,(char-to-string character))
|
||||||
|
(total_ms . ,total-ms)
|
||||||
|
(plan_ms . ,ekp-live-commit-evaluator--plan-ms)
|
||||||
|
(para_ms . ,ekp-live-commit-evaluator--para-ms)
|
||||||
|
(dp_ms . ,ekp-live-commit-evaluator--dp-ms)
|
||||||
|
(module_ms . ,ekp-live-commit-evaluator--module-ms)
|
||||||
|
(publication_ms
|
||||||
|
. ,(+ ekp-live-commit-evaluator--install-ms
|
||||||
|
ekp-live-commit-evaluator--clear-ms))
|
||||||
|
(publish_calls . ,ekp-live-commit-evaluator--publish-calls)
|
||||||
|
(row_crossings . ,ekp-live-commit-evaluator--row-crossings)
|
||||||
|
(plan_calls . ,ekp-live-commit-evaluator--plan-calls)
|
||||||
|
(module_calls . ,ekp-live-commit-evaluator--module-calls)
|
||||||
|
(append_calls . ,ekp-live-commit-evaluator--append-calls)
|
||||||
|
(append_hits . ,ekp-live-commit-evaluator--append-hits)
|
||||||
|
(cache_hits . ,ekp-live-commit-evaluator--cache-hits)
|
||||||
|
(cache_misses
|
||||||
|
. ,(- ekp-live-commit-evaluator--cache-gets
|
||||||
|
ekp-live-commit-evaluator--cache-hits))
|
||||||
|
(cache_ms . ,ekp-live-commit-evaluator--cache-ms)
|
||||||
|
(transaction_ms . ,ekp-live-commit-evaluator--transaction-ms)
|
||||||
|
(signature_ms . ,ekp-live-commit-evaluator--signature-ms)
|
||||||
|
(gc_events . ,gc-events)
|
||||||
|
(gc_ms . ,gc-ms))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--prefix-length (text target)
|
||||||
|
"Return shortest prefix length of TEXT measuring at least TARGET pixels."
|
||||||
|
(let ((low 1)
|
||||||
|
(high (length text)))
|
||||||
|
(while (< low high)
|
||||||
|
(let ((middle (/ (+ low high) 2)))
|
||||||
|
(if (>= (ekp--measured-width (substring text 0 middle)) target)
|
||||||
|
(setq high middle)
|
||||||
|
(setq low (1+ middle)))))
|
||||||
|
low))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--fixture (width rows)
|
||||||
|
"Return mixed prose spanning approximately ROWS lines at WIDTH."
|
||||||
|
(let* ((unit ekp-live-commit-evaluator--corpus)
|
||||||
|
(target (* width rows))
|
||||||
|
(unit-width (max 1 (ekp--measured-width unit)))
|
||||||
|
(copies (max 1 (ceiling (/ (float target) unit-width))))
|
||||||
|
(text (apply #'concat (make-list (1+ copies) unit))))
|
||||||
|
(substring text 0
|
||||||
|
(ekp-live-commit-evaluator--prefix-length text target))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--projection-hash ()
|
||||||
|
"Return a stable hash of the current source plus display projection."
|
||||||
|
(secure-hash
|
||||||
|
'sha256
|
||||||
|
(prin1-to-string (buffer-substring (point-min) (point-max)))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--source-hash ()
|
||||||
|
"Return a stable hash of the current logical source."
|
||||||
|
(secure-hash
|
||||||
|
'sha256
|
||||||
|
(buffer-substring-no-properties (point-min) (point-max))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--metadata (engine gc-mode width rows)
|
||||||
|
"Return sample metadata for ENGINE, GC-MODE, WIDTH, and ROWS."
|
||||||
|
`((engine . ,engine) (gc_mode . ,gc-mode)
|
||||||
|
(width . ,width) (rows . ,rows)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--collect (metadata)
|
||||||
|
"Collect a fixed structural-commit sample count for METADATA."
|
||||||
|
(let* ((width (alist-get 'width metadata))
|
||||||
|
(target (ekp-live-commit-evaluator--env-number
|
||||||
|
"EKP_LIVE_COMMIT_SAMPLES"
|
||||||
|
(if (= width 80) 8 2)))
|
||||||
|
(ordinary-cap (ekp-live-commit-evaluator--env-number
|
||||||
|
"EKP_LIVE_ORDINARY_SAMPLES"
|
||||||
|
(if (= width 80) 32 8)))
|
||||||
|
(stream (string-to-list ekp-live-commit-evaluator--corpus))
|
||||||
|
(index 0)
|
||||||
|
(commits 0)
|
||||||
|
samples)
|
||||||
|
(while (and (< commits target) (< index 4000))
|
||||||
|
(let ((sample
|
||||||
|
(ekp-live-commit-evaluator--sample
|
||||||
|
(nth (% index (length stream)) stream) metadata)))
|
||||||
|
(if (equal (alist-get 'event_kind sample) "structural-commit")
|
||||||
|
(progn (cl-incf commits) (push sample samples))
|
||||||
|
(when (< (- (length samples) commits) ordinary-cap)
|
||||||
|
(push sample samples))))
|
||||||
|
(cl-incf index))
|
||||||
|
(unless (= commits target)
|
||||||
|
(error "Only %d structural commits after %d keys" commits index))
|
||||||
|
(nreverse samples)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--run-cell (engine gc-mode width rows)
|
||||||
|
"Measure one ENGINE, GC-MODE, WIDTH, and ROWS matrix cell."
|
||||||
|
(with-temp-buffer
|
||||||
|
(text-mode)
|
||||||
|
(insert (ekp-live-commit-evaluator--fixture width rows))
|
||||||
|
(goto-char (point-max))
|
||||||
|
(cl-letf (((symbol-function 'ekp-buffer--window-pixel)
|
||||||
|
(lambda (&optional _window) width)))
|
||||||
|
(let ((ekp-auto-justify-paragraph-limit most-positive-fixnum))
|
||||||
|
(ekp-auto-justify-mode 1)
|
||||||
|
(ekp-live-commit-evaluator--self-insert ?x)
|
||||||
|
(let* ((metadata
|
||||||
|
(ekp-live-commit-evaluator--metadata
|
||||||
|
engine gc-mode width rows))
|
||||||
|
(samples
|
||||||
|
(ekp-live-commit-evaluator--instrument
|
||||||
|
(lambda ()
|
||||||
|
(ekp-live-commit-evaluator--collect metadata)))))
|
||||||
|
`((engine . ,engine) (gc_mode . ,gc-mode)
|
||||||
|
(width . ,width) (rows . ,rows)
|
||||||
|
(source_hash . ,(ekp-live-commit-evaluator--source-hash))
|
||||||
|
(projection_hash
|
||||||
|
. ,(ekp-live-commit-evaluator--projection-hash))
|
||||||
|
(conflicts . ,(length ekp-buffer--conflicts))
|
||||||
|
(samples . ,samples)))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--run-cell-with-gc
|
||||||
|
(engine gc-mode width rows)
|
||||||
|
"Run an ENGINE cell under GC-MODE at WIDTH and ROWS."
|
||||||
|
(let ((gc-cons-threshold
|
||||||
|
(if (equal gc-mode "gc-excluded")
|
||||||
|
most-positive-fixnum
|
||||||
|
ekp-live-commit-evaluator--default-gc-threshold)))
|
||||||
|
(garbage-collect)
|
||||||
|
(ekp-live-commit-evaluator--run-cell engine gc-mode width rows)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--measure-engine (engine)
|
||||||
|
"Measure every matrix cell for ENGINE."
|
||||||
|
(let ((ekp-use-c-module (equal engine "c"))
|
||||||
|
scenarios)
|
||||||
|
(dolist (gc-mode
|
||||||
|
(ekp-live-commit-evaluator--env-strings
|
||||||
|
"EKP_LIVE_COMMIT_GC_MODES"
|
||||||
|
'("gc-excluded" "default-gc")))
|
||||||
|
(dolist (width
|
||||||
|
(ekp-live-commit-evaluator--env-numbers
|
||||||
|
"EKP_LIVE_COMMIT_WIDTHS"
|
||||||
|
ekp-live-commit-evaluator--widths))
|
||||||
|
(dolist (rows
|
||||||
|
(ekp-live-commit-evaluator--env-numbers
|
||||||
|
"EKP_LIVE_COMMIT_ROWS"
|
||||||
|
ekp-live-commit-evaluator--rows))
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(push
|
||||||
|
(ekp-live-commit-evaluator--run-cell-with-gc
|
||||||
|
engine gc-mode width rows)
|
||||||
|
scenarios))))
|
||||||
|
(nreverse scenarios)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--measure-round ()
|
||||||
|
"Measure one evaluator round and return a JSON-compatible record."
|
||||||
|
(require 'ekp)
|
||||||
|
(require 'ekp-buffer)
|
||||||
|
(ekp-c-module-load)
|
||||||
|
(unless (ekp--c-available-p)
|
||||||
|
(error "C module did not load"))
|
||||||
|
`((label . ,(or (getenv "EKP_LIVE_COMMIT_LABEL") "unknown"))
|
||||||
|
(round . ,(ekp-live-commit-evaluator--env-number
|
||||||
|
"EKP_LIVE_COMMIT_ROUND" 0))
|
||||||
|
(module_version . ,(ekp-c-version))
|
||||||
|
(scenarios
|
||||||
|
. ,(cl-mapcan
|
||||||
|
#'ekp-live-commit-evaluator--measure-engine
|
||||||
|
(ekp-live-commit-evaluator--env-strings
|
||||||
|
"EKP_LIVE_COMMIT_ENGINES" '("c" "elisp"))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--write-record (record)
|
||||||
|
"Append JSON RECORD to `EKP_LIVE_COMMIT_OUTPUT'."
|
||||||
|
(let ((path (getenv "EKP_LIVE_COMMIT_OUTPUT")))
|
||||||
|
(unless path
|
||||||
|
(error "EKP_LIVE_COMMIT_OUTPUT is required"))
|
||||||
|
(write-region (concat (json-encode record) "\n") nil path t 'silent)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--read-jsonl (path)
|
||||||
|
"Read JSON objects from PATH."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents path)
|
||||||
|
(mapcar
|
||||||
|
(lambda (line)
|
||||||
|
(json-parse-string line :object-type 'alist :array-type 'list))
|
||||||
|
(split-string (buffer-string) "\n" t))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--samples (records)
|
||||||
|
"Return all flat samples from RECORDS."
|
||||||
|
(cl-loop for record in records
|
||||||
|
append
|
||||||
|
(cl-loop for scenario in (alist-get 'scenarios record)
|
||||||
|
append (alist-get 'samples scenario))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--matching-samples
|
||||||
|
(records event engine gc-mode &optional width)
|
||||||
|
"Return RECORDS samples matching EVENT, ENGINE, GC-MODE, and WIDTH."
|
||||||
|
(cl-remove-if-not
|
||||||
|
(lambda (sample)
|
||||||
|
(and (equal (alist-get 'event_kind sample) event)
|
||||||
|
(equal (alist-get 'engine sample) engine)
|
||||||
|
(equal (alist-get 'gc_mode sample) gc-mode)
|
||||||
|
(or (null width) (= (alist-get 'width sample) width))))
|
||||||
|
(ekp-live-commit-evaluator--samples records)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--percentile (values percentile)
|
||||||
|
"Return nearest-rank PERCENTILE from VALUES."
|
||||||
|
(let* ((sorted (sort (copy-sequence values) #'<))
|
||||||
|
(rank (max 0 (1- (ceiling (* percentile (length sorted)))))))
|
||||||
|
(unless sorted
|
||||||
|
(error "No samples for percentile"))
|
||||||
|
(nth rank sorted)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--statistics (samples key)
|
||||||
|
"Return percentile statistics for numeric KEY in SAMPLES."
|
||||||
|
(let ((values (mapcar (lambda (sample) (alist-get key sample)) samples)))
|
||||||
|
`((p50 . ,(ekp-live-commit-evaluator--percentile values 0.50))
|
||||||
|
(p95 . ,(ekp-live-commit-evaluator--percentile values 0.95))
|
||||||
|
(p99 . ,(ekp-live-commit-evaluator--percentile values 0.99)))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--improvement (baseline candidate)
|
||||||
|
"Return percentage improvement from BASELINE to CANDIDATE."
|
||||||
|
(if (zerop baseline)
|
||||||
|
0.0
|
||||||
|
(* 100.0 (/ (- baseline candidate) baseline))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--comparison (baseline candidate)
|
||||||
|
"Return timing and layer comparison for BASELINE and CANDIDATE samples."
|
||||||
|
(let ((base-total
|
||||||
|
(ekp-live-commit-evaluator--statistics baseline 'total_ms))
|
||||||
|
(cand-total
|
||||||
|
(ekp-live-commit-evaluator--statistics candidate 'total_ms)))
|
||||||
|
`((baseline . ((total . ,base-total)
|
||||||
|
(plan . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
baseline 'plan_ms))
|
||||||
|
(paragraph . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
baseline 'para_ms))
|
||||||
|
(dp . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
baseline 'dp_ms))
|
||||||
|
(module . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
baseline 'module_ms))
|
||||||
|
(publication
|
||||||
|
. ,(ekp-live-commit-evaluator--statistics
|
||||||
|
baseline 'publication_ms))
|
||||||
|
(signature . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
baseline 'signature_ms))
|
||||||
|
(transaction . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
baseline 'transaction_ms))))
|
||||||
|
(candidate . ((total . ,cand-total)
|
||||||
|
(plan . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
candidate 'plan_ms))
|
||||||
|
(paragraph . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
candidate 'para_ms))
|
||||||
|
(dp . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
candidate 'dp_ms))
|
||||||
|
(module . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
candidate 'module_ms))
|
||||||
|
(publication
|
||||||
|
. ,(ekp-live-commit-evaluator--statistics
|
||||||
|
candidate 'publication_ms))
|
||||||
|
(signature . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
candidate 'signature_ms))
|
||||||
|
(transaction . ,(ekp-live-commit-evaluator--statistics
|
||||||
|
candidate 'transaction_ms))))
|
||||||
|
(p95_improvement_pct
|
||||||
|
. ,(ekp-live-commit-evaluator--improvement
|
||||||
|
(alist-get 'p95 base-total) (alist-get 'p95 cand-total)))
|
||||||
|
(p99_improvement_pct
|
||||||
|
. ,(ekp-live-commit-evaluator--improvement
|
||||||
|
(alist-get 'p99 base-total) (alist-get 'p99 cand-total))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--scenario-key (scenario)
|
||||||
|
"Return the comparison key for SCENARIO."
|
||||||
|
(mapcar (lambda (key) (alist-get key scenario))
|
||||||
|
'(engine gc_mode width rows)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--scenario-layout (scenario)
|
||||||
|
"Return SCENARIO's source and projection identity."
|
||||||
|
(list (alist-get 'source_hash scenario)
|
||||||
|
(alist-get 'projection_hash scenario)
|
||||||
|
(alist-get 'conflicts scenario)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--layout-table (records)
|
||||||
|
"Return a sorted layout identity table for RECORDS."
|
||||||
|
(sort
|
||||||
|
(cl-loop for record in records
|
||||||
|
append
|
||||||
|
(cl-loop for scenario in (alist-get 'scenarios record)
|
||||||
|
collect
|
||||||
|
(cons (ekp-live-commit-evaluator--scenario-key scenario)
|
||||||
|
(ekp-live-commit-evaluator--scenario-layout
|
||||||
|
scenario))))
|
||||||
|
(lambda (left right)
|
||||||
|
(string< (prin1-to-string left) (prin1-to-string right)))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--engine-layouts (records engine)
|
||||||
|
"Return ENGINE layout identities with engine removed from RECORDS."
|
||||||
|
(sort
|
||||||
|
(cl-loop for record in records
|
||||||
|
append
|
||||||
|
(cl-loop for scenario in (alist-get 'scenarios record)
|
||||||
|
when (equal (alist-get 'engine scenario) engine)
|
||||||
|
collect
|
||||||
|
(list
|
||||||
|
(cdr (ekp-live-commit-evaluator--scenario-key scenario))
|
||||||
|
(ekp-live-commit-evaluator--scenario-layout scenario))))
|
||||||
|
(lambda (left right)
|
||||||
|
(string< (prin1-to-string left) (prin1-to-string right)))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--layout-parity-p (baseline candidate)
|
||||||
|
"Return non-nil when all BASELINE and CANDIDATE layouts match."
|
||||||
|
(and (equal (ekp-live-commit-evaluator--layout-table baseline)
|
||||||
|
(ekp-live-commit-evaluator--layout-table candidate))
|
||||||
|
(equal (ekp-live-commit-evaluator--engine-layouts baseline "c")
|
||||||
|
(ekp-live-commit-evaluator--engine-layouts baseline "elisp"))
|
||||||
|
(equal (ekp-live-commit-evaluator--engine-layouts candidate "c")
|
||||||
|
(ekp-live-commit-evaluator--engine-layouts
|
||||||
|
candidate "elisp"))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--zero-work-p (records)
|
||||||
|
"Return non-nil when every ordinary key in RECORDS does zero layout work."
|
||||||
|
(cl-every
|
||||||
|
(lambda (sample)
|
||||||
|
(and (= (alist-get 'publish_calls sample) 0)
|
||||||
|
(= (alist-get 'plan_calls sample) 0)
|
||||||
|
(= (alist-get 'module_calls sample) 0)))
|
||||||
|
(cl-remove-if-not
|
||||||
|
(lambda (sample)
|
||||||
|
(equal (alist-get 'event_kind sample) "ordinary-key"))
|
||||||
|
(ekp-live-commit-evaluator--samples records))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--valid-gc-excluded-p (records)
|
||||||
|
"Return non-nil when excluded-GC samples in RECORDS contain no GC."
|
||||||
|
(cl-every
|
||||||
|
(lambda (sample)
|
||||||
|
(or (not (equal (alist-get 'gc_mode sample) "gc-excluded"))
|
||||||
|
(= (alist-get 'gc_events sample) 0)))
|
||||||
|
(ekp-live-commit-evaluator--samples records)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--conflict-free-p (records)
|
||||||
|
"Return non-nil when every scenario in RECORDS has no conflicts."
|
||||||
|
(cl-every
|
||||||
|
(lambda (record)
|
||||||
|
(cl-every
|
||||||
|
(lambda (scenario)
|
||||||
|
(= (alist-get 'conflicts scenario) 0))
|
||||||
|
(alist-get 'scenarios record)))
|
||||||
|
records))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--ordinary-p99 (records)
|
||||||
|
"Return excluded-GC ordinary-key p99 for RECORDS."
|
||||||
|
(let ((samples
|
||||||
|
(cl-remove-if-not
|
||||||
|
(lambda (sample)
|
||||||
|
(and (equal (alist-get 'event_kind sample) "ordinary-key")
|
||||||
|
(equal (alist-get 'gc_mode sample) "gc-excluded")))
|
||||||
|
(ekp-live-commit-evaluator--samples records))))
|
||||||
|
(alist-get 'p99
|
||||||
|
(ekp-live-commit-evaluator--statistics samples 'total_ms))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--width-report
|
||||||
|
(baseline candidate engine width)
|
||||||
|
"Compare BASELINE and CANDIDATE structural commits for ENGINE at WIDTH."
|
||||||
|
(let ((base
|
||||||
|
(ekp-live-commit-evaluator--matching-samples
|
||||||
|
baseline "structural-commit" engine "gc-excluded" width))
|
||||||
|
(cand
|
||||||
|
(ekp-live-commit-evaluator--matching-samples
|
||||||
|
candidate "structural-commit" engine "gc-excluded" width)))
|
||||||
|
(ekp-live-commit-evaluator--comparison base cand)))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--target-pass-p (report)
|
||||||
|
"Return non-nil when WIDTH-80 REPORT meets target and improvement gates."
|
||||||
|
(let* ((candidate (alist-get 'candidate report))
|
||||||
|
(total (alist-get 'total candidate)))
|
||||||
|
(and (<= (alist-get 'p95 total)
|
||||||
|
ekp-live-commit-evaluator--target-ms)
|
||||||
|
(<= (alist-get 'p99 total)
|
||||||
|
ekp-live-commit-evaluator--target-ms)
|
||||||
|
(>= (alist-get 'p95_improvement_pct report)
|
||||||
|
ekp-live-commit-evaluator--minimum-improvement)
|
||||||
|
(>= (alist-get 'p99_improvement_pct report)
|
||||||
|
ekp-live-commit-evaluator--minimum-improvement))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--non-regression-p
|
||||||
|
(baseline candidate engine width)
|
||||||
|
"Return non-nil when ENGINE at WIDTH avoids a material p95 regression."
|
||||||
|
(let* ((report
|
||||||
|
(ekp-live-commit-evaluator--width-report
|
||||||
|
baseline candidate engine width))
|
||||||
|
(base (alist-get 'p95
|
||||||
|
(alist-get 'total (alist-get 'baseline report))))
|
||||||
|
(cand (alist-get 'p95
|
||||||
|
(alist-get 'total (alist-get 'candidate report)))))
|
||||||
|
(<= cand (* base
|
||||||
|
(+ 1.0
|
||||||
|
(/ ekp-live-commit-evaluator--maximum-regression
|
||||||
|
100.0))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--all-widths-pass-p
|
||||||
|
(baseline candidate)
|
||||||
|
"Return non-nil when no engine materially regresses at any width."
|
||||||
|
(let ((widths
|
||||||
|
(ekp-live-commit-evaluator--env-numbers
|
||||||
|
"EKP_LIVE_COMMIT_WIDTHS"
|
||||||
|
ekp-live-commit-evaluator--widths)))
|
||||||
|
(cl-every
|
||||||
|
(lambda (engine)
|
||||||
|
(cl-every
|
||||||
|
(lambda (width)
|
||||||
|
(ekp-live-commit-evaluator--non-regression-p
|
||||||
|
baseline candidate engine width))
|
||||||
|
widths))
|
||||||
|
'("c" "elisp"))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--report (baseline candidate)
|
||||||
|
"Return the final evaluator report for BASELINE and CANDIDATE."
|
||||||
|
(let* ((c80 (ekp-live-commit-evaluator--width-report
|
||||||
|
baseline candidate "c" 80))
|
||||||
|
(elisp80 (ekp-live-commit-evaluator--width-report
|
||||||
|
baseline candidate "elisp" 80))
|
||||||
|
(ordinary-p99
|
||||||
|
(ekp-live-commit-evaluator--ordinary-p99 candidate))
|
||||||
|
(parity (ekp-live-commit-evaluator--layout-parity-p
|
||||||
|
baseline candidate))
|
||||||
|
(zero-work
|
||||||
|
(and (ekp-live-commit-evaluator--zero-work-p baseline)
|
||||||
|
(ekp-live-commit-evaluator--zero-work-p candidate)))
|
||||||
|
(gc-valid
|
||||||
|
(and (ekp-live-commit-evaluator--valid-gc-excluded-p baseline)
|
||||||
|
(ekp-live-commit-evaluator--valid-gc-excluded-p candidate)))
|
||||||
|
(conflict-free
|
||||||
|
(and (ekp-live-commit-evaluator--conflict-free-p baseline)
|
||||||
|
(ekp-live-commit-evaluator--conflict-free-p candidate)))
|
||||||
|
(all-widths
|
||||||
|
(ekp-live-commit-evaluator--all-widths-pass-p
|
||||||
|
baseline candidate))
|
||||||
|
(pass
|
||||||
|
(and parity zero-work gc-valid conflict-free all-widths
|
||||||
|
(<= ordinary-p99
|
||||||
|
ekp-live-commit-evaluator--ordinary-target-ms)
|
||||||
|
(ekp-live-commit-evaluator--target-pass-p c80)
|
||||||
|
(ekp-live-commit-evaluator--target-pass-p elisp80))))
|
||||||
|
`((pass . ,pass) (layout_parity . ,parity)
|
||||||
|
(ordinary_zero_work . ,zero-work)
|
||||||
|
(gc_excluded_valid . ,gc-valid)
|
||||||
|
(conflict_free . ,conflict-free)
|
||||||
|
(all_widths_non_regression . ,all-widths)
|
||||||
|
(ordinary_candidate_p99_ms . ,ordinary-p99)
|
||||||
|
(width_80 . ((c . ,c80) (elisp . ,elisp80))))))
|
||||||
|
|
||||||
|
(defun ekp-live-commit-evaluator--compare ()
|
||||||
|
"Compare raw JSONL paths from the environment and enforce the contract."
|
||||||
|
(let* ((baseline
|
||||||
|
(ekp-live-commit-evaluator--read-jsonl
|
||||||
|
(getenv "EKP_LIVE_COMMIT_BASELINE_JSONL")))
|
||||||
|
(candidate
|
||||||
|
(ekp-live-commit-evaluator--read-jsonl
|
||||||
|
(getenv "EKP_LIVE_COMMIT_CANDIDATE_JSONL")))
|
||||||
|
(report
|
||||||
|
(ekp-live-commit-evaluator--report baseline candidate))
|
||||||
|
(path (getenv "EKP_LIVE_COMMIT_REPORT")))
|
||||||
|
(when path
|
||||||
|
(write-region (concat (json-encode report) "\n")
|
||||||
|
nil path nil 'silent))
|
||||||
|
(princ (concat (json-encode report) "\n"))
|
||||||
|
(unless (eq (alist-get 'pass report) t)
|
||||||
|
(kill-emacs 1))))
|
||||||
|
|
||||||
|
(if (equal (getenv "EKP_LIVE_COMMIT_MODE") "compare")
|
||||||
|
(ekp-live-commit-evaluator--compare)
|
||||||
|
(ekp-live-commit-evaluator--write-record
|
||||||
|
(ekp-live-commit-evaluator--measure-round)))
|
||||||
|
|
||||||
|
;;; ekp-live-commit-evaluator.el ends here
|
||||||
@ -790,6 +790,85 @@ module is bypassed automatically (it has no looseness support)."
|
|||||||
24))
|
24))
|
||||||
(should (= calls 1)))))
|
(should (= calls 1)))))
|
||||||
|
|
||||||
|
(defun ekp-test--exact-append-plan (plan text width)
|
||||||
|
"Return PLAN extended to TEXT after proving fresh parity at WIDTH."
|
||||||
|
(let ((incremental (ekp-layout-plan-append plan text width)))
|
||||||
|
(should incremental)
|
||||||
|
(let ((actual (ekp-render-layout-string incremental)))
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(should
|
||||||
|
(equal-including-properties
|
||||||
|
actual
|
||||||
|
(ekp-render-layout-string
|
||||||
|
(ekp-layout-plan text width)))))
|
||||||
|
incremental))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-layout-plan-append-matches-fresh-plan ()
|
||||||
|
"Incremental plain-text appends must equal a fresh global plan."
|
||||||
|
(ekp-tests--with-clean-state
|
||||||
|
(dolist (use-c (if (ekp-tests--c-available) '(nil t) '(nil)))
|
||||||
|
(let* ((ekp-use-c-module use-c)
|
||||||
|
(width 24)
|
||||||
|
(text "alpha beta gamma changes responsive")
|
||||||
|
(plan (ekp-layout-plan text width)))
|
||||||
|
(dolist (suffix '("ly" " " "a" " 中文" " mixed" " continuation"))
|
||||||
|
(setq text (concat text suffix)
|
||||||
|
plan (ekp-test--exact-append-plan plan text width)))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-layout-plan-append-varied-chains-match-fresh ()
|
||||||
|
"Diverse character-by-character append chains retain exact parity."
|
||||||
|
(ekp-tests--with-clean-state
|
||||||
|
(dolist (use-c (if (ekp-tests--c-available) '(nil t) '(nil)))
|
||||||
|
(dolist (case '((justify nil 18 "ly 中文,punctuation.")
|
||||||
|
(justify 4 27 "extraordinary-hyphenation")
|
||||||
|
(ragged-right nil 21 " mixed 拉丁 alpha beta")
|
||||||
|
(center nil 31 " repeated spaces 文末")))
|
||||||
|
(let* ((ekp-use-c-module use-c)
|
||||||
|
(ekp-alignment (nth 0 case))
|
||||||
|
(ekp-first-line-indent (nth 1 case))
|
||||||
|
(width (nth 2 case))
|
||||||
|
(text "alpha beta gamma")
|
||||||
|
(plan (ekp-layout-plan text width)))
|
||||||
|
(dolist (character (string-to-list (nth 3 case)))
|
||||||
|
(setq text (concat text (char-to-string character))
|
||||||
|
plan (ekp-test--exact-append-plan
|
||||||
|
plan text width))))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-layout-plan-append-recomputes-dirty-boundary ()
|
||||||
|
"The first retokenized boundary must not retain stale break metadata."
|
||||||
|
(ekp-tests--with-clean-state
|
||||||
|
(dolist (use-c (if (ekp-tests--c-available) '(nil t) '(nil)))
|
||||||
|
(let* ((ekp-use-c-module use-c)
|
||||||
|
(text "alpha beta g-o。ycfrnqcc。( (")
|
||||||
|
(width 12)
|
||||||
|
(plan (ekp-layout-plan text width)))
|
||||||
|
(ekp-test--exact-append-plan plan (concat text "l") width)))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-layout-plan-append-rejects-unsafe-contexts ()
|
||||||
|
"The append path rejects properties and non-1D layout contexts."
|
||||||
|
(ekp-tests--with-clean-state
|
||||||
|
(let* ((text "alpha beta gamma")
|
||||||
|
(plan (ekp-layout-plan text 24))
|
||||||
|
(plain (concat text " delta"))
|
||||||
|
(styled (copy-sequence plain)))
|
||||||
|
(add-text-properties 0 5 '(face bold) styled)
|
||||||
|
(should-not (ekp-layout-plan-append plan styled 24))
|
||||||
|
(should-not
|
||||||
|
(ekp-layout-plan-append plan (concat text "\nnext") 24))
|
||||||
|
(should-not
|
||||||
|
(ekp-layout-plan-append plan (concat text "\ttail") 24))
|
||||||
|
(should-not (ekp-layout-plan-append plan plain 25))
|
||||||
|
(let ((ekp-looseness 1))
|
||||||
|
(should-not (ekp-layout-plan-append plan plain 24)))
|
||||||
|
(let ((ekp-parshape '((0 . 24))))
|
||||||
|
(should-not (ekp-layout-plan-append plan plain 24)))
|
||||||
|
(let ((ekp-first-line-indent 4))
|
||||||
|
(should-not (ekp-layout-plan-append plan plain 24)))
|
||||||
|
(let ((ekp-alignment 'center))
|
||||||
|
(should-not (ekp-layout-plan-append plan plain 24)))
|
||||||
|
(let ((ekp-latin-lang "de_DE"))
|
||||||
|
(should-not (ekp-layout-plan-append plan plain 24))))))
|
||||||
|
|
||||||
(ert-deftest ekp-test-layout-plan-omits-zero-source-zero-width-gaps ()
|
(ert-deftest ekp-test-layout-plan-omits-zero-source-zero-width-gaps ()
|
||||||
"The projection plan must omit gaps that cannot install a property."
|
"The projection plan must omit gaps that cannot install a property."
|
||||||
(ekp-tests--with-clean-state
|
(ekp-tests--with-clean-state
|
||||||
|
|||||||
79
tests/run-live-commit-evaluator.sh
Executable file
79
tests/run-live-commit-evaluator.sh
Executable file
@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Evaluate task030 against the frozen portable baseline.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
||||||
|
GOAL="$ROOT/.omx/goals/performance/narrow-live-commit"
|
||||||
|
BASELINE_ROOT="$GOAL/baseline/source"
|
||||||
|
RAW="$GOAL/raw"
|
||||||
|
ROUNDS=${EKP_LIVE_COMMIT_ROUNDS:-4}
|
||||||
|
|
||||||
|
if test -n "${EMACS:-}"; then
|
||||||
|
EMACS_BIN=$EMACS
|
||||||
|
elif command -v emacs >/dev/null 2>&1; then
|
||||||
|
EMACS_BIN=$(command -v emacs)
|
||||||
|
elif test -x /Applications/Emacs.app/Contents/MacOS/Emacs-arm64-11; then
|
||||||
|
EMACS_BIN=/Applications/Emacs.app/Contents/MacOS/Emacs-arm64-11
|
||||||
|
else
|
||||||
|
printf '%s\n' "live-commit-evaluator: Emacs executable not found" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
test -f "$BASELINE_ROOT/ekp.el"
|
||||||
|
test -f "$BASELINE_ROOT/ekp-buffer.el"
|
||||||
|
test -f "$BASELINE_ROOT/ekp_c/ekp.dylib"
|
||||||
|
test -f "$BASELINE_ROOT/dictionaries/hyph_en_US.dic"
|
||||||
|
|
||||||
|
make -C "$ROOT/ekp_c" clean all PROFILE=portable
|
||||||
|
mkdir -p "$RAW"
|
||||||
|
BASELINE_JSONL="$RAW/baseline.jsonl"
|
||||||
|
CANDIDATE_JSONL="$RAW/candidate.jsonl"
|
||||||
|
REPORT="$GOAL/latest-report.json"
|
||||||
|
: >"$BASELINE_JSONL"
|
||||||
|
: >"$CANDIDATE_JSONL"
|
||||||
|
|
||||||
|
run_round()
|
||||||
|
{
|
||||||
|
code_root=$1
|
||||||
|
label=$2
|
||||||
|
round=$3
|
||||||
|
output=$4
|
||||||
|
EKP_LIVE_COMMIT_LABEL=$label \
|
||||||
|
EKP_LIVE_COMMIT_ROUND=$round \
|
||||||
|
EKP_LIVE_COMMIT_OUTPUT=$output \
|
||||||
|
"$EMACS_BIN" -Q --batch -L "$code_root" -L "$ROOT/tests" \
|
||||||
|
-l "$ROOT/tests/ekp-live-commit-evaluator.el"
|
||||||
|
}
|
||||||
|
|
||||||
|
round=1
|
||||||
|
while test "$round" -le "$ROUNDS"; do
|
||||||
|
if test $((round % 2)) -eq 1; then
|
||||||
|
run_round "$BASELINE_ROOT" baseline "$round" "$BASELINE_JSONL"
|
||||||
|
run_round "$ROOT" candidate "$round" "$CANDIDATE_JSONL"
|
||||||
|
else
|
||||||
|
run_round "$ROOT" candidate "$round" "$CANDIDATE_JSONL"
|
||||||
|
run_round "$BASELINE_ROOT" baseline "$round" "$BASELINE_JSONL"
|
||||||
|
fi
|
||||||
|
round=$((round + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
EKP_LIVE_COMMIT_MODE=compare \
|
||||||
|
EKP_LIVE_COMMIT_BASELINE_JSONL="$BASELINE_JSONL" \
|
||||||
|
EKP_LIVE_COMMIT_CANDIDATE_JSONL="$CANDIDATE_JSONL" \
|
||||||
|
EKP_LIVE_COMMIT_REPORT="$REPORT" \
|
||||||
|
"$EMACS_BIN" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||||
|
-l "$ROOT/tests/ekp-live-commit-evaluator.el"
|
||||||
|
|
||||||
|
if test "${EKP_LIVE_COMMIT_SKIP_AUDIT:-0}" = 1; then
|
||||||
|
printf '%s\n' "live-commit-evaluator: performance and parity gates pass"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$ROOT/tests/run-tests.sh" "$EMACS_BIN"
|
||||||
|
"$EMACS_BIN" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||||
|
-l "$ROOT/tests/ekp-fuzz.el"
|
||||||
|
"$ROOT/tests/check-release.sh"
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
"live-commit-evaluator: performance, parity, ERT, fuzz, and release gates pass"
|
||||||
Loading…
Reference in New Issue
Block a user