Skip to main content

tree_sitter_generate/
generate.rs

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
70// NOTE: This constant must be kept in sync with the definition of
71// `TREE_SITTER_LANGUAGE_VERSION` in `lib/include/tree_sitter/api.h`.
72const 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    // Populate a new empty grammar directory.
249    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    // Read the grammar file.
267    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    // Ensure that the output directory exists
273    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    // If our job is only to generate `grammar.json` and not `parser.c`, stop here.
282    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    // Generate the parser and related files.
307    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/// This will read the `tree-sitter.json` config file and attempt to extract the version.
414///
415/// If the file is not found in the current directory or any of its parent directories, this will
416/// return `None` to maintain backwards compatibility. If the file is found but the version cannot
417/// be parsed as semver, this will return an error.
418#[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(); // filename
455        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    // The "file:///" prefix is incompatible with the quickjs runtime, but is required
489    // for node and bun
490    #[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                // If there's a newline, split the last line from the rest of the output
565                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}