Skip to main content

ironwork_syntax/
lib.rs

1//! ironwork for COBOL: from fixed-format source text to a syntax tree. Nothing here knows about
2//! storage or execution.
3
4pub mod ast;
5pub mod bms;
6pub mod copy;
7pub mod csd;
8mod debugging;
9pub mod dli;
10pub mod extended;
11pub mod feedback;
12pub mod jni;
13pub mod lexer;
14pub mod messages;
15pub mod parser;
16pub mod report;
17pub mod source;
18pub mod sql;
19pub mod system;
20
21use std::fmt;
22
23pub use rt::vocab::Pos;
24
25/// The severity of a compiler message, IBM's five levels in order (Programming Guide SC27-8714-03,
26/// Table 38, p. 282).
27#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub enum Severity {
29    /// I: the program runs correctly.
30    Informational,
31    /// W: a possible error; the program probably runs correctly.
32    Warning,
33    /// E: an error IBM's compiler corrects, though the program may not run as expected.
34    Error,
35    /// S: an error the compiler cannot correct; the program should not be run.
36    Severe,
37    /// U: the compilation ended.
38    Unrecoverable,
39}
40
41impl Severity {
42    /// The return code a compilation ends with when this is its most severe message.
43    pub const fn return_code(self) -> u8 {
44        match self {
45            Self::Informational => 0,
46            Self::Warning => 4,
47            Self::Error => 8,
48            Self::Severe => 12,
49            Self::Unrecoverable => 16,
50        }
51    }
52
53    /// The word a message's line carries after its position; an error's carries none.
54    pub const fn label(self) -> Option<&'static str> {
55        match self {
56            Self::Informational => Some("informational"),
57            Self::Warning => Some("warning"),
58            Self::Error | Self::Severe | Self::Unrecoverable => None,
59        }
60    }
61
62    /// The letter after a message id's number: `IWC0101-S`.
63    pub const fn letter(self) -> char {
64        match self {
65            Self::Informational => 'I',
66            Self::Warning => 'W',
67            Self::Error => 'E',
68            Self::Severe => 'S',
69            Self::Unrecoverable => 'U',
70        }
71    }
72}
73
74/// A compiler message; [`Error::at`] makes a severe one, and [`messages::Message::at`] one from the
75/// catalogue, with its id.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct Error {
78    pub pos: Pos,
79    pub message: String,
80    /// The COPY member the position is in, when it is not the program itself.
81    pub file: Option<String>,
82    pub severity: Severity,
83    /// The catalogue's id for the message, without its severity letter (docs/messages.md).
84    pub id: Option<&'static str>,
85}
86
87impl Error {
88    pub fn at(pos: Pos, message: impl Into<String>) -> Self {
89        Self { pos, message: message.into(), file: None, severity: Severity::Severe, id: None }
90    }
91
92    pub fn warning(pos: Pos, message: impl Into<String>) -> Self {
93        Self::at(pos, message).graded(Severity::Warning)
94    }
95
96    pub fn graded(mut self, severity: Severity) -> Self {
97        self.severity = severity;
98        self
99    }
100
101    /// Names the member a position in a COPY member came from.
102    pub fn in_files(mut self, files: &[String]) -> Self {
103        self.file = files.get(self.pos.file as usize).filter(|f| !f.is_empty()).cloned();
104        self
105    }
106
107    /// `file:line:col: message`, with `main` naming the program itself; a warning or informational
108    /// message puts its label before the message, and an id goes before the message:
109    /// `file:line:col: warning: IWX0001-W message`.
110    pub fn place(&self, main: &str) -> String {
111        match (&self.file, self.pos.line) {
112            (Some(f), _) => format!("{f}:{self}"),
113            (None, 0) => format!("{main}: {}", self.labelled()),
114            (None, _) => format!("{main}:{self}"),
115        }
116    }
117
118    /// The message after its id, and both after its severity's label when it has one.
119    pub fn labelled(&self) -> String {
120        let id = self.id.map(|id| format!("{id}-{} ", self.severity.letter())).unwrap_or_default();
121        match self.severity.label() {
122            Some(label) => format!("{label}: {id}{}", self.message),
123            None => format!("{id}{}", self.message),
124        }
125    }
126}
127
128impl fmt::Display for Error {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}: {}", self.pos, self.labelled())
131    }
132}
133
134/// The return code of a compilation that gave these messages: the highest of theirs, 0 for none.
135pub fn return_code(messages: &[Error]) -> u8 {
136    messages.iter().map(|m| m.severity.return_code()).max().unwrap_or(0)
137}
138
139/// The message a refusal names: the first of the most severe.
140pub fn most_severe(messages: &[Error]) -> Option<&Error> {
141    messages.iter().rev().max_by_key(|m| m.severity)
142}
143
144impl std::error::Error for Error {}
145
146/// The first program in the source.
147pub fn parse(text: &str) -> Result<ast::Program, Error> {
148    parse_with(text, &copy::Libraries::default())
149}
150
151/// The first program in the source, with COPY members resolved from `libraries`.
152pub fn parse_with(text: &str, libraries: &copy::Libraries) -> Result<ast::Program, Error> {
153    Ok(parse_all_with(text, libraries)?.remove(0))
154}
155
156/// The names after each PROGRAM-ID in the source, read without COPY members or a parse; empty for
157/// a source that does not lex.
158pub fn program_ids(text: &str, compliance: numeric::Compliance) -> Vec<String> {
159    let Ok(tokens) = source::read_under(text, 0, false, compliance).and_then(|s| lexer::lex_under(&s, compliance)) else { return Vec::new() };
160    let mut names = Vec::new();
161    let mut rest = tokens.iter().map(|t| &t.tok);
162    while let Some(tok) = rest.next() {
163        if !matches!(tok, lexer::Tok::Word(w) if w == "PROGRAM-ID") {
164            continue;
165        }
166        let name = match rest.next() {
167            Some(lexer::Tok::Period) => rest.next(),
168            other => other,
169        };
170        if let Some(lexer::Tok::Word(n) | lexer::Tok::Alnum(n)) = name {
171            names.push(n.clone());
172        }
173    }
174    names
175}
176
177/// Every program in the source, in order, nested programs after the one that contains them. The
178/// libraries' compliance level says how the source and its members are read. Debugging lines are
179/// program text through COPY and REPLACE, and comments after them outside a program compiled WITH
180/// DEBUGGING MODE (Language Reference SC27-8713-03, p. 693). Outside debugging mode Enterprise
181/// COBOL accepts a debugging line that does not read as text, such as one holding an unclosed
182/// literal; then every debugging line is read as a comment.
183pub fn parse_all_with(text: &str, libraries: &copy::Libraries) -> Result<Vec<ast::Program>, Error> {
184    let compliance = libraries.compliance();
185    let mut files = vec![String::new()];
186    let read = |debugging: bool, files: &mut Vec<String>| -> Result<(source::Source, Vec<lexer::Token>), Error> {
187        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))?;
188        let tokens = lexer::lex_under(&source, compliance).map_err(|e| e.in_files(files))?;
189        Ok((source, tokens))
190    };
191    let (source, mut tokens) = match read(true, &mut files) {
192        Ok((source, lexed)) => {
193            let tokens = debugging::keep(lexed, source.debugging.as_deref().unwrap_or_default());
194            (source, tokens)
195        }
196        Err(e) => {
197            files.truncate(1);
198            let (source, tokens) = read(false, &mut files)?;
199            if debugging::requested(&tokens) {
200                return Err(e);
201            }
202            (source, tokens)
203        }
204    };
205    if compliance == numeric::Compliance::Extended {
206        tokens = extended::rewrite(tokens, &source.options).map_err(|e| e.in_files(&files))?;
207    }
208    let mut programs = parser::parse(&tokens, source.options).map_err(|e| e.in_files(&files))?;
209    for p in &mut programs {
210        p.sources = files.clone();
211    }
212    Ok(programs)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    const ALL: [Severity; 5] = [Severity::Informational, Severity::Warning, Severity::Error, Severity::Severe, Severity::Unrecoverable];
220
221    fn message(severity: Severity, line: u32) -> Error {
222        Error::at(Pos { file: 0, line, col: 8 }, format!("m{line}")).graded(severity)
223    }
224
225    #[test]
226    fn severities_rise_with_ibms_return_codes() {
227        assert_eq!(ALL.map(Severity::return_code), [0, 4, 8, 12, 16]);
228        assert!(ALL.windows(2).all(|w| w[0] < w[1]));
229        assert_eq!(Error::at(Pos::default(), "m").severity, Severity::Severe);
230        assert_eq!(Error::warning(Pos::default(), "m").severity, Severity::Warning);
231    }
232
233    #[test]
234    fn a_compilation_returns_the_highest_code_of_its_messages() {
235        let codes = |severities: &[Severity]| return_code(&severities.iter().enumerate().map(|(i, &s)| message(s, i as u32 + 1)).collect::<Vec<_>>());
236        use Severity::*;
237        assert_eq!(codes(&[]), 0);
238        assert_eq!(codes(&[Informational, Informational]), 0);
239        assert_eq!(codes(&[Informational, Warning]), 4);
240        assert_eq!(codes(&[Warning, Error, Informational]), 8);
241        assert_eq!(codes(&[Error, Severe, Warning]), 12);
242        assert_eq!(codes(&[Informational, Warning, Error, Severe]), 12);
243        assert_eq!(codes(&[Severe, Unrecoverable]), 16);
244    }
245
246    #[test]
247    fn the_message_a_refusal_names_is_the_first_of_the_most_severe() {
248        let messages = [message(Severity::Warning, 1), message(Severity::Severe, 2), message(Severity::Error, 3), message(Severity::Severe, 4)];
249        assert_eq!(most_severe(&messages).map(|m| m.pos.line), Some(2));
250        assert!(most_severe(&[]).is_none());
251    }
252
253    #[test]
254    fn an_error_line_carries_no_label_and_a_warning_or_informational_one_says_which_it_is() {
255        let lines: Vec<String> = ALL.map(|s| message(s, 7).place("p.cbl")).to_vec();
256        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"]);
257        let unplaced = |s: Severity| Error::at(Pos::default(), "m").graded(s).place("p.cbl");
258        assert_eq!((unplaced(Severity::Severe), unplaced(Severity::Warning)), ("p.cbl: m".to_string(), "p.cbl: warning: m".to_string()));
259        let in_member = Error::warning(Pos { file: 1, line: 3, col: 8 }, "m3").in_files(&[String::new(), "COPYA".into()]);
260        assert_eq!(in_member.place("p.cbl"), "COPYA:3:8: warning: m3");
261        assert_eq!(message(Severity::Severe, 3).to_string(), "3:8: m3");
262    }
263}