Skip to main content

ironwork_syntax/
dli.rs

1//! EXEC DLI commands as the IMS translator reads them (IMS Application Programming: EXEC DLI
2//! Commands for CICS and IMS, SC18-7811-04, chapters 4-6): each command, the longer spellings it
3//! has, and the options it takes.
4
5pub struct Command {
6    pub name: &'static str,
7    pub aliases: &'static [&'static str],
8    /// `None` where the book leaves the options to IMS's Operations Guide: GMSG, ICMD and RCMD.
9    pub options: Option<&'static [&'static str]>,
10}
11
12const GET_UNIQUE: &[&str] = &[
13    "PCB", "AIB", "KEYFEEDBACK", "FEEDBACKLEN", "INTO", "VARIABLE", "LAST", "SEGMENT", "SEGLENGTH", "OFFSET", "LOCKED", "LOCKCLASS", "MOVENEXT", "GETFIRST", "SET",
14    "SETCOND", "SETZERO", "SETPARENT", "WHERE", "FIELDLENGTH", "KEYS", "KEYLENGTH",
15];
16
17const GET_NEXT: &[&str] = &[
18    "PCB", "AIB", "KEYFEEDBACK", "FEEDBACKLEN", "INTO", "VARIABLE", "FIRST", "LAST", "CURRENT", "SEGMENT", "SEGLENGTH", "OFFSET", "LOCKED", "LOCKCLASS", "MOVENEXT",
19    "GETFIRST", "SET", "SETCOND", "SETZERO", "SETPARENT", "WHERE", "FIELDLENGTH", "KEYS", "KEYLENGTH",
20];
21
22/// From the book's Format diagrams (pp. 35-81). GHU, GHN and GHNP take their Get command's
23/// options (p. 102); every command that takes PCB may take AIB instead (p. 5).
24pub const COMMANDS: &[Command] = &[
25    Command { name: "GU", aliases: &["GET UNIQUE"], options: Some(GET_UNIQUE) },
26    Command { name: "GHU", aliases: &[], options: Some(GET_UNIQUE) },
27    Command { name: "GN", aliases: &["GET NEXT"], options: Some(GET_NEXT) },
28    Command { name: "GHN", aliases: &[], options: Some(GET_NEXT) },
29    Command { name: "GNP", aliases: &["GET NEXT IN PARENT"], options: Some(GET_NEXT) },
30    Command { name: "GHNP", aliases: &[], options: Some(GET_NEXT) },
31    Command { name: "ISRT", aliases: &["INSERT"], options: Some(&["PCB", "AIB", "VARIABLE", "FIRST", "LAST", "CURRENT", "SEGMENT", "SEGLENGTH", "FROM", "OFFSET", "MOVENEXT", "GETFIRST", "SET", "SETCOND", "SETZERO", "WHERE", "FIELDLENGTH", "KEYS", "KEYLENGTH"]) },
32    Command { name: "DLET", aliases: &["DELETE"], options: Some(&["PCB", "AIB", "VARIABLE", "SEGMENT", "SEGLENGTH", "FROM", "SETZERO"]) },
33    Command { name: "REPL", aliases: &["REPLACE"], options: Some(&["PCB", "AIB", "VARIABLE", "SEGMENT", "SEGLENGTH", "OFFSET", "FROM", "MOVENEXT", "SET", "SETCOND", "SETZERO"]) },
34    Command { name: "POS", aliases: &["POSITION"], options: Some(&["PCB", "AIB", "INTO", "KEYFEEDBACK", "FEEDBACKLEN", "SEGMENT", "WHERE", "FIELDLENGTH"]) },
35    Command { name: "RETRIEVE", aliases: &[], options: Some(&["PCB", "AIB", "KEYFEEDBACK", "FEEDBACKLEN"]) },
36    Command { name: "LOAD", aliases: &[], options: Some(&["PCB", "AIB", "VARIABLE", "SEGMENT", "SEGLENGTH", "FROM"]) },
37    Command { name: "SCHD", aliases: &["SCHEDULE"], options: Some(&["PSB", "SYSSERVE", "NODHABEND"]) },
38    Command { name: "TERM", aliases: &["TERMINATE"], options: Some(&[]) },
39    Command { name: "ACCEPT", aliases: &[], options: Some(&["STATUSGROUP", "AIB"]) },
40    Command { name: "CHKP", aliases: &["CHECKPOINT"], options: Some(&["ID", "AIB"]) },
41    Command { name: "DEQ", aliases: &[], options: Some(&["LOCKCLASS", "AIB"]) },
42    Command { name: "LOG", aliases: &[], options: Some(&["FROM", "LENGTH", "AIB"]) },
43    Command { name: "QUERY", aliases: &[], options: Some(&["PCB", "AIB"]) },
44    Command { name: "REFRESH", aliases: &[], options: Some(&["DBQUERY", "AIB"]) },
45    Command { name: "ROLB", aliases: &[], options: Some(&[]) },
46    Command { name: "ROLL", aliases: &[], options: Some(&[]) },
47    Command { name: "ROLS", aliases: &[], options: Some(&["PCB", "TOKEN", "AREA", "AIB"]) },
48    Command { name: "SETS", aliases: &[], options: Some(&["TOKEN", "AREA", "AIB"]) },
49    Command { name: "SETU", aliases: &[], options: Some(&["TOKEN", "AREA"]) },
50    Command { name: "STAT", aliases: &["STATISTICS"], options: Some(&["PCB", "INTO", "LENGTH", "VSAM", "NONVSAM", "FORMATTED", "UNFORMATTED", "SUMMARY", "AIB"]) },
51    Command { name: "SYMCHKP", aliases: &[], options: Some(&["ID", "AREA1", "AREA2", "AREA3", "AREA4", "AREA5", "AREA6", "AREA7", "LENGTH1", "LENGTH2", "LENGTH3", "LENGTH4", "LENGTH5", "LENGTH6", "LENGTH7"]) },
52    Command { name: "XRST", aliases: &[], options: Some(&["MAXLENGTH", "ID", "AREA1", "AREA2", "AREA3", "AREA4", "AREA5", "AREA6", "AREA7", "LENGTH1", "LENGTH2", "LENGTH3", "LENGTH4", "LENGTH5", "LENGTH6", "LENGTH7"]) },
53    Command { name: "GMSG", aliases: &[], options: None },
54    Command { name: "ICMD", aliases: &[], options: None },
55    Command { name: "RCMD", aliases: &[], options: None },
56];
57
58/// Options whose argument is a name rather than data: in double parentheses it is an area that
59/// holds the name.
60pub const NAMED: &[&str] = &["SEGMENT", "PSB"];
61
62/// The command a block's words begin with, longest spelling first, and how many words it takes.
63pub fn command(words: &[&str]) -> Option<(&'static Command, usize)> {
64    let upper: Vec<String> = words.iter().map(|w| w.to_ascii_uppercase()).collect();
65    let spelled = |s: &str| {
66        let n = s.split(' ').count();
67        (upper.len() >= n && upper[..n].join(" ") == s).then_some(n)
68    };
69    COMMANDS
70        .iter()
71        .flat_map(|c| c.aliases.iter().chain([&c.name]).filter_map(move |s| Some((c, spelled(s)?))))
72        .max_by_key(|&(_, n)| n)
73}
74
75pub fn find(name: &str) -> Option<&'static Command> {
76    COMMANDS.iter().find(|c| c.name == name)
77}
78
79const RELATIONS: &[&str] = &[">=", "<=", "¬=", "=>", "=<", "=", ">", "<", "EQ", "NE", "GT", "GE", "LT", "LE"];
80const CONNECTORS: &[&str] = &["AND", "OR", "&", "|", "*", "+"];
81
82/// A WHERE argument's comparisons, each a segment field, a relational operator and the value's
83/// text: a data name or a literal; `Err` names what is wrong.
84pub fn qualification(text: &str) -> Result<Vec<(String, String, String)>, String> {
85    let mut out = Vec::new();
86    let mut rest = text.trim();
87    while !rest.is_empty() {
88        let field_end = rest.find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '#' | '@' | '$'))).unwrap_or(rest.len());
89        let field = &rest[..field_end];
90        if field.is_empty() {
91            return Err(format!("a segment field name, not {rest}"));
92        }
93        rest = rest[field_end..].trim_start();
94        let relation = RELATIONS
95            .iter()
96            .find(|r| rest.get(..r.len()).is_some_and(|p| p.eq_ignore_ascii_case(r)) && (r.chars().all(|c| !c.is_ascii_alphabetic()) || rest[r.len()..].starts_with(' ')))
97            .ok_or_else(|| format!("a relational operator after {field}"))?;
98        rest = rest[relation.len()..].trim_start();
99        let (value, after) = value(rest);
100        if value.is_empty() {
101            return Err(format!("a value after {field} {relation}"));
102        }
103        out.push((field.to_ascii_uppercase(), relation.to_ascii_uppercase(), value.to_owned()));
104        rest = after.trim_start();
105        if let Some(c) = CONNECTORS.iter().find(|c| rest.get(..c.len()).is_some_and(|p| p.eq_ignore_ascii_case(c)) && rest[c.len()..].starts_with([' ', '\t'])) {
106            rest = rest[c.len()..].trim_start();
107        }
108    }
109    Ok(out)
110}
111
112/// A value at the start of `text`, a quoted literal whole, and what follows it.
113fn value(text: &str) -> (&str, &str) {
114    if let Some(q @ ('\'' | '"')) = text.chars().next() {
115        let mut end = 1;
116        let bytes = text.as_bytes();
117        while end < bytes.len() {
118            if bytes[end] == q as u8 {
119                if bytes.get(end + 1) == Some(&(q as u8)) {
120                    end += 2;
121                    continue;
122                }
123                return (&text[..=end], &text[end + 1..]);
124            }
125            end += 1;
126        }
127        return (text, "");
128    }
129    let end = text.find(char::is_whitespace).unwrap_or(text.len());
130    (&text[..end], &text[end..])
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn long_spellings_name_their_command() {
139        let words = |s: &'static str| s.split_whitespace().collect::<Vec<_>>();
140        assert_eq!(command(&words("GET NEXT IN PARENT USING PCB(1)")).map(|(c, n)| (c.name, n)), Some(("GNP", 4)));
141        assert_eq!(command(&words("gn using pcb(1)")).map(|(c, n)| (c.name, n)), Some(("GN", 1)));
142        assert!(command(&words("FETCH X")).is_none());
143    }
144
145    #[test]
146    fn a_qualification_is_comparisons_joined_by_and_or_or() {
147        assert_eq!(qualification("ACCNTID = PA-ACCT-ID").unwrap(), [("ACCNTID".into(), "=".into(), "PA-ACCT-ID".into())]);
148        let q = qualification("KEYA > SEGKEY1 AND KEYA < 'A 350' OR KEYA>=X").unwrap();
149        assert_eq!(q.iter().map(|(f, r, v)| format!("{f}{r}{v}")).collect::<Vec<_>>(), ["KEYA>SEGKEY1", "KEYA<'A 350'", "KEYA>=X"]);
150        assert!(qualification("= X").is_err());
151        assert!(qualification("KEYA X").is_err());
152    }
153}