Skip to content

Selection: accept , and whitespace as list separators (#116) - #118

Merged
vv137 merged 2 commits into
mainfrom
fix/116-comma-selection-lists
Aug 2, 2026
Merged

Selection: accept , and whitespace as list separators (#116)#118
vv137 merged 2 commits into
mainfrom
fix/116-comma-selection-lists

Conversation

@vv137

@vv137 vv137 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Fixes #116.

The bug

resi 2,3,6 silently selected only residue 2 — no warning, no error, exit 0.

The comma never reaches the selection grammar: CommandParser::parse splits
argv on commas as well as whitespace, so :color red resi 2,3,6 arrives at the
parser as the string "resi 2 3 6". The grammar only accepted + between list
entries, so it read resi 2 and dropped the rest without a word.

That is the dangerous shape the issue calls out — a figure coloured from a
truncated selection looks entirely plausible and survives visual review.

The fix

Value lists now treat +, , and plain whitespace as the same separator:

:color red resi 2+3+6     " 26 atoms
:color red resi 2,3,6     " 26 atoms  (was 9 — resi 2 only)
:color red resi 2 3 6     " 26 atoms

Two prongs, because the comma survives on some paths and not others:

  • The tokenizer maps , to the + token, which covers commands that pass the
    raw argument tail through (:select, pca(...)/centroid(...)).
  • Value lists also accept bare adjacency, which is what a comma degrades into
    after CommandParser. A list ends at an operator or the next primary
    keyword, so chain A,B and name CA still parses as it reads, and - keeps
    its range meaning (resi 10-12,20 = 10, 11, 12, 20).

Also fixed, same shape

  • resn and element accepted a single value only — not even + lists.
  • imgt's numeric set stopped at the first non-+ token; its region names
    truncated on every separator (imgt cdr1,cdr2 was cdr1). An unknown region
    now voids the whole term rather than quietly narrowing to the ones that
    parsed.
  • imgt was missing from isPrimaryKeyword, so it neither terminated a list
    nor bounded a :loadalign file-glob tail.
  • Slash components split on a gap too: /1brs/A/10,11/ is 10+11, not residue
    1011.
  • docs/SCRIPTING.md promised pca(resi 1,2,3) == pca(resi 1 2 3). Both were
    truncating; both now work, and the comma→space pre-pass in ExprContext that
    existed to fake it is gone.

Verification

  • 59 pre-existing selection expressions (operators, slash forms, within/same
    /byres, object qualifiers, IMGT, nucleic selectors) produce byte-identical
    results before and after.
  • New unit tests assert +/,/space equivalence across all six keywords and
    both slash positions, and pin the boundaries: a list stops at an operator and
    at a primary keyword, and adjacency never overrides an explicit operator.
  • Full suite green (264 selection assertions).

Deliberately not in this PR

  • The general silence. Selection::parse still ignores unconsumed trailing
    tokens, so other malformed expressions stay quiet. Making that loud needs an
    error channel on Selection::parse and its ~16 call sites — filed separately.
  • :align chain=A,B. Same root cause one level up, but unreachable from the
    grammar: chain=A and B are separate argv tokens before any selection
    parsing happens. Filed separately.

🤖 Generated with Claude Code

vv137 added 2 commits August 2, 2026 15:30
`resi 2,3,6` silently selected only residue 2. CommandParser splits argv on
commas, so the expression reached the parser as the string "resi 2 3 6", and
the grammar only accepted `+` between list entries — everything past the
first value was dropped with no error and exit 0, which is how a figure can
be rendered from a truncated selection and still look plausible.

Value lists (chain, resn, resi, name, element, imgt) now treat `+`, `,` and
plain whitespace interchangeably. The list ends at an operator or the next
primary keyword, so `chain A,B and name CA` still parses as it reads, and
`-` keeps its range meaning (`resi 10-12,20` = 10, 11, 12, 20).

Also fixed along the way:
- `resn` and `element` accepted only a single value, not even `+` lists.
- The `imgt` numeric set stopped at the first non-`+` token.
- `imgt` was missing from isPrimaryKeyword, so it did not terminate a list
  or a `:loadalign` selection tail.
- Slash components now split on a gap too, so `/1brs/A/10,11/` is 10+11
  rather than residue 1011.

Verified against 59 existing selection expressions: byte-identical results
before and after.

Fixes #116
Review follow-up to the previous commit, no behavior change except where
noted.

- chain / resn / name / element were the same parse-a-list-then-compare-a-
  string-field code four times over, with the list rule copy-pasted into
  each. They now share parseValueList(field, kw, case), so the next change
  to the separator rule can't reach three of the four and miss the fourth.
  Their labels also stop claiming `chain A` for `chain A,B`.
- Hoisted isOperatorWord() next to isPrimaryKeyword and gave both the same
  case-insensitive matcher, instead of atListValue keeping a second copy of
  the operator vocabulary (and a lowercase string copy per token to use it).
- resi and imgt stated one list rule twice, in opposite polarity; both now
  call atListNumber().
- Tokenizer::scan() is private — it leaves `spaced` unset, so next()/peek()
  are the only correct entry points.
- Dropped the comma→space pre-pass in ExprContext: pca()/centroid() hand the
  paren body over verbatim and the grammar reads commas itself now. Its
  comment claimed the selection grammar never uses commas, which this branch
  makes false.

Behavior change: `imgt cdr1,cdr2` was still truncating to cdr1 — the region
spelling most users type, and one the docs in the previous commit already
promised was a list. Region names now list like the numeric form, and an
unknown region voids the whole term rather than quietly narrowing to the
regions that did parse.
@vv137
vv137 merged commit 28d6d8e into main Aug 2, 2026
2 checks passed
@vv137
vv137 deleted the fix/116-comma-selection-lists branch August 2, 2026 07:24
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.

resi with a comma-separated list silently selects only the first residue

1 participant