Skip to content

release: 3.3.0 — frecency suggestions, Makefile completion and quoted cmdsub fixes - #195

Open
SMOSTAFAH1 wants to merge 12 commits into
mainfrom
develop
Open

SMOSTAFAH1 wants to merge 12 commits into
mainfrom
develop

Conversation

@SMOSTAFAH1

Copy link
Copy Markdown
Member

What & why

Closes #

How I verified it

  • make re (SAFE=1 debug) builds clean, no warnings
  • make re OPT=1 (SAFE=0 optimized) builds clean, no warnings
  • make test is green (libc heap)
  • cd tests && ./verify_alloc.sh green on both SAFE=1 and SAFE=0
  • Added test(s) covering this bug/feature (and nearby cases)
  • No leaks — checked under ASan/LSan (SAFE=1) and/or HELLISH_ALLOC_STATS
    (SAFE=0)
  • No crashes/segfaults/UB, including on edge cases (empty input, bad quotes,
    huge args, deep nesting)
  • make norm clean on touched files (no in-function comments, ≤80 cols,
    no new globals)
  • Documented: code comments (Doxygen for complex fns), commit body, this PR

Notes / trade-offs

qwO-Owp and others added 10 commits October 18, 2026 20:39
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>
@SMOSTAFAH1 SMOSTAFAH1 self-assigned this Nov 10, 2026
@SMOSTAFAH1 SMOSTAFAH1 changed the title Develop release: 3.3.0 — frecency suggestions, Makefile completion and quoted cmdsub fixes Nov 11, 2026
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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants