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 feedback;
11pub mod jni;
12pub mod lexer;
13pub mod parser;
14pub mod report;
15pub mod source;
16pub mod sql;
17pub mod system;
18
19use std::fmt;
20
21pub use rt::vocab::Pos;
22
23/// The severity of a compiler message, IBM's five levels in order (Programming Guide SC27-8714-03,
24/// Table 38, p. 282).
25#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum Severity {
27    /// I: the program runs correctly.
28    Informational,
29    /// W: a possible error; the program probably runs correctly.
30    Warning,
31    /// E: an error IBM's compiler corrects, though the program may not run as expected.
32    Error,
33    /// S: an error the compiler cannot correct; the program should not be run.
34    Severe,
35    /// U: the compilation ended.
36    Unrecoverable,
37}
38
39impl Severity {
40    /// The return code a compilation ends with when this is its most severe message.
41    pub const fn return_code(self) -> u8 {
42        match self {
43            Self::Informational => 0,
44            Self::Warning => 4,
45            Self::Error => 8,
46            Self::Severe => 12,
47            Self::Unrecoverable => 16,
48        }
49    }
50
51    /// The word a message's line carries after its position; an error's carries none.
52    pub const fn label(self) -> Option<&'static str> {
53        match self {
54            Self::Informational => Some("informational"),
55            Self::Warning => Some("warning"),
56            Self::Error | Self::Severe | Self::Unrecoverable => None,
57        }
58    }
59}
60
61/// A compiler message; [`Error::at`] makes a severe one.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct Error {
64    pub pos: Pos,
65    pub message: String,
66    /// The COPY member the position is in, when it is not the program itself.
67    pub file: Option<String>,
68    pub severity: Severity,
69}
70
71impl Error {
72    pub fn at(pos: Pos, message: impl Into<String>) -> Self {
73        Self { pos, message: message.into(), file: None, severity: Severity::Severe }
74    }
75
76    pub fn warning(pos: Pos, message: impl Into<String>) -> Self {
77        Self::at(pos, message).graded(Severity::Warning)
78    }
79
80    pub fn graded(mut self, severity: Severity) -> Self {
81        self.severity = severity;
82        self
83    }
84
85    /// Names the member a position in a COPY member came from.
86    pub fn in_files(mut self, files: &[String]) -> Self {
87        self.file = files.get(self.pos.file as usize).filter(|f| !f.is_empty()).cloned();
88        self
89    }
90
91    /// `file:line:col: message`, with `main` naming the program itself; a warning or informational
92    /// message puts its label before the message: `file:line:col: warning: message`.
93    pub fn place(&self, main: &str) -> String {
94        match (&self.file, self.pos.line) {
95            (Some(f), _) => format!("{f}:{self}"),
96            (None, 0) => format!("{main}: {}", self.labelled()),
97            (None, _) => format!("{main}:{self}"),
98        }
99    }
100
101    /// The message, after its severity's label when it has one.
102    pub fn labelled(&self) -> String {
103        match self.severity.label() {
104            Some(label) => format!("{label}: {}", self.message),
105            None => self.message.clone(),
106        }
107    }
108}
109
110impl fmt::Display for Error {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "{}: {}", self.pos, self.labelled())
113    }
114}
115
116/// The return code of a compilation that gave these messages: the highest of theirs, 0 for none.
117pub fn return_code(messages: &[Error]) -> u8 {
118    messages.iter().map(|m| m.severity.return_code()).max().unwrap_or(0)
119}
120
121/// The message a refusal names: the first of the most severe.
122pub fn most_severe(messages: &[Error]) -> Option<&Error> {
123    messages.iter().rev().max_by_key(|m| m.severity)
124}
125
126impl std::error::Error for Error {}
127
128/// The first program in the source.
129pub fn parse(text: &str) -> Result<ast::Program, Error> {
130    parse_with(text, &copy::Libraries::default())
131}
132
133/// The first program in the source, with COPY members resolved from `libraries`.
134pub fn parse_with(text: &str, libraries: &copy::Libraries) -> Result<ast::Program, Error> {
135    Ok(parse_all_with(text, libraries)?.remove(0))
136}
137
138/// Every program in the source, in order, nested programs after the one that contains them.
139pub fn parse_all_with(text: &str, libraries: &copy::Libraries) -> Result<Vec<ast::Program>, Error> {
140    let mut files = vec![String::new()];
141    let mut source = source::read(text).and_then(|s| copy::expand(s, libraries, &mut files)).and_then(copy::replace).map_err(|e| e.in_files(&files))?;
142    let mut tokens = lexer::lex(&source).map_err(|e| e.in_files(&files))?;
143    if debugging::requested(&tokens) {
144        files.truncate(1);
145        source = source::read_file_debugging(text, 0).and_then(|s| copy::expand(s, libraries, &mut files)).and_then(copy::replace).map_err(|e| e.in_files(&files))?;
146        let lexed = lexer::lex(&source).map_err(|e| e.in_files(&files))?;
147        tokens = debugging::keep(lexed, source.debugging.as_deref().unwrap_or_default());
148    }
149    dbcs_literal(&tokens, &source.options).map_err(|e| e.in_files(&files))?;
150    let mut programs = parser::parse(&tokens, source.options).map_err(|e| e.in_files(&files))?;
151    for p in &mut programs {
152        p.sources = files.clone();
153    }
154    Ok(programs)
155}
156
157/// Under NSYMBOL(DBCS) an N literal is a DBCS literal (Programming Guide SC27-8714-03, p. 388),
158/// which ironwork cannot hold (assumption [`numeric::assumptions::NSYMBOL_DBCS`]).
159fn dbcs_literal(tokens: &[lexer::Token], cards: &[String]) -> Result<(), Error> {
160    let mut options = numeric::Options::default();
161    for card in cards {
162        options.apply(card).ok();
163    }
164    match tokens.iter().find(|t| matches!(t.tok, lexer::Tok::National(_))) {
165        Some(t) if options.nsymbol == numeric::Nsymbol::Dbcs => Err(Error::at(t.pos, "an N literal is a DBCS literal under NSYMBOL(DBCS), and ironwork has no DBCS data")),
166        _ => Ok(()),
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    const ALL: [Severity; 5] = [Severity::Informational, Severity::Warning, Severity::Error, Severity::Severe, Severity::Unrecoverable];
175
176    fn message(severity: Severity, line: u32) -> Error {
177        Error::at(Pos { file: 0, line, col: 8 }, format!("m{line}")).graded(severity)
178    }
179
180    #[test]
181    fn severities_rise_with_ibms_return_codes() {
182        assert_eq!(ALL.map(Severity::return_code), [0, 4, 8, 12, 16]);
183        assert!(ALL.windows(2).all(|w| w[0] < w[1]));
184        assert_eq!(Error::at(Pos::default(), "m").severity, Severity::Severe);
185        assert_eq!(Error::warning(Pos::default(), "m").severity, Severity::Warning);
186    }
187
188    #[test]
189    fn a_compilation_returns_the_highest_code_of_its_messages() {
190        let codes = |severities: &[Severity]| return_code(&severities.iter().enumerate().map(|(i, &s)| message(s, i as u32 + 1)).collect::<Vec<_>>());
191        use Severity::*;
192        assert_eq!(codes(&[]), 0);
193        assert_eq!(codes(&[Informational, Informational]), 0);
194        assert_eq!(codes(&[Informational, Warning]), 4);
195        assert_eq!(codes(&[Warning, Error, Informational]), 8);
196        assert_eq!(codes(&[Error, Severe, Warning]), 12);
197        assert_eq!(codes(&[Informational, Warning, Error, Severe]), 12);
198        assert_eq!(codes(&[Severe, Unrecoverable]), 16);
199    }
200
201    #[test]
202    fn the_message_a_refusal_names_is_the_first_of_the_most_severe() {
203        let messages = [message(Severity::Warning, 1), message(Severity::Severe, 2), message(Severity::Error, 3), message(Severity::Severe, 4)];
204        assert_eq!(most_severe(&messages).map(|m| m.pos.line), Some(2));
205        assert!(most_severe(&[]).is_none());
206    }
207
208    #[test]
209    fn an_error_line_carries_no_label_and_a_warning_or_informational_one_says_which_it_is() {
210        let lines: Vec<String> = ALL.map(|s| message(s, 7).place("p.cbl")).to_vec();
211        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"]);
212        let unplaced = |s: Severity| Error::at(Pos::default(), "m").graded(s).place("p.cbl");
213        assert_eq!((unplaced(Severity::Severe), unplaced(Severity::Warning)), ("p.cbl: m".to_string(), "p.cbl: warning: m".to_string()));
214        let in_member = Error::warning(Pos { file: 1, line: 3, col: 8 }, "m3").in_files(&[String::new(), "COPYA".into()]);
215        assert_eq!(in_member.place("p.cbl"), "COPYA:3:8: warning: m3");
216        assert_eq!(message(Severity::Severe, 3).to_string(), "3:8: m3");
217    }
218}