Skip to content

fix(parser): fall back from (( … )) arith to nested subshells#48

Merged
mpecan merged 1 commit into
mainfrom
feat/42-arith-subshell-fallback
Apr 14, 2026
Merged

fix(parser): fall back from (( … )) arith to nested subshells#48
mpecan merged 1 commit into
mainfrom
feat/42-arith-subshell-fallback

Conversation

@mpecan
Copy link
Copy Markdown
Owner

@mpecan mpecan commented Apr 14, 2026

Summary

  • Adds the ((…)) → nested-subshell fallback that bash's parser performs when the body of ((…)) is not a valid arithmetic expression. Input like ((x\n> 0)\n) is now parsed as two nested subshells instead of failing with expected )), got Eof.
  • Introduces a small LexerCheckpoint save/restore API (pos, line, peeked, last_token_end) on Lexer. The arith dispatch in parse_command_inner now tries parse_arith_command first and, on a RableError::MatchedPair from read_until_double_paren, restores the checkpoint and re-parses via parse_subshell. Other arith-side errors still propagate.
  • Removes reserved_word_as_word 2 from KNOWN_ORACLE_FAILURES in tests/integration.rs.

Closes #42.

Test plan

  • cargo fmt
  • cargo clippy --all-targets -- -D warnings — no warnings
  • cargo test — 238 passed, 0 failed
  • New unit tests in src/parser/tests.rs:
    • arith_command_with_inner_parens(( (1+2) )) still parses as arithmetic
    • double_paren_falls_back_to_nested_subshells — the exact issue input
    • double_paren_fallback_preserves_following_command — lexer state is clean after fallback
    • double_paren_fallback_inside_if — fallback works when dispatched from inside a compound construct
  • reserved_word_as_word 2 now passes in oracle_bash_valid_divergences

Bash resolves the `((` ambiguity by tentatively parsing as an arithmetic
command and re-parsing as two nested subshells `( ( … ) )` when the body
is not a valid arithmetic expression. Rable committed to arith as soon
as it saw `((` and errored out on e.g. `((x\n> 0)\n)`.

Add a small `LexerCheckpoint` save/restore API to `Lexer` and wrap the
`((` dispatch in `parse_command_inner` with a try-then-fallback: on a
`MatchedPair` error from `read_until_double_paren` (the precise signal
that `))` couldn't be found at depth 0), rewind and call
`parse_subshell` instead. Other arith-side errors still propagate.

Closes #42.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mpecan mpecan merged commit 1437f00 into main Apr 14, 2026
5 checks passed
@mpecan mpecan deleted the feat/42-arith-subshell-fallback branch April 14, 2026 20:52
mpecan pushed a commit that referenced this pull request Apr 19, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.2.0](rable-v0.1.15...rable-v0.2.0)
(2026-04-18)


### ⚠ BREAKING CHANGES

* tighten lexer API surface and relocate WordSpan to ast
([#70](#70))

### Bug Fixes

* **format:** align cmdsub reformatter with bash canonical form
([#49](#49))
([c7a4411](c7a4411))
* **lexer:** accept sloppy heredoc terminator in cmdsub mode
([#50](#50))
([40f394f](40f394f))
* **lexer:** backticks opaque when content is invalid
([#71](#71))
([e72166f](e72166f)),
closes [#38](#38)
* **lexer:** disable reserved-word recognition after assignment words
([#44](#44))
([42e1fc0](42e1fc0))
* **lexer:** stop treating ]] and unbalanced [...] as special outside
conditionals ([#45](#45))
([4bf5a5c](4bf5a5c))
* **parser:** fall back from (( … )) arith to nested subshells
([#48](#48))
([1437f00](1437f00))


### Code Refactoring

* **format:** introduce Formatter struct
([#65](#65))
([d965a8f](d965a8f))
* **lexer:** drop Result&lt;Token&gt; wrapper from operator readers
([#62](#62))
([d52a841](d52a841))
* **lexer:** split read_word_token into classify + advance + dispatch
helpers ([#63](#63))
([3ba09f5](3ba09f5))
* **parser:** extract fill_heredoc_contents visitor helpers
([#68](#68))
([40e6165](40e6165))
* **parser:** extract helpers from three oversize parsers
([#69](#69))
([25d0762](25d0762))
* **sexp:** dispatch NodeKind Display to per-category helpers
([#66](#66))
([44b0330](44b0330))
* **sexp:** table-drive ANSI-C escape dispatch
([#67](#67))
([91a5267](91a5267))
* tighten lexer API surface and relocate WordSpan to ast
([#70](#70))
([5171d01](5171d01))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: repository-butler[bot] <166800726+repository-butler[bot]@users.noreply.github.com>
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.

parser: (( ... )) should fall back to nested subshells when body is not a valid arithmetic expression

1 participant