1use std::{
2 fmt::Display,
3 ops::{Deref, DerefMut},
4};
5
6use serde::{Deserialize, Serialize};
7
8use crate::diagnostics::diagnostics_registry::DIAGNOSTICS;
9use plc_ast::ast::AstNode;
10use plc_source::{
11 source_location::{SourceLocation, SourceLocationFactory},
12 SourceCode,
13};
14
15pub mod diagnostics_registry;
16
17pub const INTERNAL_LLVM_ERROR: &str = "internal llvm codegen error";
18
19#[derive(Default, Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Severity {
23 Ignore,
24 #[default]
25 Info,
26 Warning,
27 Error,
28}
29
30#[derive(Debug)]
34pub struct Diagnostic {
35 pub inner: Box<DiagnosticsInner>,
36}
37
38#[derive(Debug)]
39pub struct DiagnosticsInner {
40 pub message: String,
42 pub primary_location: SourceLocation,
44 pub secondary_locations: Option<Vec<SourceLocation>>,
46 pub error_code: &'static str,
48 pub sub_diagnostics: Vec<Diagnostic>,
50 pub internal_error: Option<anyhow::Error>,
52}
53
54impl Deref for Diagnostic {
55 type Target = DiagnosticsInner;
56
57 fn deref(&self) -> &Self::Target {
58 &self.inner
59 }
60}
61
62impl DerefMut for Diagnostic {
63 fn deref_mut(&mut self) -> &mut Self::Target {
64 &mut self.inner
65 }
66}
67
68impl std::error::Error for Diagnostic {
69 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70 self.internal_error.as_ref().and_then(|it| it.source())
71 }
72}
73
74impl From<std::io::Error> for Diagnostic {
75 fn from(value: std::io::Error) -> Self {
76 Diagnostic::new(value.to_string()).with_error_code("E002").with_internal_error(value.into())
77 }
78}
79
80impl Diagnostic {
82 pub fn new(message: impl Into<String>) -> Self {
83 let inner = DiagnosticsInner {
84 message: message.into(),
85 primary_location: SourceLocation::undefined(),
86 secondary_locations: Default::default(),
87 error_code: "E001", sub_diagnostics: Default::default(),
89 internal_error: Default::default(),
90 };
91 Self { inner: Box::new(inner) }
92 }
93
94 pub fn with_location<T>(mut self, location: T) -> Self
95 where
96 T: Into<SourceLocation>,
97 {
98 self.primary_location = location.into();
99 self
100 }
101
102 pub fn with_secondary_location<T>(mut self, location: T) -> Self
103 where
104 T: Into<SourceLocation>,
105 {
106 self.secondary_locations.get_or_insert_with(Default::default).push(location.into());
107 self
108 }
109
110 pub fn with_secondary_locations(mut self, locations: Vec<SourceLocation>) -> Self {
111 self.secondary_locations.get_or_insert_with(Default::default).extend(locations);
112 self
113 }
114
115 pub fn with_error_code(mut self, code: &'static str) -> Self {
116 debug_assert!(DIAGNOSTICS.get(code).is_some(), "Error {code} does not exist");
117
118 self.error_code = code;
119 self
120 }
121
122 pub fn with_sub_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
123 self.sub_diagnostics.push(diagnostic);
124 self
125 }
126
127 pub fn with_sub_diagnostics(mut self, diagnostics: Vec<Diagnostic>) -> Self {
128 self.sub_diagnostics.extend(diagnostics);
129 self
130 }
131
132 pub fn with_internal_error(mut self, error: anyhow::Error) -> Self {
133 self.internal_error = Some(error);
134 self
135 }
136
137 pub fn from_serde_error(error: serde_json::Error, source: &SourceCode) -> Self {
138 let factory = SourceLocationFactory::for_source(source);
139 let line = error.line();
140 let column = error.column();
141
142 let message = error.to_string();
144 let message = if let Some(pos) = message.find("at line") {
145 message.chars().take(pos).collect()
146 } else {
147 message
148 };
149 let range = factory.create_range_to_end_of_line(line, column);
150 Diagnostic::new(message).with_error_code("E088").with_location(range)
151 }
152}
153
154impl PartialEq for Diagnostic {
155 fn eq(&self, other: &Self) -> bool {
156 self.error_code == other.error_code
157 && self.message == other.message
158 && self.primary_location == other.primary_location
159 && self.secondary_locations == other.secondary_locations
160 && self.sub_diagnostics == other.sub_diagnostics
161 }
162}
163
164impl Eq for Diagnostic {}
165
166impl Display for Diagnostic {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 write!(f, "{}", self.get_message())?;
169 let location = self.get_location();
170 if !location.is_undefined() {
171 write!(f, " at: {location}")
172 } else {
173 Ok(())
174 }
175 }
176}
177
178impl Diagnostic {
179 pub fn get_message(&self) -> &str {
180 self.message.as_str()
181 }
182
183 pub fn get_location(&self) -> SourceLocation {
184 self.primary_location.clone()
185 }
186
187 pub fn get_secondary_locations(&self) -> Option<&[SourceLocation]> {
188 self.secondary_locations.as_deref()
189 }
190
191 pub fn get_error_code(&self) -> &'static str {
192 self.error_code
193 }
194
195 pub fn get_sub_diagnostics(&self) -> Vec<&Diagnostic> {
196 let mut diagnostics = vec![];
197 self.sub_diagnostics.iter().for_each(|d| {
198 diagnostics.push(d);
199 diagnostics.extend(d.get_sub_diagnostics());
200 });
201 diagnostics
202 }
203}
204
205impl Diagnostic {
207 pub fn unexpected_token_found<T>(expected: &str, found: &str, location: T) -> Diagnostic
208 where
209 T: Into<SourceLocation>,
210 {
211 Diagnostic::new(format!("Unexpected token: expected {expected} but found {found}"))
212 .with_error_code("E007")
213 .with_location(location)
214 }
215
216 pub fn missing_function<T>(location: T) -> Diagnostic
217 where
218 T: Into<SourceLocation>,
219 {
220 Diagnostic::new("Cannot generate code outside of function context.")
221 .with_error_code("E072")
222 .with_location(location.into())
223 }
224
225 pub fn codegen_error<T, U>(message: T, location: U) -> Diagnostic
226 where
227 T: Into<String>,
228 U: Into<SourceLocation>,
229 {
230 Diagnostic::new(message).with_location(location.into()).with_error_code("E071")
231 }
232
233 pub fn llvm_error(file: &str, llvm_error: &str) -> Diagnostic {
234 Diagnostic::new(format!("{file}: Internal llvm error: {:}", llvm_error)).with_error_code("E005")
235 }
236
237 pub fn missing_token<T>(expected_token: &str, location: T) -> Diagnostic
238 where
239 T: Into<SourceLocation>,
240 {
241 Diagnostic::new(format!("Missing expected Token {expected_token}"))
242 .with_location(location)
243 .with_error_code("E006")
244 }
245
246 pub fn invalid_argument_count<T>(expected: usize, actual: usize, location: T) -> Diagnostic
247 where
248 T: Into<SourceLocation>,
249 {
250 fn message(value: usize) -> String {
252 match value {
253 1 => format!("{value} argument"),
254 _ => format!("{value} arguments"),
255 }
256 }
257
258 Diagnostic::new(format!(
259 "this POU takes {expected} but {actual} {was_or_were} supplied",
260 expected = message(expected),
261 actual = message(actual),
262 was_or_were = if actual == 1 { "was" } else { "were" }
263 ))
264 .with_error_code("E032")
265 .with_location(location.into())
266 }
267
268 pub fn unknown_type<T>(type_name: &str, location: T) -> Diagnostic
269 where
270 T: Into<SourceLocation>,
271 {
272 Diagnostic::new(format!("Unknown type: {type_name:}")).with_error_code("E052").with_location(location)
273 }
274
275 pub fn unresolved_reference<T>(reference: &str, location: T) -> Diagnostic
276 where
277 T: Into<SourceLocation>,
278 {
279 Diagnostic::new(format!("Could not resolve reference to {reference:}"))
280 .with_error_code("E048")
281 .with_location(location)
282 }
283
284 pub fn invalid_assignment<T>(right_type: &str, left_type: &str, location: T) -> Diagnostic
285 where
286 T: Into<SourceLocation>,
287 {
288 Diagnostic::new(format!("Invalid assignment: cannot assign '{right_type}' to '{left_type}'"))
289 .with_error_code("E037")
290 .with_location(location)
291 }
292
293 pub fn invalid_polymorphic_assignment<T>(right_type: &str, left_type: &str, location: T) -> Diagnostic
294 where
295 T: Into<SourceLocation>,
296 {
297 Diagnostic::new(format!(
298 "Invalid assignment: '{left_type}' and '{right_type}' are not related and cannot be used polymorphically"
299 ))
300 .with_error_code("E125")
301 .with_location(location)
302 }
303
304 pub fn invalid_interface_pou_assignment<T>(
305 pou_name: &str,
306 interface_name: &str,
307 location: T,
308 ) -> Diagnostic
309 where
310 T: Into<SourceLocation>,
311 {
312 Diagnostic::new(format!(
313 "Invalid assignment: '{pou_name}' does not implement interface '{interface_name}'"
314 ))
315 .with_error_code("E126")
316 .with_location(location)
317 }
318
319 pub fn invalid_interface_assignment<T>(source_iface: &str, target_iface: &str, location: T) -> Diagnostic
320 where
321 T: Into<SourceLocation>,
322 {
323 Diagnostic::new(format!(
324 "Invalid assignment: '{target_iface}' and '{source_iface}' are not related and cannot be used polymorphically"
325 ))
326 .with_error_code("E126")
327 .with_location(location)
328 }
329
330 pub fn invalid_direct_interface_call<T>(location: T) -> Diagnostic
331 where
332 T: Into<SourceLocation>,
333 {
334 Diagnostic::new("Interfaces cannot be called directly")
335 .with_error_code("E129")
336 .with_location(location)
337 }
338
339 pub fn cannot_generate_initializer<T>(variable_name: &str, location: T) -> Diagnostic
340 where
341 T: Into<SourceLocation>,
342 {
343 Self::new(format!(
344 "Cannot generate literal initializer for '{variable_name}': Value cannot be derived"
345 ))
346 .with_error_code("E041")
347 .with_location(location)
348 }
349
350 pub fn cannot_generate_call_statement(operator: &AstNode) -> Diagnostic {
351 Diagnostic::codegen_error(format!("cannot generate call statement for {:?}", operator), operator)
353 }
354
355 pub fn cannot_generate_from_empty_literal<T>(type_name: &str, location: T) -> Diagnostic
356 where
357 T: Into<SourceLocation>,
358 {
359 Diagnostic::codegen_error(
360 format!("Cannot generate {type_name} from empty literal").as_str(),
361 location,
362 )
363 }
364
365 pub fn const_pragma_is_not_allowed<T>(location: T) -> Diagnostic
366 where
367 T: Into<SourceLocation>,
368 {
369 Diagnostic::new("Pragma {constant} is not allowed in POU declarations")
370 .with_location(location)
371 .with_error_code("E105")
372 }
373}
374
375impl Diagnostic {
377 pub fn unnamed_control<T>(location: T) -> Diagnostic
378 where
379 T: Into<SourceLocation>,
380 {
381 Diagnostic::new("Unnamed control").with_error_code("E087").with_location(location)
382 }
383
384 pub fn unsupported_cfc_expression<T>(expression: &str, location: T) -> Diagnostic
385 where
386 T: Into<SourceLocation>,
387 {
388 Diagnostic::new(format!("Unsupported CFC expression: `{expression}`"))
389 .with_error_code("E083")
390 .with_location(location)
391 }
392
393 pub fn unconnected_element<T>(name: &str, location: T) -> Diagnostic
394 where
395 T: Into<SourceLocation>,
396 {
397 Diagnostic::new(format!("Element `{name}` is unconnected and will be ignored"))
398 .with_error_code("E084")
399 .with_location(location)
400 }
401
402 pub fn disconnected_return<T>(location: T) -> Diagnostic
403 where
404 T: Into<SourceLocation>,
405 {
406 Diagnostic::new("Return element is not connected to a condition")
407 .with_error_code("E085")
408 .with_location(location)
409 }
410
411 pub fn duplicate_connector<T>(label: &str, location: T) -> Diagnostic
412 where
413 T: Into<SourceLocation>,
414 {
415 Diagnostic::new(format!("Connector `{label}` is already defined"))
416 .with_error_code("E081")
417 .with_location(location)
418 }
419
420 pub fn dangling_continuation<T>(label: &str, location: T) -> Diagnostic
421 where
422 T: Into<SourceLocation>,
423 {
424 Diagnostic::new(format!("Continuation `{label}` has no matching connector"))
425 .with_error_code("E082")
426 .with_location(location)
427 }
428
429 pub fn open_connector<T>(label: &str, location: T) -> Diagnostic
430 where
431 T: Into<SourceLocation>,
432 {
433 Diagnostic::new(format!("Connector `{label}` has no incoming connection"))
434 .with_error_code("E086")
435 .with_location(location)
436 }
437
438 pub fn undefined_jump_target<T>(label: &str, location: T) -> Diagnostic
439 where
440 T: Into<SourceLocation>,
441 {
442 Diagnostic::new(format!("Jump refers to undefined label `{label}`"))
443 .with_error_code("E142")
444 .with_location(location)
445 }
446
447 pub fn unused_label<T>(label: &str, location: T) -> Diagnostic
448 where
449 T: Into<SourceLocation>,
450 {
451 Diagnostic::new(format!("Label `{label}` is not referenced by any jump"))
452 .with_error_code("E143")
453 .with_location(location)
454 }
455
456 pub fn duplicate_label<T>(label: &str, location: T) -> Diagnostic
457 where
458 T: Into<SourceLocation>,
459 {
460 Diagnostic::new(format!("Label `{label}` is already defined"))
461 .with_error_code("E144")
462 .with_location(location)
463 }
464
465 pub fn unknown_block_type<T>(name: &str, location: T) -> Diagnostic
466 where
467 T: Into<SourceLocation>,
468 {
469 Diagnostic::new(format!("Block `{name}` refers to an undeclared POU"))
470 .with_error_code("E146")
471 .with_location(location)
472 }
473
474 pub fn undeclared_block_output<T>(output: &str, name: &str, location: T) -> Diagnostic
475 where
476 T: Into<SourceLocation>,
477 {
478 Diagnostic::new(format!("Output `{output}` is not declared by `{name}`"))
479 .with_error_code("E147")
480 .with_location(location)
481 }
482
483 pub fn unconnected_en<T>(name: &str, location: T) -> Diagnostic
484 where
485 T: Into<SourceLocation>,
486 {
487 Diagnostic::new(format!("Block `{name}` has an unconnected EN pin"))
488 .with_error_code("E152")
489 .with_location(location)
490 }
491
492 pub fn eno_cycle<T>(name: &str, location: T) -> Diagnostic
493 where
494 T: Into<SourceLocation>,
495 {
496 Diagnostic::new(format!("EN pin of block `{name}` resolves through an ENO cycle"))
497 .with_error_code("E153")
498 .with_location(location)
499 }
500
501 pub fn negated_reference_assignment<T>(name: &str, location: T) -> Diagnostic
502 where
503 T: Into<SourceLocation>,
504 {
505 Diagnostic::new(format!("Reference assignment to `{name}` cannot be negated"))
506 .with_error_code("E154")
507 .with_location(location)
508 }
509
510 pub fn duplicate_return_pin<T>(name: &str, location: T) -> Diagnostic
511 where
512 T: Into<SourceLocation>,
513 {
514 Diagnostic::new(format!("Block `{name}` has more than one return pin"))
515 .with_error_code("E155")
516 .with_location(location)
517 }
518
519 pub fn disconnected_jump<T>(location: T) -> Diagnostic
520 where
521 T: Into<SourceLocation>,
522 {
523 Diagnostic::new("Jump element is not connected to a condition and can never be taken")
524 .with_error_code("E145")
525 .with_location(location)
526 }
527
528 pub fn unresolved_generic_output<T>(block: &str, location: T) -> Diagnostic
529 where
530 T: Into<SourceLocation>,
531 {
532 Diagnostic::new(format!(
533 "Cannot determine a type for generic block `{block}`: no input decides its type"
534 ))
535 .with_error_code("E149")
536 .with_location(location)
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use codespan_reporting::files::{Location, SimpleFile};
543
544 use crate::{diagnostics::Severity, reporter::clang::ClangFormatDiagnosticReporter};
545
546 #[test]
547 fn test_build_diagnostic_msg() {
548 let reporter = ClangFormatDiagnosticReporter::default();
549 let file = SimpleFile::new("test.st".to_string(), "source".to_string());
550 let start = Location { line_number: 4, column_number: 1 };
551 let end = Location { line_number: 4, column_number: 4 };
552 let res = reporter.build_diagnostic_msg(
553 Some(&file),
554 Some(&start),
555 Some(&end),
556 "E001",
557 &Severity::Error,
558 "This is an error",
559 );
560
561 assert_eq!(res, "test.st:4:1:{4:1-4:4}: error[E001]: This is an error");
562 }
563
564 #[test]
565 fn test_build_diagnostic_msg_equal_start_end() {
566 let reporter = ClangFormatDiagnosticReporter::default();
567 let file = SimpleFile::new("test.st".to_string(), "source".to_string());
568 let start = Location { line_number: 4, column_number: 1 };
569 let end = Location { line_number: 4, column_number: 1 };
570 let res = reporter.build_diagnostic_msg(
571 Some(&file),
572 Some(&start),
573 Some(&end),
574 "E001",
575 &Severity::Error,
576 "This is an error",
577 );
578
579 assert_eq!(res, "test.st:4:1: error[E001]: This is an error");
580 }
581
582 #[test]
583 fn test_build_diagnostic_msg_no_location() {
584 let reporter = ClangFormatDiagnosticReporter::default();
585 let file = SimpleFile::new("test.st".to_string(), "source".to_string());
586 let res = reporter.build_diagnostic_msg(
587 Some(&file),
588 None,
589 None,
590 "E001",
591 &Severity::Error,
592 "This is an error",
593 );
594
595 assert_eq!(res, "test.st: error[E001]: This is an error");
596 }
597
598 #[test]
599 fn test_build_diagnostic_msg_no_file() {
600 let reporter = ClangFormatDiagnosticReporter::default();
601 let start = Location { line_number: 4, column_number: 1 };
602 let end = Location { line_number: 4, column_number: 4 };
603 let res = reporter.build_diagnostic_msg(
604 None,
605 Some(&start),
606 Some(&end),
607 "E001",
608 &Severity::Error,
609 "This is an error",
610 );
611
612 assert_eq!(res, "error[E001]: This is an error");
613 }
614
615 #[test]
616 fn test_build_diagnostic_msg_no_file_no_location() {
617 let reporter = ClangFormatDiagnosticReporter::default();
618 let res =
619 reporter.build_diagnostic_msg(None, None, None, "E001", &Severity::Error, "This is an error");
620
621 assert_eq!(res, "error[E001]: This is an error");
622 }
623}