Skip to main content

brink_compiler/
lib.rs

1//! Compiler for inkle's ink narrative scripting language.
2//!
3//! Orchestrates the full compilation pipeline: file discovery, parsing
4//! (`brink-syntax`), HIR lowering (`brink-ir`), semantic analysis
5//! (`brink-analyzer`), and codegen into the `brink-format` binary
6//! representation consumed by `brink-runtime`.
7
8mod driver;
9
10pub use brink_driver::AnalysisOptions;
11pub use brink_ir::{DiagnosticCode, FileId};
12
13use brink_format::StoryData;
14use std::io;
15use std::path::Path;
16
17/// A diagnostic resolved for consumption outside the compiler.
18///
19/// The internal [`Diagnostic`] keys a file by [`FileId`] — an interning index
20/// that is only meaningful inside the compiler instance that produced it and
21/// is not stable across recompiles. A consumer (an editor, a host integration,
22/// an LSP) cannot map that id back to a file on its own. `ResolvedDiagnostic`
23/// carries the file's `path` — byte-identical to the string the host used as
24/// the entry point / answered the `read_file` callback with — so a diagnostic
25/// can always be located. `file` is retained for in-result correlation only.
26///
27/// `range` is left as byte offsets into the file's source. Line/column
28/// resolution is deliberately not baked in: column units are consumer-specific
29/// (LSP uses UTF-16 code units, a terminal uses bytes or chars), and the
30/// consumer already holds the source text to resolve them in the unit it needs.
31#[derive(Debug, Clone)]
32pub struct ResolvedDiagnostic {
33    /// The file this diagnostic belongs to, keyed by its source path.
34    pub path: String,
35    /// The originating file's interning id — for in-result correlation only.
36    pub file: FileId,
37    /// The source span this diagnostic points at, as byte offsets.
38    pub range: rowan::TextRange,
39    /// Human-readable message describing the problem.
40    pub message: String,
41    /// Structured error code for documentation and tooling.
42    pub code: DiagnosticCode,
43}
44
45/// Successful compilation output, including any non-fatal warnings.
46#[derive(Debug)]
47pub struct CompileOutput {
48    pub data: StoryData,
49    pub warnings: Vec<ResolvedDiagnostic>,
50}
51
52/// Successful LIR compilation output, including any non-fatal warnings.
53pub struct LirOutput {
54    pub program: brink_ir::lir::Program,
55    pub warnings: Vec<ResolvedDiagnostic>,
56}
57
58/// Compile an ink story from an entry-point file path.
59///
60/// Reads files from disk, follows INCLUDEs, and runs the full compilation
61/// pipeline. Returns the compiled story data or a list of diagnostics.
62pub fn compile_path(path: &Path) -> Result<CompileOutput, CompileError> {
63    compile(path.to_string_lossy().as_ref(), |p| {
64        std::fs::read_to_string(p).map_err(|e| io::Error::new(e.kind(), format!("{p}: {e}")))
65    })
66}
67
68/// Compile an ink story with caller-provided file reading.
69///
70/// The `read_file` callback is called for the entry point and each
71/// `INCLUDE`d file discovered during parsing. This enables compilation in
72/// WASM, tests, and editor contexts where files are not on disk.
73pub fn compile<F>(entry: &str, read_file: F) -> Result<CompileOutput, CompileError>
74where
75    F: FnMut(&str) -> Result<String, io::Error>,
76{
77    driver::compile(entry, read_file)
78}
79
80/// Compile with explicit analysis options — e.g. a registered host-capability
81/// manifest and external-check severity (the "compiler flag, error by
82/// default"). Manifest-driven diagnostics are surfaced as compile warnings or
83/// errors per the severity policy.
84pub fn compile_with_options<F>(
85    entry: &str,
86    read_file: F,
87    options: AnalysisOptions,
88) -> Result<CompileOutput, CompileError>
89where
90    F: FnMut(&str) -> Result<String, io::Error>,
91{
92    driver::compile_with_options(entry, read_file, options)
93}
94
95/// Compile an ink story to the ink.json format (same as inklecate output).
96///
97/// Useful for diffing brink's output against the reference compiler.
98pub fn compile_to_json<F>(entry: &str, read_file: F) -> Result<brink_json::InkJson, CompileError>
99where
100    F: FnMut(&str) -> Result<String, io::Error>,
101{
102    let lir_output = driver::compile_to_lir(entry, read_file)?;
103    Ok(brink_codegen_json::emit(&lir_output.program))
104}
105
106/// Compile ink source from a string to the ink.json format.
107pub fn compile_string_to_json(source: &str) -> Result<brink_json::InkJson, CompileError> {
108    compile_to_json("<string>", |_| Ok(source.to_string()))
109}
110
111/// Errors that can occur during compilation.
112#[derive(Debug, thiserror::Error)]
113pub enum CompileError {
114    /// File I/O error (missing file, permission denied, etc.).
115    #[error("I/O error: {0}")]
116    Io(#[from] io::Error),
117    /// One or more diagnostics prevented compilation.
118    #[error("{} diagnostic(s) prevented compilation", .0.len())]
119    Diagnostics(Vec<ResolvedDiagnostic>),
120    /// Circular INCLUDE dependency detected.
121    #[error("circular INCLUDE dependency: {0}")]
122    CircularInclude(String),
123}
124
125impl From<brink_driver::DiscoverError> for CompileError {
126    fn from(err: brink_driver::DiscoverError) -> Self {
127        match err {
128            brink_driver::DiscoverError::Io(e) => Self::Io(e),
129            brink_driver::DiscoverError::CircularInclude(msg) => Self::CircularInclude(msg),
130        }
131    }
132}