fix(rust)!: make lookahead accessors fallible and iteration fused

`current_symbol` and `current_symbol_name` return an `Option` (`None` when the
iterator is not positioned on a symbol). Change `iter_names`'s item from
`&'static str` to `&str`.
This commit is contained in:
Will Lillis 2026-08-10 17:01:29 -05:00
parent 7bf4a10995
commit 081929092f
3 changed files with 91 additions and 19 deletions

View file

@ -31,11 +31,12 @@ fn test_lookahead_iterator() {
let mut lookahead = language.lookahead_iterator(next_state).unwrap();
assert_eq!(*lookahead.language(), language);
assert!(lookahead.iter_names().eq(expected_symbols));
assert_eq!(lookahead.iter_names().count(), 0);
lookahead.reset_state(next_state);
assert!(lookahead.reset_state(next_state));
assert!(lookahead.iter_names().eq(expected_symbols));
lookahead.reset(&language, next_state);
assert!(lookahead.reset(&language, next_state));
assert!(
lookahead
.map(|s| language.node_kind_for_id(s).unwrap())
@ -66,6 +67,33 @@ fn test_lookahead_iterator_modifiable_only_by_mut() {
let _ = names.next();
}
#[test]
fn test_lookahead_iterator_exhaustion() {
let language = get_language("json");
for state in 0..language.parse_state_count() {
let state = u16::try_from(state).unwrap();
let mut lookahead = language.lookahead_iterator(state).unwrap();
// A fresh iterator is not positioned on a symbol.
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.current_symbol_name(), None);
let count = lookahead.by_ref().count();
// An exhausted iterator is not positioned on a symbol, and stays exhausted.
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.current_symbol_name(), None);
assert_eq!(lookahead.by_ref().count(), 0);
assert_eq!(lookahead.iter_names().count(), 0);
// Resetting restores it exactly.
assert!(lookahead.reset_state(state));
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.by_ref().count(), count);
}
}
#[test]
fn test_symbol_metadata_checks() {
let language = get_language("rust");

View file

@ -387,3 +387,18 @@ fn test_wasm_oom() {
);
});
}
#[test]
fn test_lookahead_iterator_outlives_wasm_language() {
allocations::record(|| {
let mut store = WasmStore::new(&ENGINE).unwrap();
let wasm = fs::read(WASM_DIR.join("tree-sitter-ruby.wasm")).unwrap();
let language = store.load_language("ruby", &wasm).unwrap();
let mut lookahead = language.lookahead_iterator(0).unwrap();
drop(language);
// The iterator retains the language, so the names are still live.
assert!(lookahead.iter_names().count() > 0);
});
}

View file

@ -670,8 +670,12 @@ impl Language {
/// This returns `None` if state is invalid for this language.
///
/// Iterating [`LookaheadIterator`] will yield valid symbols in the given
/// parse state. Newly created lookahead iterators will return the `ERROR`
/// symbol from [`LookaheadIterator::current_symbol`].
/// parse state. A newly created iterator is not positioned on a symbol, so
/// [`LookaheadIterator::current_symbol`] returns `None` until the first
/// [`Iterator::next`] call.
///
/// The iterator retains the language, so the language may be dropped while
/// the iterator is still in use.
///
/// Lookahead iterators can be useful to generate suggestions and improve
/// syntax error diagnostics. To get symbols valid in an `ERROR` node, use the
@ -2337,22 +2341,34 @@ impl LookaheadIterator {
}
/// Get the current symbol of the lookahead iterator.
///
/// Returns `None` if the iterator is not positioned on a symbol:
///
/// - Before the first [`Iterator::next`] call
/// - After the iterator is exhausted
/// - After a [`Self::reset`] or [`Self::reset_state`] call
#[doc(alias = "ts_lookahead_iterator_current_symbol")]
#[must_use]
pub fn current_symbol(&self) -> u16 {
unsafe { ffi::ts_lookahead_iterator_current_symbol(self.0.as_ptr()) }
pub fn current_symbol(&self) -> Option<u16> {
// C signals "not positioned" through a null symbol name.
let name = unsafe { ffi::ts_lookahead_iterator_current_symbol_name(self.0.as_ptr()) };
(!name.is_null())
.then(|| unsafe { ffi::ts_lookahead_iterator_current_symbol(self.0.as_ptr()) })
}
/// Get the current symbol name of the lookahead iterator.
///
/// Returns `None` if the iterator is not positioned on a symbol.
#[doc(alias = "ts_lookahead_iterator_current_symbol_name")]
#[must_use]
pub fn current_symbol_name(&self) -> &'static str {
pub fn current_symbol_name(&self) -> Option<&str> {
unsafe {
CStr::from_ptr(ffi::ts_lookahead_iterator_current_symbol_name(
self.0.as_ptr(),
))
.to_str()
.unwrap()
let name = ffi::ts_lookahead_iterator_current_symbol_name(self.0.as_ptr());
if name.is_null() {
None
} else {
Some(CStr::from_ptr(name).to_str().unwrap())
}
}
}
@ -2375,31 +2391,44 @@ impl LookaheadIterator {
}
/// Iterate symbol names.
pub fn iter_names(&mut self) -> impl Iterator<Item = &'static str> + '_ {
pub fn iter_names(&mut self) -> impl iter::FusedIterator<Item = &str> + '_ {
LookaheadNamesIterator(self)
}
}
impl Iterator for LookaheadNamesIterator<'_> {
type Item = &'static str;
impl<'a> Iterator for LookaheadNamesIterator<'a> {
type Item = &'a str;
#[doc(alias = "ts_lookahead_iterator_next")]
fn next(&mut self) -> Option<Self::Item> {
unsafe { ffi::ts_lookahead_iterator_next(self.0.0.as_ptr()) }
.then(|| self.0.current_symbol_name())
let ptr = self.0.0.as_ptr();
// SAFETY: The borrow keeps the iterator (and the language refcount it holds)
// alive for `'a`. The name is non-null because the iterator is positioned
// whenever `next` returns `true`.
unsafe {
ffi::ts_lookahead_iterator_next(ptr).then(|| {
let name = ffi::ts_lookahead_iterator_current_symbol_name(ptr);
debug_assert!(!name.is_null());
CStr::from_ptr(name).to_str().unwrap()
})
}
}
}
impl iter::FusedIterator for LookaheadNamesIterator<'_> {}
impl Iterator for LookaheadIterator {
type Item = u16;
#[doc(alias = "ts_lookahead_iterator_next")]
fn next(&mut self) -> Option<Self::Item> {
// the first symbol is always `0` so we can safely skip it
unsafe { ffi::ts_lookahead_iterator_next(self.0.as_ptr()) }.then(|| self.current_symbol())
unsafe { ffi::ts_lookahead_iterator_next(self.0.as_ptr()) }
.then(|| unsafe { ffi::ts_lookahead_iterator_current_symbol(self.0.as_ptr()) })
}
}
impl iter::FusedIterator for LookaheadIterator {}
impl Drop for LookaheadIterator {
#[doc(alias = "ts_lookahead_iterator_delete")]
fn drop(&mut self) {