1use std::{collections::BTreeMap, sync::LazyLock};
2#[cfg(feature = "load")]
3use std::{
4 env, fs,
5 io::Write,
6 path::{Path, PathBuf},
7 process::{Command, Stdio},
8};
9
10use bitflags::bitflags;
11#[cfg(feature = "load")]
12use log::warn;
13use node_types::VariableInfo;
14use regex::{Regex, RegexBuilder};
15use rules::{Alias, Symbol};
16#[cfg(feature = "load")]
17use semver::Version;
18#[cfg(feature = "load")]
19use serde::Deserialize;
20use serde::Serialize;
21use thiserror::Error;
22
23mod build_tables;
24mod dedup;
25mod grammars;
26mod nfa;
27mod node_types;
28pub mod parse_grammar;
29mod prepare_grammar;
30#[cfg(feature = "qjs-rt")]
31mod quickjs;
32mod render;
33mod rules;
34mod tables;
35
36use build_tables::build_tables;
37pub use build_tables::ParseTableBuilderError;
38use grammars::{InlinedProductionMap, InputGrammar, LexicalGrammar, SyntaxGrammar};
39pub use node_types::{SuperTypeCycleError, VariableInfoError};
40use parse_grammar::parse_grammar;
41pub use parse_grammar::ParseGrammarError;
42use prepare_grammar::prepare_grammar;
43pub use prepare_grammar::PrepareGrammarError;
44use render::render_c_code;
45pub use render::{RenderError, ABI_VERSION_MAX, ABI_VERSION_MIN};
46
47static JSON_COMMENT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
48 RegexBuilder::new("^\\s*//.*")
49 .multi_line(true)
50 .build()
51 .unwrap()
52});
53
54struct JSONOutput {
55 #[cfg(feature = "load")]
56 node_types_json: String,
57 syntax_grammar: SyntaxGrammar,
58 lexical_grammar: LexicalGrammar,
59 inlines: InlinedProductionMap,
60 simple_aliases: BTreeMap<Symbol, Alias>,
61 variable_info: Vec<VariableInfo>,
62}
63
64struct GeneratedParser {
65 c_code: String,
66 #[cfg(feature = "load")]
67 node_types_json: String,
68}
69
70const LANGUAGE_VERSION: usize = 15;
73
74pub const ALLOC_HEADER: &str = include_str!("templates/alloc.h");
75pub const ARRAY_HEADER: &str = include_str!("templates/array.h");
76pub const PARSER_HEADER: &str = include_str!("parser.h.inc");
77
78pub type GenerateResult<T> = Result<T, GenerateError>;
79
80#[derive(Debug, Error, Serialize)]
81pub enum GenerateError {
82 #[error("Error with specified path -- {0}")]
83 GrammarPath(String),
84 #[error(transparent)]
85 IO(IoError),
86 #[cfg(feature = "load")]
87 #[error(transparent)]
88 LoadGrammarFile(#[from] LoadGrammarError),
89 #[error(transparent)]
90 ParseGrammar(#[from] ParseGrammarError),
91 #[error(transparent)]
92 Prepare(#[from] PrepareGrammarError),
93 #[error(transparent)]
94 VariableInfo(#[from] VariableInfoError),
95 #[error(transparent)]
96 BuildTables(#[from] ParseTableBuilderError),
97 #[error(transparent)]
98 Render(#[from] RenderError),
99 #[cfg(feature = "load")]
100 #[error(transparent)]
101 ParseVersion(#[from] ParseVersionError),
102 #[error(transparent)]
103 SuperTypeCycle(#[from] SuperTypeCycleError),
104}
105
106#[derive(Debug, Error, Serialize)]
107pub struct IoError {
108 pub error: String,
109 pub path: Option<String>,
110}
111
112#[cfg(feature = "load")]
113impl IoError {
114 fn new(error: &std::io::Error, path: Option<&Path>) -> Self {
115 Self {
116 error: error.to_string(),
117 path: path.map(|p| p.to_string_lossy().to_string()),
118 }
119 }
120}
121
122impl std::fmt::Display for IoError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 write!(f, "{}", self.error)?;
125 if let Some(ref path) = self.path {
126 write!(f, " ({path})")?;
127 }
128 Ok(())
129 }
130}
131
132#[cfg(feature = "load")]
133pub type LoadGrammarFileResult<T> = Result<T, LoadGrammarError>;
134
135#[cfg(feature = "load")]
136#[derive(Debug, Error, Serialize)]
137pub enum LoadGrammarError {
138 #[error("Path to a grammar file with `.js` or `.json` extension is required")]
139 InvalidPath,
140 #[error("Failed to load grammar.js -- {0}")]
141 LoadJSGrammarFile(#[from] JSError),
142 #[error("Failed to load grammar.json -- {0}")]
143 IO(IoError),
144 #[error("Unknown grammar file extension: {0:?}")]
145 FileExtension(PathBuf),
146}
147
148#[cfg(feature = "load")]
149#[derive(Debug, Error, Serialize)]
150pub enum ParseVersionError {
151 #[error("{0}")]
152 Version(String),
153 #[error("{0}")]
154 JSON(String),
155 #[error(transparent)]
156 IO(IoError),
157}
158
159#[cfg(feature = "load")]
160pub type JSResult<T> = Result<T, JSError>;
161
162#[cfg(feature = "load")]
163#[derive(Debug, Error, Serialize)]
164pub enum JSError {
165 #[error("Failed to run `{runtime}` -- {error}")]
166 JSRuntimeSpawn { runtime: String, error: String },
167 #[error("Got invalid UTF8 from `{runtime}` -- {error}")]
168 JSRuntimeUtf8 { runtime: String, error: String },
169 #[error("`{runtime}` process exited with status {code}")]
170 JSRuntimeExit { runtime: String, code: i32 },
171 #[error("Failed to open stdin for `{runtime}`")]
172 JSRuntimeStdin { runtime: String },
173 #[error("Failed to write {item} to `{runtime}`'s stdin -- {error}")]
174 JSRuntimeWrite {
175 runtime: String,
176 item: String,
177 error: String,
178 },
179 #[error("Failed to read output from `{runtime}` -- {error}")]
180 JSRuntimeRead { runtime: String, error: String },
181 #[error(transparent)]
182 IO(IoError),
183 #[cfg(feature = "qjs-rt")]
184 #[error("Failed to get relative path")]
185 RelativePath,
186 #[error("Could not parse this package's version as semver -- {0}")]
187 Semver(String),
188 #[error("Failed to serialze grammar JSON -- {0}")]
189 Serialzation(String),
190 #[cfg(feature = "qjs-rt")]
191 #[error("QuickJS error: {0}")]
192 QuickJS(String),
193}
194
195#[cfg(feature = "load")]
196impl From<serde_json::Error> for JSError {
197 fn from(value: serde_json::Error) -> Self {
198 Self::Serialzation(value.to_string())
199 }
200}
201
202#[cfg(feature = "load")]
203impl From<semver::Error> for JSError {
204 fn from(value: semver::Error) -> Self {
205 Self::Semver(value.to_string())
206 }
207}
208
209#[cfg(feature = "qjs-rt")]
210impl From<rquickjs::Error> for JSError {
211 fn from(value: rquickjs::Error) -> Self {
212 Self::QuickJS(value.to_string())
213 }
214}
215
216bitflags! {
217 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
218 pub struct OptLevel: u32 {
219 const MergeStates = 1 << 0;
220 }
221}
222
223impl Default for OptLevel {
224 fn default() -> Self {
225 Self::MergeStates
226 }
227}
228
229#[cfg(feature = "load")]
230#[allow(clippy::too_many_arguments)]
231pub fn generate_parser_in_directory<T, U, V>(
232 repo_path: T,
233 out_path: Option<U>,
234 grammar_path: Option<V>,
235 mut abi_version: usize,
236 report_symbol_name: Option<&str>,
237 js_runtime: Option<&str>,
238 generate_parser: bool,
239 optimizations: OptLevel,
240) -> GenerateResult<()>
241where
242 T: Into<PathBuf>,
243 U: Into<PathBuf>,
244 V: Into<PathBuf>,
245{
246 let mut repo_path: PathBuf = repo_path.into();
247
248 let grammar_path = if let Some(path) = grammar_path {
250 let path_buf: PathBuf = path.into();
251 if !path_buf
252 .try_exists()
253 .map_err(|e| GenerateError::GrammarPath(e.to_string()))?
254 {
255 fs::create_dir_all(&path_buf)
256 .map_err(|e| GenerateError::IO(IoError::new(&e, Some(path_buf.as_path()))))?;
257 repo_path = path_buf;
258 repo_path.join("grammar.js")
259 } else {
260 path_buf
261 }
262 } else {
263 repo_path.join("grammar.js")
264 };
265
266 let grammar_json = load_grammar_file(&grammar_path, js_runtime)?;
268
269 let src_path = out_path.map_or_else(|| repo_path.join("src"), |p| p.into());
270 let header_path = src_path.join("tree_sitter");
271
272 fs::create_dir_all(&src_path)
274 .map_err(|e| GenerateError::IO(IoError::new(&e, Some(src_path.as_path()))))?;
275
276 if grammar_path.file_name().unwrap() != "grammar.json" {
277 fs::write(src_path.join("grammar.json"), &grammar_json)
278 .map_err(|e| GenerateError::IO(IoError::new(&e, Some(src_path.as_path()))))?;
279 }
280
281 let input_grammar = parse_grammar(&grammar_json)?;
283
284 if !generate_parser {
285 let node_types_json = generate_node_types_from_grammar(&input_grammar)?.node_types_json;
286 write_file(&src_path.join("node-types.json"), node_types_json)?;
287 return Ok(());
288 }
289
290 let semantic_version = read_grammar_version(&repo_path)?;
291
292 if semantic_version.is_none() && abi_version > ABI_VERSION_MIN {
293 warn!(
294 concat!(
295 "No `tree-sitter.json` file found in your grammar, ",
296 "this file is required to generate with ABI {}. ",
297 "Using ABI version {} instead.\n",
298 "This file can be set up with `tree-sitter init`. ",
299 "For more information, see https://tree-sitter.github.io/tree-sitter/cli/init."
300 ),
301 abi_version, ABI_VERSION_MIN
302 );
303 abi_version = ABI_VERSION_MIN;
304 }
305
306 let GeneratedParser {
308 c_code,
309 node_types_json,
310 } = generate_parser_for_grammar_with_opts(
311 &input_grammar,
312 abi_version,
313 semantic_version.map(|v| (v.major as u8, v.minor as u8, v.patch as u8)),
314 report_symbol_name,
315 optimizations,
316 )?;
317
318 write_file(&src_path.join("parser.c"), c_code)?;
319 write_file(&src_path.join("node-types.json"), node_types_json)?;
320 fs::create_dir_all(&header_path)
321 .map_err(|e| GenerateError::IO(IoError::new(&e, Some(header_path.as_path()))))?;
322 write_file(&header_path.join("alloc.h"), ALLOC_HEADER)?;
323 write_file(&header_path.join("array.h"), ARRAY_HEADER)?;
324 write_file(&header_path.join("parser.h"), PARSER_HEADER)?;
325
326 Ok(())
327}
328
329pub fn generate_parser_for_grammar(
330 grammar_json: &str,
331 semantic_version: Option<(u8, u8, u8)>,
332) -> GenerateResult<(String, String)> {
333 let grammar_json = JSON_COMMENT_REGEX.replace_all(grammar_json, "\n");
334 let input_grammar = parse_grammar(&grammar_json)?;
335 let parser = generate_parser_for_grammar_with_opts(
336 &input_grammar,
337 LANGUAGE_VERSION,
338 semantic_version,
339 None,
340 OptLevel::default(),
341 )?;
342 Ok((input_grammar.name, parser.c_code))
343}
344
345fn generate_node_types_from_grammar(input_grammar: &InputGrammar) -> GenerateResult<JSONOutput> {
346 let (syntax_grammar, lexical_grammar, inlines, simple_aliases) =
347 prepare_grammar(input_grammar)?;
348 let variable_info =
349 node_types::get_variable_info(&syntax_grammar, &lexical_grammar, &simple_aliases)?;
350
351 #[cfg(feature = "load")]
352 let node_types_json = node_types::generate_node_types_json(
353 &syntax_grammar,
354 &lexical_grammar,
355 &simple_aliases,
356 &variable_info,
357 )?;
358 Ok(JSONOutput {
359 #[cfg(feature = "load")]
360 node_types_json: serde_json::to_string_pretty(&node_types_json).unwrap(),
361 syntax_grammar,
362 lexical_grammar,
363 inlines,
364 simple_aliases,
365 variable_info,
366 })
367}
368
369fn generate_parser_for_grammar_with_opts(
370 input_grammar: &InputGrammar,
371 abi_version: usize,
372 semantic_version: Option<(u8, u8, u8)>,
373 report_symbol_name: Option<&str>,
374 optimizations: OptLevel,
375) -> GenerateResult<GeneratedParser> {
376 let JSONOutput {
377 syntax_grammar,
378 lexical_grammar,
379 inlines,
380 simple_aliases,
381 variable_info,
382 #[cfg(feature = "load")]
383 node_types_json,
384 } = generate_node_types_from_grammar(input_grammar)?;
385 let supertype_symbol_map =
386 node_types::get_supertype_symbol_map(&syntax_grammar, &simple_aliases, &variable_info);
387 let tables = build_tables(
388 &syntax_grammar,
389 &lexical_grammar,
390 &simple_aliases,
391 &variable_info,
392 &inlines,
393 report_symbol_name,
394 optimizations,
395 )?;
396 let c_code = render_c_code(
397 &input_grammar.name,
398 tables,
399 syntax_grammar,
400 lexical_grammar,
401 simple_aliases,
402 abi_version,
403 semantic_version,
404 supertype_symbol_map,
405 )?;
406 Ok(GeneratedParser {
407 c_code,
408 #[cfg(feature = "load")]
409 node_types_json,
410 })
411}
412
413#[cfg(feature = "load")]
419fn read_grammar_version(repo_path: &Path) -> Result<Option<Version>, ParseVersionError> {
420 #[derive(Deserialize)]
421 struct TreeSitterJson {
422 metadata: Metadata,
423 }
424
425 #[derive(Deserialize)]
426 struct Metadata {
427 version: String,
428 }
429
430 let filename = "tree-sitter.json";
431 let mut path = repo_path.join(filename);
432
433 loop {
434 let json = path
435 .exists()
436 .then(|| {
437 let contents = fs::read_to_string(path.as_path())
438 .map_err(|e| ParseVersionError::IO(IoError::new(&e, Some(path.as_path()))))?;
439 serde_json::from_str::<TreeSitterJson>(&contents).map_err(|e| {
440 ParseVersionError::JSON(format!("Failed to parse `{}` -- {e}", path.display()))
441 })
442 })
443 .transpose()?;
444 if let Some(json) = json {
445 return Version::parse(&json.metadata.version)
446 .map_err(|e| {
447 ParseVersionError::Version(format!(
448 "Failed to parse `{}` version as semver -- {e}",
449 path.display()
450 ))
451 })
452 .map(Some);
453 }
454 path.pop(); if !path.pop() {
456 return Ok(None);
457 }
458 path.push(filename);
459 }
460}
461
462#[cfg(feature = "load")]
463pub fn load_grammar_file(
464 grammar_path: &Path,
465 js_runtime: Option<&str>,
466) -> LoadGrammarFileResult<String> {
467 if grammar_path.is_dir() {
468 Err(LoadGrammarError::InvalidPath)?;
469 }
470 match grammar_path.extension().and_then(|e| e.to_str()) {
471 Some("js") => Ok(load_js_grammar_file(grammar_path, js_runtime)?),
472 Some("json") => Ok(fs::read_to_string(grammar_path)
473 .map_err(|e| LoadGrammarError::IO(IoError::new(&e, Some(grammar_path))))?),
474 _ => Err(LoadGrammarError::FileExtension(grammar_path.to_owned()))?,
475 }
476}
477
478#[cfg(feature = "load")]
479fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResult<String> {
480 let grammar_path = dunce::canonicalize(grammar_path)
481 .map_err(|e| JSError::IO(IoError::new(&e, Some(grammar_path))))?;
482
483 #[cfg(feature = "qjs-rt")]
484 if js_runtime == Some("native") {
485 return quickjs::execute_native_runtime(&grammar_path);
486 }
487
488 #[cfg(windows)]
491 let grammar_path = PathBuf::from(format!("file:///{}", grammar_path.display()));
492
493 let js_runtime = js_runtime.unwrap_or("node");
494
495 let mut js_command = Command::new(js_runtime);
496 match js_runtime {
497 "node" => {
498 js_command.args(["--input-type=module", "-"]);
499 }
500 "bun" => {
501 js_command.arg("-");
502 }
503 "deno" => {
504 js_command.args(["run", "--allow-all", "-"]);
505 }
506 _ => {}
507 }
508
509 let mut js_process = js_command
510 .env("TREE_SITTER_GRAMMAR_PATH", grammar_path)
511 .stdin(Stdio::piped())
512 .stdout(Stdio::piped())
513 .spawn()
514 .map_err(|e| JSError::JSRuntimeSpawn {
515 runtime: js_runtime.to_string(),
516 error: e.to_string(),
517 })?;
518
519 let mut js_stdin = js_process
520 .stdin
521 .take()
522 .ok_or_else(|| JSError::JSRuntimeStdin {
523 runtime: js_runtime.to_string(),
524 })?;
525
526 let cli_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
527 write!(
528 js_stdin,
529 "globalThis.TREE_SITTER_CLI_VERSION_MAJOR = {};
530 globalThis.TREE_SITTER_CLI_VERSION_MINOR = {};
531 globalThis.TREE_SITTER_CLI_VERSION_PATCH = {};",
532 cli_version.major, cli_version.minor, cli_version.patch,
533 )
534 .map_err(|e| JSError::JSRuntimeWrite {
535 runtime: js_runtime.to_string(),
536 item: "tree-sitter version".to_string(),
537 error: e.to_string(),
538 })?;
539 js_stdin
540 .write(include_bytes!("./dsl.js"))
541 .map_err(|e| JSError::JSRuntimeWrite {
542 runtime: js_runtime.to_string(),
543 item: "grammar dsl".to_string(),
544 error: e.to_string(),
545 })?;
546 drop(js_stdin);
547
548 let output = js_process
549 .wait_with_output()
550 .map_err(|e| JSError::JSRuntimeRead {
551 runtime: js_runtime.to_string(),
552 error: e.to_string(),
553 })?;
554 match output.status.code() {
555 Some(0) => {
556 let stdout = String::from_utf8(output.stdout).map_err(|e| JSError::JSRuntimeUtf8 {
557 runtime: js_runtime.to_string(),
558 error: e.to_string(),
559 })?;
560
561 let mut grammar_json = &stdout[..];
562
563 if let Some(pos) = stdout.rfind('\n') {
564 let node_output = &stdout[..pos];
566 grammar_json = &stdout[pos + 1..];
567
568 let mut stdout = std::io::stdout().lock();
569 stdout
570 .write_all(node_output.as_bytes())
571 .map_err(|e| JSError::IO(IoError::new(&e, None)))?;
572 stdout
573 .write_all(b"\n")
574 .map_err(|e| JSError::IO(IoError::new(&e, None)))?;
575 stdout
576 .flush()
577 .map_err(|e| JSError::IO(IoError::new(&e, None)))?;
578 }
579
580 Ok(serde_json::to_string_pretty(&serde_json::from_str::<
581 serde_json::Value,
582 >(grammar_json)?)?)
583 }
584 Some(code) => Err(JSError::JSRuntimeExit {
585 runtime: js_runtime.to_string(),
586 code,
587 }),
588 None => Err(JSError::JSRuntimeExit {
589 runtime: js_runtime.to_string(),
590 code: -1,
591 }),
592 }
593}
594
595#[cfg(feature = "load")]
596pub fn write_file(path: &Path, body: impl AsRef<[u8]>) -> GenerateResult<()> {
597 fs::write(path, body).map_err(|e| GenerateError::IO(IoError::new(&e, Some(path))))
598}
599
600#[cfg(test)]
601mod tests {
602 use super::{LANGUAGE_VERSION, PARSER_HEADER};
603 #[test]
604 fn test_language_versions_are_in_sync() {
605 let api_h = include_str!("../../../lib/include/tree_sitter/api.h");
606 let api_language_version = api_h
607 .lines()
608 .find_map(|line| {
609 line.trim()
610 .strip_prefix("#define TREE_SITTER_LANGUAGE_VERSION ")
611 .and_then(|v| v.parse::<usize>().ok())
612 })
613 .expect("Failed to find TREE_SITTER_LANGUAGE_VERSION definition in api.h");
614 assert_eq!(LANGUAGE_VERSION, api_language_version);
615 }
616
617 #[test]
618 fn test_parser_header_in_sync() {
619 let parser_h = include_str!("../../../lib/src/parser.h");
620 assert!(
621 parser_h == PARSER_HEADER,
622 "parser.h.inc is out of sync with lib/src/parser.h. Run: cp lib/src/parser.h crates/generate/src/parser.h.inc"
623 );
624 }
625}