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}