1pub mod ast;
5pub mod bms;
6pub mod copy;
7pub mod csd;
8mod debugging;
9mod directives;
10pub mod dli;
11pub mod extended;
12pub mod feedback;
13pub mod jni;
14pub mod lexer;
15pub mod messages;
16pub mod parser;
17pub mod report;
18pub mod source;
19pub mod sql;
20pub mod system;
21
22use std::fmt;
23
24pub use rt::vocab::Pos;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub enum Severity {
30 Informational,
32 Warning,
34 Error,
36 Severe,
38 Unrecoverable,
40}
41
42impl Severity {
43 pub const fn return_code(self) -> u8 {
45 match self {
46 Self::Informational => 0,
47 Self::Warning => 4,
48 Self::Error => 8,
49 Self::Severe => 12,
50 Self::Unrecoverable => 16,
51 }
52 }
53
54 pub const fn label(self) -> Option<&'static str> {
56 match self {
57 Self::Informational => Some("informational"),
58 Self::Warning => Some("warning"),
59 Self::Error | Self::Severe | Self::Unrecoverable => None,
60 }
61 }
62
63 pub const fn letter(self) -> char {
65 match self {
66 Self::Informational => 'I',
67 Self::Warning => 'W',
68 Self::Error => 'E',
69 Self::Severe => 'S',
70 Self::Unrecoverable => 'U',
71 }
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct Error {
79 pub pos: Pos,
80 pub message: String,
81 pub file: Option<String>,
83 pub severity: Severity,
84 pub id: Option<&'static str>,
86}
87
88impl Error {
89 pub fn at(pos: Pos, message: impl Into<String>) -> Self {
90 Self { pos, message: message.into(), file: None, severity: Severity::Severe, id: None }
91 }
92
93 pub fn warning(pos: Pos, message: impl Into<String>) -> Self {
94 Self::at(pos, message).graded(Severity::Warning)
95 }
96
97 pub fn graded(mut self, severity: Severity) -> Self {
98 self.severity = severity;
99 self
100 }
101
102 pub fn in_files(mut self, files: &[String]) -> Self {
104 self.file = files.get(self.pos.file as usize).filter(|f| !f.is_empty()).cloned();
105 self
106 }
107
108 pub fn place(&self, main: &str) -> String {
112 match (&self.file, self.pos.line) {
113 (Some(f), _) => format!("{f}:{self}"),
114 (None, 0) => format!("{main}: {}", self.labelled()),
115 (None, _) => format!("{main}:{self}"),
116 }
117 }
118
119 pub fn labelled(&self) -> String {
121 let id = self.id.map(|id| format!("{id}-{} ", self.severity.letter())).unwrap_or_default();
122 match self.severity.label() {
123 Some(label) => format!("{label}: {id}{}", self.message),
124 None => format!("{id}{}", self.message),
125 }
126 }
127}
128
129impl fmt::Display for Error {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}: {}", self.pos, self.labelled())
132 }
133}
134
135pub fn return_code(messages: &[Error]) -> u8 {
137 messages.iter().map(|m| m.severity.return_code()).max().unwrap_or(0)
138}
139
140pub fn most_severe(messages: &[Error]) -> Option<&Error> {
142 messages.iter().rev().max_by_key(|m| m.severity)
143}
144
145impl std::error::Error for Error {}
146
147pub fn parse(text: &str) -> Result<ast::Program, Error> {
149 parse_with(text, ©::Libraries::default())
150}
151
152pub fn parse_with(text: &str, libraries: ©::Libraries) -> Result<ast::Program, Error> {
154 Ok(parse_all_with(text, libraries)?.remove(0))
155}
156
157pub fn program_ids(text: &str, compliance: numeric::Compliance) -> Vec<String> {
160 let Ok(tokens) = source::read_under(text, 0, false, compliance).and_then(|s| lexer::lex_under(&s, compliance)) else { return Vec::new() };
161 let mut names = Vec::new();
162 let mut rest = tokens.iter().map(|t| &t.tok);
163 while let Some(tok) = rest.next() {
164 if !matches!(tok, lexer::Tok::Word(w) if w == "PROGRAM-ID") {
165 continue;
166 }
167 let name = match rest.next() {
168 Some(lexer::Tok::Period) => rest.next(),
169 other => other,
170 };
171 if let Some(lexer::Tok::Word(n) | lexer::Tok::Alnum(n)) = name {
172 names.push(n.clone());
173 }
174 }
175 names
176}
177
178pub fn parse_all_with(text: &str, libraries: ©::Libraries) -> Result<Vec<ast::Program>, Error> {
185 let compliance = libraries.compliance();
186 let mut files = vec![String::new()];
187 let read = |debugging: bool, files: &mut Vec<String>| -> Result<(source::Source, Vec<lexer::Token>), Error> {
188 let source = source::read_under(text, 0, debugging, compliance).and_then(|s| copy::expand(s, libraries, files)).and_then(copy::replace).map_err(|e| e.in_files(files))?;
189 let tokens = lexer::lex_under(&source, compliance).map_err(|e| e.in_files(files))?;
190 Ok((source, tokens))
191 };
192 let (source, mut tokens) = match read(true, &mut files) {
193 Ok((source, lexed)) => {
194 let tokens = debugging::keep(lexed, source.debugging.as_deref().unwrap_or_default());
195 (source, tokens)
196 }
197 Err(e) => {
198 files.truncate(1);
199 let (source, tokens) = read(false, &mut files)?;
200 if debugging::requested(&tokens) {
201 return Err(e);
202 }
203 (source, tokens)
204 }
205 };
206 if compliance == numeric::Compliance::Extended {
207 tokens = extended::rewrite(tokens, &source.options).map_err(|e| e.in_files(&files))?;
208 }
209 let mut programs = parser::parse(&tokens, source.options, compliance).map_err(|e| e.in_files(&files))?;
210 for p in &mut programs {
211 p.sources = files.clone();
212 }
213 Ok(programs)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 const ALL: [Severity; 5] = [Severity::Informational, Severity::Warning, Severity::Error, Severity::Severe, Severity::Unrecoverable];
221
222 fn message(severity: Severity, line: u32) -> Error {
223 Error::at(Pos { file: 0, line, col: 8 }, format!("m{line}")).graded(severity)
224 }
225
226 #[test]
227 fn severities_rise_with_ibms_return_codes() {
228 assert_eq!(ALL.map(Severity::return_code), [0, 4, 8, 12, 16]);
229 assert!(ALL.windows(2).all(|w| w[0] < w[1]));
230 assert_eq!(Error::at(Pos::default(), "m").severity, Severity::Severe);
231 assert_eq!(Error::warning(Pos::default(), "m").severity, Severity::Warning);
232 }
233
234 #[test]
235 fn a_compilation_returns_the_highest_code_of_its_messages() {
236 let codes = |severities: &[Severity]| return_code(&severities.iter().enumerate().map(|(i, &s)| message(s, i as u32 + 1)).collect::<Vec<_>>());
237 use Severity::*;
238 assert_eq!(codes(&[]), 0);
239 assert_eq!(codes(&[Informational, Informational]), 0);
240 assert_eq!(codes(&[Informational, Warning]), 4);
241 assert_eq!(codes(&[Warning, Error, Informational]), 8);
242 assert_eq!(codes(&[Error, Severe, Warning]), 12);
243 assert_eq!(codes(&[Informational, Warning, Error, Severe]), 12);
244 assert_eq!(codes(&[Severe, Unrecoverable]), 16);
245 }
246
247 #[test]
248 fn the_message_a_refusal_names_is_the_first_of_the_most_severe() {
249 let messages = [message(Severity::Warning, 1), message(Severity::Severe, 2), message(Severity::Error, 3), message(Severity::Severe, 4)];
250 assert_eq!(most_severe(&messages).map(|m| m.pos.line), Some(2));
251 assert!(most_severe(&[]).is_none());
252 }
253
254 #[test]
255 fn an_error_line_carries_no_label_and_a_warning_or_informational_one_says_which_it_is() {
256 let lines: Vec<String> = ALL.map(|s| message(s, 7).place("p.cbl")).to_vec();
257 assert_eq!(lines, ["p.cbl:7:8: informational: m7", "p.cbl:7:8: warning: m7", "p.cbl:7:8: m7", "p.cbl:7:8: m7", "p.cbl:7:8: m7"]);
258 let unplaced = |s: Severity| Error::at(Pos::default(), "m").graded(s).place("p.cbl");
259 assert_eq!((unplaced(Severity::Severe), unplaced(Severity::Warning)), ("p.cbl: m".to_string(), "p.cbl: warning: m".to_string()));
260 let in_member = Error::warning(Pos { file: 1, line: 3, col: 8 }, "m3").in_files(&[String::new(), "COPYA".into()]);
261 assert_eq!(in_member.place("p.cbl"), "COPYA:3:8: warning: m3");
262 assert_eq!(message(Severity::Severe, 3).to_string(), "3:8: m3");
263 }
264}