Repository navigation
release: 3.3.0 — frecency suggestions, Makefile completion and quoted cmdsub fixes - #195
Open
SMOSTAFAH1 wants to merge 12 commits into
Open
SMOSTAFAH1 wants to merge 12 commits into
SMOSTAFAH1 wants to merge 12 commits into
Conversation
The ghost, Alt-N/Alt-P and `make <TAB>` now prefer what is run often and lately: each distinct line scores the sum of 2^16 >> (age / 64) over its occurrences in the HELLISH_SUGGEST_MAX window, ties to the newest. HELLISH_SUGGEST_STRATEGY=recent restores newest-first; read every prompt. One ranking primitive (rl_suggest_rank) feeds the ghost and the cycle; it groups by sort over a reused scratch buffer instead of libft's t_hash, which strdups every key (per-key malloc churn). The fast path is kept: a score does not depend on the prefix, so a winner that still matches a longer line stays the best of the smaller set. make TAB turns readline's match sort off only for its own answer; every attempt starts with it on. libc qsort, not ft_qsort: ft_qsort pivots on the last element and is O(n^2) on runs of identical entries, which arrive already in ranking order: 6.76 s for one key over 20000 copies (F13 red), under 1.5 s with qsort. Tests: autosuggest F1-F13 plus a randomized model-vs-shell cycle check, case_cycle_dupes pinned to `recent` with a frecent twin (the only existing case whose order changes), make cases 25-29 (order, sort-flag restore, recent, user complete spec, -C/-f), frontend budget with 1000 distinct entries and Backspace: 6.0 syscalls / 1.1 writes per key. Mutants MF1-MF11 (age, frequency, tie, direction, strategy x2, window, sort flag, make history, cycle order, clean filter) each turn cases red.
feat(editor): rank suggestions and make targets by frecency (#140)
Unreleased: develop carries the change, main does not yet. The notes are ready for whoever cuts the release.
docs(release): draft the v3.3.0 notes (unreleased)
readline 8.3 made rl_free_line_state free the context of those three
readers on SIGINT, on the bet that an application's handler longjmps and
the reader never runs again. bash does, and readline83-003 taught
readline's own rl_getc to abort to the top level for handlers that
return. rl_getc_hook replaces that rl_getc and hands the reader EOF, so
the reader unwound through a freed `cxt`. On readline 8.3 at Debian's
patch level:
freed by: rl_getc_hook -> rl_take_signals -> rl_check_signals
-> _rl_handle_signal -> rl_free_line_state
-> _rl_state_sigcleanup -> _rl_isearch_cleanup
-> _rl_scxt_dispose
double free: rl_search_history -> _rl_isearch_cleanup
-> _rl_scxt_dispose
ASan reports the double free for ^R and M-x. Under a readline built with
ASan all three readers report a heap-use-after-free WRITE first, in
_rl_search_getchar. A release build takes a SIGSEGV on ^R, 3 runs of 3,
which is the login shell gone. The `ps2` failures in CI were the same
crash: that scenario shares the session ^R killed.
Guarding only rl_take_signals' own look is not enough, and the tests say
so: readline also looks at its deferred signal at the end of every
rl_redisplay, inside the search, and a ^C sent with the keys of a ^R came
through there. rl_intr.c puts a filter in front of readline's SIGINT
handler instead. While such a reader is up the signal is parked, the
reader gets EOF and unwinds with its context intact, the path every
readline before 8.3 took, and back in the main loop the ^C is replayed
into readline. Outside those readers the filter only forwards, so ^C
anywhere else is unchanged. Blocking SIGINT from the key's return to the
next wait would also work, but needs lifting around every widget and
completion, and a child forked on a path that misses it inherits it.
prompt_interrupt_test.py gains nsearch, mx and redraw;
rl_read_race_test.py gains nsearch and M-x. 48 and 34 ok on readline
8.1.2, 8.2.13 and 8.3 at patch levels 0, 3 and 6, and against a readline
built with ASan. On the old code the same files fail on 8.3.3, the redraw
scenario included.
Closes #194
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prompt_row_writes_test.py failed on ubuntu-26.04 with "7 of 13 rows were whole inside a single write()". No row was split. The trace has `GREEN<glyph> RESET \r GREEN<glyph> RESET` in one write(), every prompt: readline 8.3 before readline83-004 reprints a prompt holding multibyte characters from column 0, deciding on a screen position where it needs a buffer offset. Debian's 8.3-4 carries patches 001 to 003. Built here with 006 the second copy is gone and the old test passes; built at 003 it fails the way CI does. The invariant is that every row is whole in one write(). The test counted the writes holding a row and compared them with the rows, which assumes one row per write. It counts the rows now, so one really cut in two is still a failure, and it is not a change in what is pinned. The redraw itself is readline's, costs 28 bytes a prompt, lands on the same cells, and ends with 004. Refs #194 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sh_skip_quoted read a double-quoted span flat, to the next `"`. Inside a
$(...), that span can hold a nested $(...), ${...} or `...` with quotes of
its own, so find_cmd_sub_end stopped on the inner quote:
x=$(printf %s "$(echo b | tr -d '"')" c) # bash: bc
# hellish: $(printf %s "b" c)
The `'` after that wrong stop opened a span with no end, depth never reached
zero, and the outer substitution stayed in the word as literal text. No error,
exit 0, the command never ran, and `hellish -n` passes it. Only an odd count of
quote bytes inside shows it: an even one balances by accident, which is why
$(a "$(b "$c")") always worked.
Found by a born2root provisioner: a readiness check written this way failed on
every first boot, which runs it under hellish, and passed on every retry, which
runs it under bash.
sh_skip_quoted now delegates to span_skip, the scanner that already walks these
spans for the word expander, so no new walker is added. tests/cmdsub_nested_quote
has 18 cases compared against bash; 13 of them fail on 3.2.0.
Left alone, as a separate concern: a `case` with its `)` inside a nested
substitution, x=$(echo "$(case a in a) echo "q'r";; esac)"), is refused earlier
by the lexer's advance_cmdsub, which has no case handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
advance_cmdsub counted parens, so the `)` of a case pattern closed a $(...) that
sits inside a double-quoted word. The scan went on with the word's quote out of
step, and a valid command was refused:
x="$(case a in a) echo "q'r";; esac)" # bash: q'r
# hellish: syntax error
span_skip already walks these spans through casescan, the automaton #95 added for
the same `)`, so advance_cmdsub delegates to it and a second walker goes.
Stacked on fix/nested-cmdsub-quote: a case nested in a case needs that half too.
Five cases join tests/issue95_case_cmdsub.
Closes #191
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
banner2.c asks each release to replace the news table and wiki/whats-new.md together and to bump HELLISH_HEADER_REV, so the banner is shown again after the update rather than the next day. The table now holds what 3.3 adds: make <TAB> with most-run targets first, and the hint keys, which take the most-run match. The switch HELLISH_SUGGEST_STRATEGY=recent is 33 columns wide against the 32 the right column has at 80, so it stays on the page. The 3.2 guide stays below the 3.3 one, demoted one level: interactive.md links its #inline-hints anchor, and nothing else documents those keys. The page records one limit found while running its examples: HISTCONTROL=ignoredups or ignoreboth stores a back-to-back repeat once, so three `make re` in a row rank as one run and the newer `make clean` wins the tie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frecency ranking for hints and make <TAB> (#140), the two parser fixes (#190, #191) and the readline 8.3 ^C fix (#194). RELEASE.md has the summary; wiki/whats-new.md, which the banner links to, has the long form. Uses chore(release) because the commit-msg hook has no `release` type, which the 3.1.1 to 3.2.0 subjects used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arch Linux containers can fail package upgrades due to file conflicts (e.g. openssl-3 split /usr/lib/libssl.so.3). Overwrite prevents pacman from aborting in archlinux:base-devel.
La-Cebolla-Loquete
approved these changes
Nov 11, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #
How I verified it
make re(SAFE=1 debug) builds clean, no warningsmake re OPT=1(SAFE=0 optimized) builds clean, no warningsmake testis green (libc heap)cd tests && ./verify_alloc.shgreen on both SAFE=1 and SAFE=0HELLISH_ALLOC_STATS(SAFE=0)
huge args, deep nesting)
make normclean on touched files (no in-function comments, ≤80 cols,no new globals)
Notes / trade-offs