Skip to main content

rustyfi_loader/
error.rs

1use std::path::PathBuf;
2
3use rustyfi_syntax::{ParseFileError, RustyfiVersion};
4
5/// Everything that can go wrong while loading a multi-file SATySFi program.
6#[derive(Debug, thiserror::Error)]
7pub enum LoadError {
8    /// Could not read a source file from disk.
9    #[error("{path}: {source}")]
10    Io {
11        path: PathBuf,
12        #[source]
13        source: std::io::Error,
14    },
15
16    /// A file failed to parse (lex or grammar error). `source` carries the
17    /// original [`ParseFileError`] (span + message) unchanged.
18    #[error("{path}: {source}")]
19    Parse {
20        path: PathBuf,
21        #[source]
22        source: ParseFileError,
23    },
24
25    /// `@require: name` could not be resolved to any file on disk.
26    #[error(
27        "cannot resolve `@require: {name}`; searched: {}",
28        format_searched(.searched)
29    )]
30    UnresolvedRequire {
31        name: String,
32        searched: Vec<PathBuf>,
33    },
34
35    /// `@import: name` could not be resolved to any file on disk.
36    #[error(
37        "cannot resolve `@import: {name}` from {}; searched: {}",
38        .from.display(),
39        format_searched(.searched)
40    )]
41    UnresolvedImport {
42        name: String,
43        from: PathBuf,
44        searched: Vec<PathBuf>,
45    },
46
47    /// The dependency graph contains a cycle; `chain` names the files
48    /// involved, in traversal order, with the first file repeated at the end
49    /// to make the loop explicit (e.g. `[a, b, a]`).
50    #[error("dependency cycle detected: {}", format_chain(.chain))]
51    Cycle { chain: Vec<PathBuf> },
52
53    /// A file reached via `@require:`/`@import:` is a document (has a body)
54    /// rather than a library.
55    #[error(
56        "{path}: required/imported file must be a library (no `in ...` body), found a document"
57    )]
58    DocumentAsDependency { path: PathBuf },
59
60    /// The entry file is a library (no body) rather than a document.
61    #[error("{path}: entry file must be a document (with an `in ...` body), found a library")]
62    LibraryAsEntry { path: PathBuf },
63
64    /// `opts.version` names a SATySFi version this port does not implement
65    /// yet (checked before any file is even read).
66    #[error(
67        "SATySFi {requested} documents are not supported yet; supported: {}",
68        format_versions(.supported)
69    )]
70    UnsupportedVersion {
71        requested: RustyfiVersion,
72        supported: Vec<RustyfiVersion>,
73    },
74
75    /// `LoadMode::Envelopes` was requested for a version with no `use`
76    /// headers — the rejected combination. Checked before any file is read.
77    #[error(
78        "SATySFi {version} has no `use` headers; `Envelopes` mode requires 0.1 \
79         (drop --deps, or pass --lang 0.1)"
80    )]
81    InvalidModeVersion { version: RustyfiVersion },
82
83    /// A [`crate::SourceProvider`] was supplied alongside `LoadMode::Envelopes`.
84    /// The Envelopes backend reads its deps/envelope configs through `std::fs`
85    /// directly, so honouring the provider for some reads and not others would
86    /// assemble a dependency graph half out of memory and half off the disk.
87    #[error(
88        "a custom source provider is only honoured in Legacy mode; \
89         `Envelopes` reads its deps/envelope configs from the filesystem directly"
90    )]
91    SourceProviderUnderEnvelopes,
92
93    /// `use package Mod` with no deps config to resolve `Mod` against
94    /// (upstream's used_as map is empty).
95    #[error(
96        "{from}: cannot resolve `use package {module}` — no pre-resolved \
97         dependency graph; pass --deps <rustyfi-deps.yaml>"
98    )]
99    PackageDependencyUnresolved { module: String, from: PathBuf },
100
101    /// Bare `use Mod` at document/open level (upstream `CannotUseHeaderUse`):
102    /// a document cannot reach into a package's internals by module name.
103    /// Permanent (not a stub): the closed resolver handles bare `use` only
104    /// *inside* envelope source trees.
105    #[error(
106        "{from}: bare `use {module}` is only allowed between files inside one \
107         package; a document must say `use package {module}` or `use {module} \
108         of \"<path>\"`"
109    )]
110    BareUseOutsidePackage { module: String, from: PathBuf },
111
112    /// `` use … of `relpath` `` matched no candidate file on disk.
113    #[error(
114        "cannot resolve `use … of `{relpath}`` from {}; searched: {}",
115        .from.display(),
116        format_searched(.searched)
117    )]
118    UnresolvedUseOf {
119        relpath: String,
120        from: PathBuf,
121        searched: Vec<PathBuf>,
122    },
123
124    /// A `use`-family header under Legacy mode (the mode that resolves
125    /// `@require:`/`@import:`). Names the fix.
126    #[error(
127        "{from}: `{header}` requires Envelopes mode (pass --deps, or let a \
128         `use` header pin it); this load ran in Legacy (@require/@import) mode"
129    )]
130    EnvelopeHeaderUnderLegacy { header: String, from: PathBuf },
131
132    /// A Legacy (`@require:`/`@import:`) header under Envelopes mode. Names
133    /// the fix.
134    #[error(
135        "{from}: `{header}` is a Legacy (@require/@import) header; Envelopes \
136         mode resolves `use package` / `use … of` headers only"
137    )]
138    LegacyHeaderUnderEnvelopes { header: String, from: PathBuf },
139
140    /// The `--deps` file (`rustyfi-deps.yaml`) could not be read.
141    /// Upstream `DepsConfigNotFound`, `depsConfig.ml:40-45`.
142    #[error("{path}: cannot read rustyfi-deps.yaml: {source}")]
143    DepsConfigNotFound {
144        path: PathBuf,
145        #[source]
146        source: std::io::Error,
147    },
148
149    /// `rustyfi-deps.yaml` failed to decode or validate — either a
150    /// YAML/shape error from `serde_yaml` or one of the two non-structural
151    /// checks (`path` must be absolute; `used_as` must be an uppercased
152    /// identifier). Upstream `DepsConfigError` wrapping `YamlError`
153    /// (`depsConfig.ml:46-47`, `yamlDecoder.ml`); `message` carries a
154    /// dotted-path context string in the same spirit as upstream's
155    /// `show_yaml_context` (wording is this port's own).
156    #[error("{path}: invalid rustyfi-deps.yaml: {message}")]
157    DepsConfigDecode { path: PathBuf, message: String },
158
159    /// A `rustyfi-envelope.yaml` could not be read. Upstream
160    /// `EnvelopeConfigNotFound`, `envelopeConfig.ml:142-147`.
161    #[error("{path}: cannot read rustyfi-envelope.yaml: {source}")]
162    EnvelopeConfigNotFound {
163        path: PathBuf,
164        #[source]
165        source: std::io::Error,
166    },
167
168    /// A `rustyfi-envelope.yaml` failed to decode or validate.
169    /// Upstream `EnvelopeConfigError`. Covers the `library`/`font` branch
170    /// check, `opentype_single`/`opentype_collection` branch check, relative
171    /// `path`, lowercased font `name`, and the 18 `markdown_conversion`
172    /// command/identifier shapes.
173    #[error("{path}: invalid rustyfi-envelope.yaml: {message}")]
174    EnvelopeConfigDecode { path: PathBuf, message: String },
175
176    /// A `source_directories` entry of an envelope could not be
177    /// listed. Upstream `CannotReadDirectory`, `envelopeReader.ml:15-16`.
178    #[error("{path}: cannot list envelope source directory: {source}")]
179    CannotReadDirectory {
180        path: PathBuf,
181        #[source]
182        source: std::io::Error,
183    },
184
185    /// Two deps-config envelopes share a name. Upstream
186    /// `EnvelopeNameConflict`, `closedEnvelopeDependencyResolver.ml:50-51`.
187    #[error("rustyfi-deps.yaml: two envelopes are named `{name}`")]
188    EnvelopeNameConflict { name: String },
189
190    /// An envelope depends on a name absent from the deps config's
191    /// envelope set. Upstream `DependencyOnUnknownEnvelope`,
192    /// `closedEnvelopeDependencyResolver.ml:71-76`.
193    #[error("envelope `{depending}` depends on unknown envelope `{depended}`")]
194    DependencyOnUnknownEnvelope { depending: String, depended: String },
195
196    /// A cycle in the envelope dependency graph. Upstream
197    /// `CyclicEnvelopeDependency`; `chain` is names (not paths), the first
198    /// element repeated last, matching [`LoadError::Cycle`]'s shape.
199    #[error("envelope dependency cycle: {}", .chain.join(" -> "))]
200    CyclicEnvelopeDependency { chain: Vec<String> },
201
202    /// Two source files in one envelope declare the same module
203    /// name. Upstream `FileModuleNameConflict`,
204    /// `closedFileDependencyResolver.ml:20-22`.
205    #[error(
206        "envelope module `{module}` is declared by both {} and {}",
207        .prev.display(),
208        .path.display()
209    )]
210    FileModuleNameConflict {
211        module: String,
212        prev: PathBuf,
213        path: PathBuf,
214    },
215
216    /// A bare `use M` inside an envelope names no sibling module.
217    /// Upstream `FileModuleNotFound`, `closedFileDependencyResolver.ml:37-41`.
218    #[error("{from}: `use {module}` names no module in this envelope")]
219    FileModuleNotFound { module: String, from: PathBuf },
220
221    /// A `use … of` header inside an envelope source tree. Upstream
222    /// `CannotUseHeaderUseOf`, `closedFileDependencyResolver.ml:51-52`.
223    #[error(
224        "{from}: `use {module} of …` is not allowed inside a package; package \
225         files address siblings by bare `use <Module>`"
226    )]
227    UseOfInsidePackage { module: String, from: PathBuf },
228
229    /// A `use package M` header whose head matches no `used_as` alias
230    /// in the supplied deps config. Upstream `UnknownPackageDependency`
231    /// (typecheck-side there, load-side here — this port's loader is the only
232    /// header consumer).
233    #[error(
234        "{from}: `use package {module}` does not match any dependency alias \
235         (`used_as`) in the supplied rustyfi-deps.yaml"
236    )]
237    UnknownPackageDependency { module: String, from: PathBuf },
238}
239
240fn format_versions(versions: &[RustyfiVersion]) -> String {
241    versions
242        .iter()
243        .map(|v| v.to_string())
244        .collect::<Vec<_>>()
245        .join(", ")
246}
247
248fn format_searched(searched: &[PathBuf]) -> String {
249    if searched.is_empty() {
250        "(no candidates; is `lib_root` configured?)".to_string()
251    } else {
252        searched
253            .iter()
254            .map(|p| p.display().to_string())
255            .collect::<Vec<_>>()
256            .join(", ")
257    }
258}
259
260fn format_chain(chain: &[PathBuf]) -> String {
261    chain
262        .iter()
263        .map(|p| p.display().to_string())
264        .collect::<Vec<_>>()
265        .join(" -> ")
266}