use std::cell::Cell;
use tree_sitter::{Language, Parser, Tree};
thread_local! {
static SCRATCH_PARSER: Cell<Option<Parser>> = const { Cell::new(None) };
}
crate::observation::counter!(parsers_built);
pub(crate) fn parse_on_scratch_parser(language: &Language, code: &[u8]) -> Tree {
let mut parser = SCRATCH_PARSER
.try_with(Cell::take)
.ok()
.flatten()
.unwrap_or_else(build_parser);
parser
.set_language(language)
.expect("invariant: grammar version is pinned and compatible with bundled tree-sitter");
let tree = parser
.parse(code, None)
.expect("invariant: parser has a language set and no cancellation flag");
let _ = SCRATCH_PARSER.try_with(|slot| slot.set(Some(parser)));
tree
}
fn build_parser() -> Parser {
parsers_built::record();
Parser::new()
}
#[cfg(all(test, feature = "rust"))]
mod tests {
use super::*;
use crate::langs::RustCode;
use crate::traits::LanguageInfo;
use crate::{Ast, LANG, Source};
fn rust_language() -> Language {
RustCode::lang()
.get_ts_language()
.expect("rust is a default feature")
}
#[test]
fn many_parses_on_one_thread_build_one_parser() {
const PARSES: usize = 8;
std::thread::spawn(|| {
assert_eq!(
parsers_built::observed(),
0,
"a fresh thread must start with an empty slot"
);
let language = rust_language();
for i in 0..PARSES {
let tree = parse_on_scratch_parser(&language, b"fn f() { let x = 1; }");
assert!(
!tree.root_node().to_sexp().contains("ERROR"),
"parse {i} must succeed"
);
}
assert_eq!(
parsers_built::observed(),
1,
"{PARSES} parses must share one parser"
);
})
.join()
.expect("parsing thread must not panic");
}
#[test]
fn each_thread_builds_its_own_parser() {
let mut counts = Vec::new();
for _ in 0..3 {
let handle = std::thread::spawn(|| {
let language = rust_language();
parse_on_scratch_parser(&language, b"fn f() {}");
parse_on_scratch_parser(&language, b"fn g() {}");
parsers_built::observed()
});
counts.push(handle.join().expect("parsing thread must not panic"));
}
assert_eq!(
counts,
vec![1, 1, 1],
"each thread builds exactly one parser for its own files"
);
}
#[test]
fn repeated_parses_through_the_public_seam_share_one_parser() {
const FILES: usize = 6;
std::thread::spawn(|| {
for i in 0..FILES {
let code = format!("fn f{i}() {{ let x = {i}; }}");
let ast = Ast::parse(Source::new(LANG::Rust, code.as_bytes()))
.expect("rust is enabled for this module");
let sexp = ast.as_tree_sitter().root_node().to_sexp();
assert!(
sexp.contains("function_item") && !sexp.contains("ERROR"),
"file {i} must parse to a real tree, got {sexp}"
);
}
assert_eq!(
parsers_built::observed(),
1,
"{FILES} files parsed through `Ast::parse` must share one parser"
);
})
.join()
.expect("parsing thread must not panic");
}
}