ADR-0024 Phase E: replay end-to-end

Migrate `replay <path>` to the walker. Shape is
Choice(StringLit, BarePath); the StringLit branch handles the
quoted form (with the existing `''` escape), and BarePath
handles the unquoted form.

Per ADR-0024's path-bearing UX change (already shipped for
import / export in Phase A), bare `replay` paths terminate at
the first whitespace byte. Paths with spaces require the
quoted form. The legacy `try_parse_replay_with_bare_path`
source-slice helper in dsl/parser.rs is removed; the
chumsky-side replay branch in command_parser stays declared
but unreachable until Phase F sweeps the chumsky path.

Tests:
- 7 new walker-specific tests for replay: bare relative path,
  bare absolute path, quoted with whitespace, quoted with
  escaped quote, case-insensitive keyword, missing-path
  error, empty-quoted-path parses to empty (runtime layer
  rejects).
- Total: 844 passed, 0 failed, 1 ignored (was 838 / 1).
- cargo clippy --all-targets -- -D warnings clean.
This commit is contained in:
claude@clouddev1
2026-05-15 07:23:51 +00:00
parent c2accc2385
commit dca472f8a5
4 changed files with 115 additions and 51 deletions
+5 -45
View File
@@ -120,9 +120,6 @@ pub fn parse_tokens(tokens: &[Token], source: &str) -> Result<Command, ParseErro
if let Some(result) = try_walker_route(source) {
return result;
}
if let Some(result) = try_parse_replay_with_bare_path(tokens, source) {
return result;
}
match command_parser().parse(tokens).into_result() {
Ok(cmd) => Ok(cmd),
Err(errs) => Err(into_parse_error(&errs, tokens, source)),
@@ -272,48 +269,11 @@ fn oxford_join(items: &[String]) -> String {
}
}
/// `replay` source-slice special case (ADR-0020 §6).
///
/// `replay <bare-path>` lets the user write paths containing
/// `/`, `.`, `~`, etc. — characters that the lexer would either
/// classify as `Punct` or as `Error(UnknownChar)`. To keep the
/// existing UX working, we detect `replay` followed by anything
/// other than a `StringLiteral` and source-slice the rest of
/// the input as the path. The quoted form (`replay '<path>'`)
/// goes through the regular chumsky path.
fn try_parse_replay_with_bare_path(
tokens: &[Token],
source: &str,
) -> Option<Result<Command, ParseError>> {
let first = tokens.first()?;
if !matches!(first.kind, TokenKind::Keyword(Keyword::Replay)) {
return None;
}
if matches!(
tokens.get(1).map(|t| &t.kind),
Some(TokenKind::StringLiteral(_))
) {
// Quoted form — chumsky handles it (and rejects any
// trailing garbage).
return None;
}
let after_replay = first.span.1;
let rest = source[after_replay..].trim();
if rest.is_empty() {
// `replay` with nothing after — produce the same shape
// of error chumsky would (positioned where the path
// should have started).
return Some(Err(ParseError::Invalid {
message: crate::t!("parse.custom.replay_path_expected"),
position: after_replay,
at_eof: true,
expected: vec!["path".to_string()],
}));
}
Some(Ok(Command::Replay {
path: rest.to_string(),
}))
}
// ADR-0024 Phase E removed `try_parse_replay_with_bare_path`:
// the walker now owns `replay` end-to-end via
// `Choice(StringLit, BarePath)`. The chumsky-side replay
// branch in `command_parser` is unreachable until Phase F
// sweeps the chumsky path.
// ADR-0024 Phase A removed `try_parse_app_path_command`: the
// walker (`crate::dsl::walker`) now owns export / import end-to-