1use alloc::{
4 boxed::Box,
5 collections::BTreeSet,
6 format,
7 string::{String, ToString},
8 vec::Vec,
9};
10
11use diag_lang::{Diagnostic, Label, SourceMap};
12use syntax_lang::Span;
13
14use crate::{Error, Language, codes, error::Report, noml};
15
16const MAX_FILES: usize = 1024;
18
19const MAX_TOTAL: usize = 64 << 20;
21
22#[derive(Clone, Debug, Default)]
88pub struct Sketch {
89 map: SourceMap,
90 paths: Vec<String>,
92 total: usize,
93}
94
95impl Sketch {
96 #[must_use]
111 pub fn new() -> Self {
112 Self::default()
113 }
114
115 pub fn add(&mut self, path: &str, text: impl Into<Box<str>>) -> Result<(), Error> {
140 let text: Box<str> = text.into();
141 let fail = |code, message: String| {
142 Err(Error::located(
143 Vec::from([crate::error::coded(code, Span::empty(0), message)]),
144 1,
145 1,
146 ))
147 };
148 let normalized = match normalize(path) {
149 Ok(p) => p,
150 Err((code, message)) => return fail(code, message),
151 };
152 if self.paths.contains(&normalized) {
153 return fail(
154 codes::FILE_TWICE,
155 format!("`{normalized}` is already in the sketch"),
156 );
157 }
158 if let Some(other) = self
159 .paths
160 .iter()
161 .find(|p| p.eq_ignore_ascii_case(&normalized))
162 {
163 return fail(
164 codes::PATH_CASE,
165 format!(
166 "`{normalized}` and `{other}` differ only by case, which is one file on Windows and macOS"
167 ),
168 );
169 }
170 if text.len() > noml::MAX_SCHEMATIC {
171 return fail(
172 codes::DOCUMENT_TOO_LARGE,
173 format!(
174 "`{normalized}` is larger than {} MiB; a sketch file is at most that",
175 noml::MAX_SCHEMATIC >> 20
176 ),
177 );
178 }
179 if self.total + text.len() > MAX_TOTAL {
180 return fail(
181 codes::SKETCH_TOO_LARGE,
182 format!("a sketch is at most {} MiB in total", MAX_TOTAL >> 20),
183 );
184 }
185 if self.paths.len() >= MAX_FILES {
186 return fail(
187 codes::TOO_MANY_FILES,
188 format!("a sketch has at most {MAX_FILES} files"),
189 );
190 }
191 let len = text.len();
192 if self.map.add(normalized.as_str(), text).is_err() {
193 return fail(
194 codes::SKETCH_TOO_LARGE,
195 String::from("the sketch does not fit in the source map"),
196 );
197 }
198 self.total += len;
199 self.paths.push(normalized);
200 Ok(())
201 }
202
203 #[must_use]
206 pub fn source_map(&self) -> &SourceMap {
207 &self.map
208 }
209
210 #[must_use]
212 pub fn len(&self) -> usize {
213 self.paths.len()
214 }
215
216 #[must_use]
218 pub fn is_empty(&self) -> bool {
219 self.paths.is_empty()
220 }
221
222 fn file(&self, i: usize) -> Option<(&str, u32)> {
224 let (_, file) = self.map.iter().nth(i)?;
225 Some((file.text(), file.span().start().to_u32()))
226 }
227}
228
229fn normalize(path: &str) -> Result<String, (diag_lang::Code, String)> {
231 let bad = |code, why: &str| Err((code, format!("path `{path}` {why}")));
232 if path.is_empty() {
233 return bad(codes::PATH_PORTABLE, "is empty");
234 }
235 if path.starts_with('/') || path.contains('\\') || path.as_bytes().get(1) == Some(&b':') {
236 return bad(
237 codes::PATH_PORTABLE,
238 "must be project-relative and `/`-separated",
239 );
240 }
241 let mut parts: Vec<&str> = Vec::new();
242 for part in path.split('/') {
243 match part {
244 "" | "." => return bad(codes::PATH_PORTABLE, "has an empty or `.` component"),
245 ".." => {
246 if parts.pop().is_none() {
247 return bad(codes::PATH_ESCAPES, "leaves the project root");
248 }
249 }
250 _ => {
251 if part.len() > 255
252 || part.chars().any(|c| {
253 c.is_control() || matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*')
254 })
255 || part.ends_with('.')
256 || part.ends_with(' ')
257 || is_reserved_name(part)
258 {
259 return bad(
260 codes::PATH_PORTABLE,
261 "is not portable to every operating system",
262 );
263 }
264 parts.push(part);
265 }
266 }
267 }
268 if parts.is_empty() {
269 return bad(codes::PATH_PORTABLE, "names no file");
270 }
271 Ok(parts.join("/"))
272}
273
274fn is_reserved_name(part: &str) -> bool {
276 let stem = part.split('.').next().unwrap_or(part).to_ascii_uppercase();
277 matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
278 || ((stem.starts_with("COM") || stem.starts_with("LPT"))
279 && stem.len() == 4
280 && stem.as_bytes()[3].is_ascii_digit()
281 && stem.as_bytes()[3] != b'0')
282}
283
284fn shift(d: &Diagnostic, base: u32) -> Diagnostic {
286 let move_span = |s: Span| Span::new(s.start().to_u32() + base, s.end().to_u32() + base);
287 let primary = d.primary();
288 let label = if primary.message().is_empty() {
289 Label::unlabelled(move_span(primary.span()))
290 } else {
291 Label::new(move_span(primary.span()), primary.message())
292 };
293 let mut out = Diagnostic::new(d.severity(), d.message(), label);
294 for s in d.secondary() {
295 out = out.with_secondary(Label::new(move_span(s.span()), s.message()));
296 }
297 for n in d.notes() {
298 out = out.with_note(n);
299 }
300 for h in d.help() {
301 out = out.with_help(h);
302 }
303 if let Some(code) = d.code() {
304 out = out.with_code(code);
305 }
306 out
307}
308
309impl Language {
310 pub fn from_sketch(sketch: &Sketch) -> Result<Self, Error> {
344 let map = &sketch.map;
345 let mut report = Report::default();
346 let n = sketch.len();
347 let rank_of = |span: Span| -> u32 {
348 let at = span.start();
349 map.locate(at).map_or(0, |(id, _)| {
350 map.iter().position(|(i, _)| i == id).unwrap_or(0) as u32
351 })
352 };
353 let Some((entry_text, entry_base)) = sketch.file(0) else {
354 report.error(codes::MISSING, Span::empty(0), "the sketch has no files");
355 return Err(report.into_sketch_error(map, &rank_of));
356 };
357 let mut roots = Vec::with_capacity(n);
359 for i in 0..n {
360 let Some((text, base)) = sketch.file(i) else {
361 continue;
362 };
363 match noml::read(text) {
364 Ok(root) => roots.push(Some(root)),
365 Err(d) => {
366 report.diagnostic(shift(&d, base));
367 roots.push(None);
368 }
369 }
370 }
371 if !report.is_clean() {
372 return Err(report.into_sketch_error(map, &rank_of));
373 }
374 let mut roots: Vec<noml::Table<'_>> = roots.into_iter().flatten().collect();
375 let format2 = roots[0].entries.iter().any(|e| e.key == "sketch");
376 if !format2 {
377 if n > 1 {
378 report.error(
379 codes::MODULES_OUTSIDE_ENTRY,
380 Span::new(entry_base, entry_base),
381 "a format-1 sketch is one file; give the entry `[sketch] format = 2` and `modules` to split it",
382 );
383 return Err(report.into_sketch_error(map, &rank_of));
384 }
385 return Language::from_lsf(entry_text);
386 }
387 for (i, root) in roots.iter_mut().enumerate() {
388 if let Some((_, base)) = sketch.file(i) {
389 root.rebase(base);
390 }
391 }
392
393 let listed: Vec<(String, Span)> = crate::spec2::modules(&roots[0])
395 .into_iter()
396 .map(|(p, s)| (p.to_string(), s))
397 .collect();
398 let mut order: Vec<usize> = Vec::new();
399 let mut seen: BTreeSet<usize> = BTreeSet::new();
400 for (path, span) in &listed {
401 match normalize(path) {
402 Ok(p) => match sketch.paths.iter().position(|q| *q == p) {
403 Some(0) => report.error(
404 codes::FILE_TWICE,
405 *span,
406 "the entry cannot list itself as a module",
407 ),
408 Some(i) if !seen.insert(i) => {
409 report.error(
410 codes::FILE_TWICE,
411 *span,
412 format!("module `{p}` is listed twice"),
413 );
414 }
415 Some(i) => order.push(i),
416 None => report.error(
417 codes::MODULE_NOT_FOUND,
418 *span,
419 format!("module `{p}` is not in the sketch"),
420 ),
421 },
422 Err((code, message)) => report.error(code, *span, message),
423 }
424 }
425 for (i, path) in sketch.paths.iter().enumerate().skip(1) {
426 if !seen.contains(&i) {
427 let base = sketch.file(i).map_or(0, |(_, b)| b);
428 report.error(
429 codes::MODULE_NOT_FOUND,
430 Span::new(base, base),
431 format!(
432 "`{path}` is in the sketch, but the entry's `modules` does not list it"
433 ),
434 );
435 }
436 }
437 let mut rules_files = 0;
439 for (i, root) in roots.iter().enumerate() {
440 if root.entries.iter().any(|e| e.key == "rules") {
441 rules_files += 1;
442 }
443 if i == 0 {
444 continue;
445 }
446 let base = sketch.file(i).map_or(0, |(_, b)| b);
447 let at = Span::new(base, base);
448 let sketch_table = root.entries.iter().find(|e| e.key == "sketch");
449 let kind = sketch_table.and_then(|e| match &e.value.kind {
450 noml::ValueKind::Table(t) => {
451 t.entries
452 .iter()
453 .find(|k| k.key == "kind")
454 .and_then(|k| match &k.value.kind {
455 noml::ValueKind::Str(s) => Some(s.text.to_string()),
456 _ => None,
457 })
458 }
459 _ => None,
460 });
461 if kind.as_deref() != Some("part") {
462 report.error(
463 codes::NOT_A_PART,
464 sketch_table.map_or(at, |e| e.key_span),
465 format!(
466 "module `{}` must say `[sketch] kind = \"part\"`",
467 sketch.paths[i]
468 ),
469 );
470 }
471 if let Some(language) = root.entries.iter().find(|e| e.key == "language") {
472 report.error(
473 codes::FILE_ROLE,
474 language.key_span,
475 "a part cannot have [language]; it belongs in the entry",
476 );
477 }
478 if !crate::spec2::modules(root).is_empty() {
479 report.error(
480 codes::MODULES_OUTSIDE_ENTRY,
481 at,
482 "`modules` is only allowed in the entry file",
483 );
484 }
485 }
486 if rules_files > 1 {
487 let start_given = roots[0].entries.iter().any(|e| {
488 e.key == "language"
489 && matches!(&e.value.kind, noml::ValueKind::Table(t) if t.entries.iter().any(|k| k.key == "start"))
490 });
491 if !start_given {
492 report.error(
493 codes::START_REQUIRED,
494 Span::new(entry_base, entry_base),
495 "[rules] spans several files, so `[language] start` is required",
496 );
497 }
498 }
499 if !report.is_clean() {
500 return Err(report.into_sketch_error(map, &rank_of));
501 }
502
503 let mut taken: Vec<Option<noml::Table<'_>>> = roots.into_iter().map(Some).collect();
505 let Some(mut merged) = taken[0].take() else {
506 return Err(report.into_sketch_error(map, &rank_of));
507 };
508 for &i in &order {
509 let Some(mut part) = taken[i].take() else {
510 continue;
511 };
512 part.entries.retain(|e| e.key != "sketch");
514 let mut twice = |key: &str, first: Span, second: Span| {
515 report.diagnostic(
516 Diagnostic::new(
517 diag_lang::Severity::Error,
518 format!("`{key}` is defined twice in this sketch"),
519 Label::new(second, "defined again here"),
520 )
521 .with_secondary(Label::new(first, "first defined here"))
522 .with_code(codes::DEFINED_TWICE),
523 );
524 };
525 merged.merge(part, &mut twice);
526 }
527 let spec = crate::schematic::interpret(merged, &mut report);
528 let locate = |span: Span| -> (u32, u32) {
529 map.line_col(span.start())
530 .map_or((1, 1), |(_, lc)| (lc.line, lc.col))
531 };
532 let grammar = spec.and_then(|spec| crate::grammar2::compile(&spec, &locate, &mut report));
533 match grammar {
534 Some(mut grammar) if report.is_clean() => {
535 if let Some(extra) = grammar.extra.as_mut() {
536 extra.warnings = report.into_sketch_warnings(&rank_of).into();
537 }
538 Ok(Language::from_grammar(grammar))
539 }
540 _ => {
541 if report.is_clean() {
542 report.error(
543 codes::MISSING,
544 Span::empty(0),
545 "the sketch could not be forged",
546 );
547 }
548 Err(report.into_sketch_error(map, &rank_of))
549 }
550 }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_normalize_paths() {
560 assert_eq!(normalize("a/b/../c.lsf").as_deref().ok(), Some("a/c.lsf"));
561 assert!(normalize("../x.lsf").is_err());
562 assert!(normalize("a//b").is_err());
563 assert!(normalize("./a").is_err());
564 assert!(normalize("/a").is_err());
565 assert!(normalize("a\\b").is_err());
566 assert!(normalize("CON.lsf").is_err());
567 assert!(normalize("com1").is_err());
568 assert!(normalize("com0").is_ok());
569 assert!(normalize("a?.lsf").is_err());
570 assert!(normalize("a.").is_err());
571 assert!(normalize("x/..").is_err());
572 }
573}