Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/glr/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ mod issue_89_optional_empty_branch {

#[test]
fn refself_no_panic() {
// `amp self_kw` — previously caused a panic inside feed_location_impl
// `amp self_kw` — previously caused a panic inside GLR feed planning
// due to a non-leaf node being pushed to next_nodes after an empty
// reduce produced a child of the original node.
let mut ctx = ParamContext::with_default_userdata();
Expand Down
353 changes: 353 additions & 0 deletions rusty_lr/tests/lr_glr_operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,69 @@ mod token_userdata_and_recovery {
}
}

mod recovery_reduce_before_sync_token {
mod deterministic {
use rusty_lr::lr1;

lr1! {
%nooptim;
%tokentype char;
%location std::ops::Range<usize>;
%userdata Vec<std::ops::Range<usize>>;
%start S;

S(&'static str): error Tail {
data.push(@error.clone());
"recovered"
};

Tail(()): Empty 's' { () };
Empty(()): "" { () };
}

#[test]
fn deterministic_recovery_feeds_sync_token_through_reduce_chain() {
let mut ctx = SContext::new(Vec::new());
ctx.feed_location('s', 0..1).unwrap();

let (value, recovered_spans) = ctx.accept().unwrap();
assert_eq!(value, "recovered");
assert_eq!(recovered_spans, [0..0]);
}
}

mod glr {
use rusty_lr::lr1;

lr1! {
%glr;
%nooptim;
%tokentype char;
%location std::ops::Range<usize>;
%userdata Vec<std::ops::Range<usize>>;
%start S;

S(&'static str): error Tail {
data.push(@error.clone());
"recovered"
};

Tail(()): Empty 's' { () };
Empty(()): "" { () };
}

#[test]
fn glr_recovery_feeds_sync_token_through_reduce_chain() {
let mut ctx = SContext::new(Vec::new());
ctx.feed_location('s', 0..1).unwrap();

let (value, recovered_spans) = ctx.accept().unwrap();
assert_eq!(value, "recovered");
assert_eq!(recovered_spans, [0..0]);
}
}
}

mod left_recursive_nullable_list {
use rusty_lr::lr1;

Expand Down Expand Up @@ -320,6 +383,43 @@ mod glr_ambiguous_concatenation {
}
}

mod glr_can_feed_planning {
use rusty_lr::lr1;

lr1! {
%glr;
%nooptim;
%tokentype char;
%start S;

S(&'static str)
: A 'x' {
"x"
}
| B 'y' {
"y"
}
| Empty {
"empty"
}
;

A(()): Empty { () };
B(()): Empty { () };
Empty(()): "" { () };
}

#[test]
fn glr_can_feed_and_can_accept_reuse_feed_planning_through_reduce_chains() {
let ctx = SContext::with_default_userdata();

assert!(ctx.can_feed(&'x'));
assert!(ctx.can_feed(&'y'));
assert!(!ctx.can_feed(&'z'));
assert!(ctx.can_accept());
}
}

mod glr_dynamic_precedence {
use rusty_lr::lr1;

Expand Down Expand Up @@ -465,4 +565,257 @@ mod reduce_action_errors {
assert_eq!(ctx.accept().unwrap(), ("shift path", ()));
}
}

mod glr_recovery_partial_errors {
use rusty_lr::lr1;

// Recovery can have GLR alternatives too. A semantic failure in one recovery branch should
// be reported without killing sibling recovery branches that successfully shifted `error`.
lr1! {
%glr;
%nooptim;
%tokentype char;
%error &'static str;
%start S;

S(&'static str)
: Good error 's' {
"recovered"
}
| Bad error 's' {
"bad"
}
;

Good(()): "" { () };

Bad(()): "" {
if true {
return Err("bad recovery branch");
}
()
};
}

#[test]
fn glr_recovery_success_reports_pruned_recovery_branch_errors() {
let mut ctx = SContext::with_default_userdata();

let success = ctx.feed('x').unwrap();
let errors = success
.errors
.expect("failed recovery branch should be reported");
assert_eq!(errors.reduce_action_errors, ["bad recovery branch"]);
ctx.debug_check();

ctx.feed('s').unwrap();
ctx.debug_check();
assert_eq!(ctx.accept().unwrap(), ("recovered", ()));
}
}

mod glr_failed_sibling_cleanup {
use rusty_lr::lr1;

// One nullable sibling succeeds, while another reaches a deeper reduce-action failure.
// The failed branch must be detached from the GSS when the successful sibling survives.
lr1! {
%glr;
%nooptim;
%tokentype char;
%error &'static str;
%start S;

S(&'static str)
: Good GoodTail {
"good"
}
| Bad BadTail {
"bad"
}
;

Good(()): "" { () };
GoodTail(()): 'x' { () };

Bad(()): "" { () };
BadTail(()): BadReduce 'x' { () };
BadReduce(()): "" {
if true {
return Err("bad nested branch");
}
()
};
}

#[test]
fn glr_feed_cleans_failed_sibling_nodes_when_another_sibling_survives() {
let mut ctx = SContext::with_default_userdata();
let success = ctx.feed('x').unwrap();

let errors = success
.errors
.expect("failed sibling branch should be reported");
assert_eq!(errors.reduce_action_errors, ["bad nested branch"]);

ctx.debug_check();
assert_eq!(ctx.accept().unwrap(), ("good", ()));
}
}

mod glr_no_action_sibling_cleanup {
use rusty_lr::lr1;

// Feeding `p` creates two live GLR branches. The following `q` keeps only the `A`
// branch, so the `B` branch is a grammatical NoAction sibling and must be removed.
lr1! {
%glr;
%nooptim;
%tokentype char;
%start S;

S(&'static str)
: A 'q' {
"a"
}
| B 'r' {
"b"
}
;

A(()): X 'p' { () };
B(()): Y 'p' { () };
X(()): 'x' { () };
Y(()): 'x' { () };
}

#[test]
fn glr_feed_cleans_no_action_sibling_nodes_when_another_branch_survives() {
let mut ctx = SContext::with_default_userdata();

ctx.feed('x').unwrap();
ctx.feed('p').unwrap();
ctx.debug_check();

let success = ctx.feed('q').unwrap();
assert!(success.errors.is_some());

ctx.debug_check();
assert_eq!(ctx.accept().unwrap(), ("a", ()));
}
}

mod glr_panic_mode_node_cleanup {
use rusty_lr::lr1;

// The `abc` branch lets `b` be shifted normally. When the next token is invalid,
// recovery must pop that shifted `b` node back to the state after `a`, where
// `error 's'` is available. The popped node must be removed from the GSS.
lr1! {
%glr;
%nooptim;
%tokentype char;
%start S;

S(&'static str)
: 'a' error 's' {
"recovered"
}
| 'a' 'b' 'c' {
"abc"
}
;
}

#[test]
fn glr_panic_mode_cleans_nodes_popped_during_recovery() {
let mut ctx = SContext::with_default_userdata();

ctx.feed('a').unwrap();
ctx.feed('b').unwrap();
ctx.debug_check();
assert!(ctx.can_panic());

ctx.feed('x').unwrap();
ctx.debug_check();

ctx.feed('s').unwrap();
ctx.debug_check();
assert_eq!(ctx.accept().unwrap(), ("recovered", ()));
}
}

mod glr_conflict_free_reduce_reuse {
use rusty_lr::{Location, lr1};
use std::sync::atomic::{AtomicUsize, Ordering};

static DATA_CLONES: AtomicUsize = AtomicUsize::new(0);
static WATCHED_LOCATION_CLONES: AtomicUsize = AtomicUsize::new(0);

#[derive(Debug, PartialEq, Eq)]
struct CloneWatchedValue(&'static str);

impl Clone for CloneWatchedValue {
fn clone(&self) -> Self {
DATA_CLONES.fetch_add(1, Ordering::SeqCst);
Self(self.0)
}
}

#[derive(Debug, PartialEq, Eq)]
struct CloneWatchedLocation {
reduced: bool,
}

impl Clone for CloneWatchedLocation {
fn clone(&self) -> Self {
if self.reduced {
WATCHED_LOCATION_CLONES.fetch_add(1, Ordering::SeqCst);
}
Self {
reduced: self.reduced,
}
}
}

impl Location for CloneWatchedLocation {
fn new<'a>(_stack: impl Iterator<Item = &'a Self> + Clone, len: usize) -> Self
where
Self: 'a,
{
Self { reduced: len > 0 }
}
}

lr1! {
%glr;
%nooptim;
%tokentype char;
%location CloneWatchedLocation;
%start S;

S(CloneWatchedValue): Pair 'c' { Pair };
Pair(CloneWatchedValue): Item 'b' { Item };
Item(CloneWatchedValue): 'a' { CloneWatchedValue("item") };
}

#[test]
fn glr_conflict_free_reduce_does_not_clone_semantic_or_location_stacks() {
DATA_CLONES.store(0, Ordering::SeqCst);
WATCHED_LOCATION_CLONES.store(0, Ordering::SeqCst);

let mut ctx = SContext::with_default_userdata();
ctx.feed_location('a', CloneWatchedLocation { reduced: false })
.unwrap();
ctx.feed_location('b', CloneWatchedLocation { reduced: false })
.unwrap();
ctx.feed_location('c', CloneWatchedLocation { reduced: false })
.unwrap();

let (value, _) = ctx.accept().unwrap();
assert_eq!(value, CloneWatchedValue("item"));
assert_eq!(DATA_CLONES.load(Ordering::SeqCst), 0);
assert_eq!(WATCHED_LOCATION_CLONES.load(Ordering::SeqCst), 0);
}
}
}
Loading
Loading