1use crate::copy::Libraries;
5use crate::messages::Message;
6use crate::{Error, Pos};
7use std::fmt::Write as _;
8use std::path::PathBuf;
9
10pub use rt::bms::{Attrb, Field, Initial, Intensity, Map, Mapset, Mode, Protection, Slot, extended_attributes, picture_size, slots};
11use rt::bms::EXTENDED;
12
13pub fn find_mapset(libraries: &Libraries, name: &str) -> Option<Result<Mapset, Error>> {
16 load(libraries, name, None).map(|(_, mapset)| mapset)
17}
18
19pub(crate) fn load(libraries: &Libraries, name: &str, library: Option<&str>) -> Option<(PathBuf, Result<Mapset, Error>)> {
20 let path = libraries.find_bms(name, library)?;
21 let mapset = std::fs::read(&path)
22 .map_err(|e| crate::messages::IWP0045.at(Pos::default(), e.to_string()))
23 .map(|bytes| crate::copy::decode(&bytes))
24 .and_then(|text| parse(&text))
25 .and_then(|mut sets| {
26 match sets.iter().position(|s| s.name.eq_ignore_ascii_case(name)) {
27 Some(i) => Ok(sets.swap_remove(i)),
28 None if sets.len() == 1 => Ok(sets.remove(0)),
29 None => Err(crate::messages::IWP0001.at(Pos::default(), format!("no mapset {} among the {} in the file", name.to_ascii_uppercase(), sets.len()))),
30 }
31 })
32 .map_err(|mut e| {
33 e.file = Some(path.display().to_string());
34 e
35 });
36 Some((path, mapset))
37}
38
39#[derive(Clone, Debug)]
40enum Val {
41 Word(String),
42 Quoted(String),
43 List(Vec<String>),
44}
45
46struct Statement {
47 line: u32,
48 label: Option<String>,
49 macro_name: String,
50 operands: Vec<(String, Val)>,
51}
52
53fn fail(line: u32, message: Message, text: impl Into<String>) -> Error {
54 message.at(Pos { file: 0, line, col: 1 }, text)
55}
56
57fn statements(text: &str) -> Result<Vec<Statement>, Error> {
59 let mut out = Vec::new();
60 let mut lines = text.lines().enumerate();
61 while let Some((n, raw)) = lines.next() {
62 let start = n as u32 + 1;
63 let line: Vec<char> = raw.chars().take(72).collect();
64 if line.first() == Some(&'*') || line.starts_with(&['.', '*']) || line.iter().all(|c| c.is_whitespace()) {
65 continue;
66 }
67 let mut joined = String::new();
68 let mut from = 0;
69 let mut line = line;
70 loop {
71 let continued = line.len() == 72 && line[71] != ' ';
72 let end = if continued { 71 } else { line.len() };
73 joined.extend(line.get(from..end).unwrap_or_default());
74 if !continued {
75 break;
76 }
77 let (_, next) = lines.next().ok_or_else(|| fail(start, crate::messages::IWP0009, "a continuation line is missing"))?;
78 line = next.chars().take(72).collect();
79 from = 15;
80 }
81 out.push(statement(&joined, start)?);
82 }
83 Ok(out)
84}
85
86fn statement(text: &str, line: u32) -> Result<Statement, Error> {
87 let token = |s: &str| s.split(char::is_whitespace).next().unwrap_or_default().len();
88 let (label, rest) = if text.starts_with(char::is_whitespace) {
89 (None, text.trim_start())
90 } else {
91 let n = token(text);
92 (Some(text[..n].to_ascii_uppercase()), text[n..].trim_start())
93 };
94 let n = token(rest);
95 let macro_name = rest[..n].to_ascii_uppercase();
96 let rest = rest[n..].trim_start();
97 if macro_name.is_empty() {
98 return Err(fail(line, crate::messages::IWP0010, "a label with no macro"));
99 }
100 Ok(Statement { line, label, macro_name, operands: operands(rest, line)? })
101}
102
103fn operands(text: &str, line: u32) -> Result<Vec<(String, Val)>, Error> {
106 let chars: Vec<char> = text.chars().collect();
107 let mut i = 0;
108 let mut out = Vec::new();
109 while i < chars.len() {
110 let key_start = i;
111 while i < chars.len() && !matches!(chars[i], '=' | ',') && !chars[i].is_whitespace() {
112 i += 1;
113 }
114 let key: String = chars[key_start..i].iter().collect::<String>().to_ascii_uppercase();
115 let mut value = Val::Word(String::new());
116 if chars.get(i) == Some(&'=') {
117 i += 1;
118 value = match chars.get(i) {
119 Some('\'') => {
120 let (s, next) = quoted(&chars, i, line)?;
121 i = next;
122 Val::Quoted(s)
123 }
124 Some('(') => {
125 let (items, next) = list(&chars, i, line)?;
126 i = next;
127 Val::List(items)
128 }
129 _ => {
130 let start = i;
131 let mut in_quote = false;
132 while i < chars.len() && (in_quote || !(chars[i] == ',' || chars[i].is_whitespace())) {
133 in_quote ^= chars[i] == '\'';
134 i += 1;
135 }
136 Val::Word(chars[start..i].iter().collect())
137 }
138 };
139 }
140 if !key.is_empty() {
141 out.push((key, value));
142 }
143 if chars.get(i) == Some(&',') {
144 i += 1;
145 while chars.get(i).is_some_and(|c| c.is_whitespace()) {
146 i += 1;
147 }
148 } else {
149 break;
150 }
151 }
152 Ok(out)
153}
154
155fn quoted(chars: &[char], at: usize, line: u32) -> Result<(String, usize), Error> {
157 let mut s = String::new();
158 let mut i = at + 1;
159 while i < chars.len() {
160 if chars[i] == '\'' {
161 if chars.get(i + 1) == Some(&'\'') {
162 s.push('\'');
163 i += 2;
164 continue;
165 }
166 return Ok((s, i + 1));
167 }
168 s.push(chars[i]);
169 i += 1;
170 }
171 Err(fail(line, crate::messages::IWP0011, "a quoted string is not closed"))
172}
173
174fn list(chars: &[char], at: usize, line: u32) -> Result<(Vec<String>, usize), Error> {
175 let mut items = Vec::new();
176 let mut current = String::new();
177 let mut i = at + 1;
178 while i < chars.len() {
179 match chars[i] {
180 '\'' => {
181 let (s, next) = quoted(chars, i, line)?;
182 current.push_str(&s);
183 i = next;
184 continue;
185 }
186 ')' => {
187 items.push(current.trim().to_owned());
188 return Ok((items.into_iter().filter(|s| !s.is_empty()).collect(), i + 1));
189 }
190 ',' => items.push(std::mem::take(&mut current).trim().to_owned()),
191 c => current.push(c),
192 }
193 i += 1;
194 }
195 Err(fail(line, crate::messages::IWP0012, "a parenthesised list is not closed"))
196}
197
198struct Operands<'a> {
199 ops: &'a [(String, Val)],
200 line: u32,
201}
202
203impl Operands<'_> {
204 fn get(&self, key: &str) -> Option<&Val> {
205 self.ops.iter().find(|(k, _)| k == key).map(|(_, v)| v)
206 }
207
208 fn words(&self, key: &str) -> Option<Vec<String>> {
209 self.get(key).map(|v| match v {
210 Val::List(items) => items.iter().map(|s| s.to_ascii_uppercase()).collect(),
211 Val::Word(w) | Val::Quoted(w) => vec![w.to_ascii_uppercase()],
212 })
213 }
214
215 fn word(&self, key: &str) -> Result<Option<String>, Error> {
216 match self.words(key) {
217 None => Ok(None),
218 Some(mut w) if w.len() == 1 => Ok(w.pop()),
219 Some(_) => Err(fail(self.line, crate::messages::IWP0013, format!("{key} takes one value"))),
220 }
221 }
222
223 fn text(&self, key: &str) -> Result<Option<String>, Error> {
224 match self.get(key) {
225 None => Ok(None),
226 Some(Val::Word(s) | Val::Quoted(s)) => Ok(Some(s.clone())),
227 Some(Val::List(_)) => Err(fail(self.line, crate::messages::IWP0014, format!("{key} takes a quoted string"))),
228 }
229 }
230
231 fn number(&self, key: &str, low: u32, high: u32) -> Result<Option<u32>, Error> {
232 let Some(w) = self.word(key)? else { return Ok(None) };
233 match w.parse::<u32>() {
234 Ok(n) if (low..=high).contains(&n) => Ok(Some(n)),
235 _ => Err(fail(self.line, crate::messages::IWP0015, format!("{key}={w} is not a number from {low} to {high}"))),
236 }
237 }
238
239 fn yes_no(&self, key: &str) -> Result<Option<bool>, Error> {
240 match self.word(key)?.as_deref() {
241 None => Ok(None),
242 Some("YES") => Ok(Some(true)),
243 Some("NO") => Ok(Some(false)),
244 Some(other) => Err(fail(self.line, crate::messages::IWP0016, format!("{key}={other}: YES or NO"))),
245 }
246 }
247
248 fn dsatts(&self) -> Result<Option<Vec<String>>, Error> {
250 if let Some(list) = self.words("DSATTS") {
251 let mut out: Vec<String> = Vec::new();
252 for a in list {
253 if !EXTENDED.iter().any(|(name, _, _)| *name == a) {
254 return Err(fail(self.line, crate::messages::IWP0017, format!("DSATTS={a} is not an extended attribute")));
255 }
256 if !out.contains(&a) {
257 out.push(a);
258 }
259 }
260 return Ok(Some(out));
261 }
262 Ok(match self.word("EXTATT")?.as_deref() {
263 None => None,
264 Some("YES") => Some(EXTENDED.iter().filter(|(_, _, implied)| *implied).map(|(a, _, _)| a.to_string()).collect()),
265 Some("NO" | "MAPONLY") => Some(Vec::new()),
266 Some(other) => return Err(fail(self.line, crate::messages::IWP0018, format!("EXTATT={other}: NO, MAPONLY or YES"))),
267 })
268 }
269}
270
271const MAX_NAME: usize = 30;
272
273struct OpenSet {
274 set: Mapset,
275 tioapfx: bool,
276 dsatts: Vec<String>,
277}
278
279pub fn parse(text: &str) -> Result<Vec<Mapset>, Error> {
281 let mut done = Vec::new();
282 let mut open: Option<OpenSet> = None;
283 for st in statements(text)? {
284 let ops = Operands { ops: &st.operands, line: st.line };
285 match st.macro_name.as_str() {
286 "PRINT" | "TITLE" | "EJECT" | "SPACE" | "END" => {}
287 "DFHMSD" => {
288 if ops.word("TYPE")?.as_deref() == Some("FINAL") {
289 done.push(open.take().ok_or_else(|| fail(st.line, crate::messages::IWP0019, "DFHMSD TYPE=FINAL with no mapset open"))?.set);
290 continue;
291 }
292 done.extend(open.take().map(|o| o.set));
293 open = Some(mapset_header(&st, &ops)?);
294 }
295 "DFHMDI" => {
296 let o = open.as_mut().ok_or_else(|| fail(st.line, crate::messages::IWP0020, "DFHMDI outside a DFHMSD"))?;
297 let map = map_header(&st, &ops, o)?;
298 o.set.maps.push(map);
299 }
300 "DFHMDF" => {
301 let map = open.as_mut().and_then(|o| o.set.maps.last_mut()).ok_or_else(|| fail(st.line, crate::messages::IWP0021, "DFHMDF outside a DFHMDI map"))?;
302 let field = field(&st, &ops, map)?;
303 map.fields.push(field);
304 }
305 other => return Err(fail(st.line, crate::messages::IWP0022, format!("unknown macro {other}"))),
306 }
307 }
308 done.extend(open.map(|o| o.set));
309 Ok(done)
310}
311
312fn name_of(st: &Statement, what: &str, max: usize) -> Result<String, Error> {
313 let name = st.label.clone().ok_or_else(|| fail(st.line, crate::messages::IWP0023, format!("{what} needs a name")))?;
314 if name.len() > max {
315 return Err(fail(st.line, crate::messages::IWP0024, format!("{what} name {name} is longer than {max} characters")));
316 }
317 Ok(name)
318}
319
320fn mapset_header(st: &Statement, ops: &Operands) -> Result<OpenSet, Error> {
321 let name = name_of(st, "DFHMSD", MAX_NAME)?;
322 if let Some(t) = ops.word("TYPE")?
324 && !matches!(t.as_str(), "DSECT" | "MAP" | "&SYSPARM" | "&&SYSPARM")
325 {
326 return Err(fail(st.line, crate::messages::IWP0025, format!("TYPE={t}: DSECT, MAP, FINAL or &SYSPARM")));
327 }
328 let mode = match ops.word("MODE")?.as_deref() {
329 None | Some("OUT") => Mode::Out,
330 Some("IN") => Mode::In,
331 Some("INOUT") => Mode::InOut,
332 Some(other) => return Err(fail(st.line, crate::messages::IWP0026, format!("MODE={other}: IN, OUT or INOUT"))),
333 };
334 let auto = ops.word("STORAGE")?.as_deref() == Some("AUTO");
335 let tioapfx = ops.yes_no("TIOAPFX")?.unwrap_or(auto);
336 Ok(OpenSet {
337 set: Mapset { name, mode, ctrl: ops.words("CTRL").unwrap_or_default(), maps: Vec::new() },
338 tioapfx,
339 dsatts: ops.dsatts()?.unwrap_or_default(),
340 })
341}
342
343fn map_header(st: &Statement, ops: &Operands, open: &OpenSet) -> Result<Map, Error> {
344 let name = name_of(st, "DFHMDI", 7)?;
345 let (lines, columns) = match ops.words("SIZE") {
346 None => (24, 80),
347 Some(v) => {
348 let dims: Vec<u16> = v.iter().map(|s| s.parse().ok().filter(|n| (1..=240).contains(n)).ok_or(())).collect::<Result<_, _>>().map_err(|()| fail(st.line, crate::messages::IWP0027, "SIZE=(lines,columns), each 1 to 240"))?;
349 match dims[..] {
350 [l, c] => (l, c),
351 _ => return Err(fail(st.line, crate::messages::IWP0027, "SIZE=(lines,columns), each 1 to 240")),
352 }
353 }
354 };
355 let origin = |key: &str| -> Result<u16, Error> {
356 match ops.word(key)?.as_deref() {
357 None | Some("NEXT" | "SAME") => Ok(1),
358 Some(w) => w.parse().ok().filter(|n| (1..=240).contains(n)).ok_or_else(|| fail(st.line, crate::messages::IWP0028, format!("{key}={w} is not a number from 1 to 240"))),
359 }
360 };
361 Ok(Map {
362 name,
363 lines,
364 columns,
365 line: origin("LINE")?,
366 column: origin("COLUMN")?,
367 ctrl: ops.words("CTRL").unwrap_or_else(|| open.set.ctrl.clone()),
368 tioapfx: ops.yes_no("TIOAPFX")?.unwrap_or(open.tioapfx),
369 dsatts: ops.dsatts()?.unwrap_or_else(|| open.dsatts.clone()),
370 fields: Vec::new(),
371 })
372}
373
374fn attrb(ops: &Operands) -> Result<Attrb, Error> {
375 let mut a = Attrb { protection: Protection::Askip, numeric: false, intensity: Intensity::Norm, detectable: false, cursor: false, fset: false };
376 let Some(words) = ops.words("ATTRB") else { return Ok(a) };
377 a.protection = Protection::Unprot;
378 for w in words {
379 match w.as_str() {
380 "ASKIP" => a.protection = Protection::Askip,
381 "PROT" => a.protection = Protection::Prot,
382 "UNPROT" => a.protection = Protection::Unprot,
383 "NUM" => a.numeric = true,
384 "BRT" => a.intensity = Intensity::Brt,
385 "NORM" => a.intensity = Intensity::Norm,
386 "DRK" => a.intensity = Intensity::Drk,
387 "DET" => a.detectable = true,
388 "IC" => a.cursor = true,
389 "FSET" => a.fset = true,
390 other => return Err(fail(ops.line, crate::messages::IWP0029, format!("ATTRB={other} is not an attribute"))),
391 }
392 }
393 Ok(a)
394}
395
396fn hex_bytes(s: &str, line: u32) -> Result<Vec<u8>, Error> {
397 if !s.len().is_multiple_of(2) || !s.is_ascii() {
398 return Err(fail(line, crate::messages::IWP0030, "XINIT takes an even number of hexadecimal digits"));
399 }
400 (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| fail(line, crate::messages::IWP0031, "XINIT takes hexadecimal digits"))).collect()
401}
402
403fn field(st: &Statement, ops: &Operands, map: &Map) -> Result<Field, Error> {
404 let name = st.label.as_ref().map(|_| name_of(st, "DFHMDF", MAX_NAME)).transpose()?;
405 let line = st.line;
406 let attrb = attrb(ops)?;
407 let occurs = ops.number("OCCURS", 1, u16::MAX.into())?.unwrap_or(1) as u16;
408 let group = ops.word("GRPNAME")?;
409 if group.is_some() && (ops.get("OCCURS").is_some() || name.is_none()) {
410 return Err(fail(line, crate::messages::IWP0032, "GRPNAME needs a labelled field and does not go with OCCURS"));
411 }
412 if group.as_ref().is_some_and(|g| g.len() > MAX_NAME) {
413 return Err(fail(line, crate::messages::IWP0033, "GRPNAME is longer than 30 characters"));
414 }
415 let initial = match (ops.text("INITIAL")?.or(ops.text("GINIT")?), ops.text("XINIT")?) {
416 (_, Some(x)) => Some(Initial::Bytes(hex_bytes(&x, line)?)),
417 (Some(t), None) => Some(Initial::Text(t)),
418 (None, None) => None,
419 };
420 let (picin, picout) = (ops.text("PICIN")?, ops.text("PICOUT")?);
421 let from_data = match &initial {
422 Some(Initial::Text(t)) => t.chars().count() as u32,
423 Some(Initial::Bytes(b)) => b.len() as u32,
424 None => 0,
425 };
426 let length = match ops.number("LENGTH", 0, 256)? {
427 Some(0) if name.is_some() => return Err(fail(line, crate::messages::IWP0034, "LENGTH=0 is allowed only on an unlabelled field, where it delimits an input field")),
428 Some(n) => n,
429 None => picin.iter().chain(&picout).map(|p| picture_size(p)).max().unwrap_or(from_data),
430 };
431 if length > 256 || (length == 0 && name.is_some()) {
432 return Err(fail(line, crate::messages::IWP0035, "LENGTH is missing, or is not from 1 to 256"));
433 }
434 let (line_no, column) = position(ops, map)?;
435 let justify = ops.words("JUSTIFY").unwrap_or_default();
436 for w in &justify {
437 if !matches!(w.as_str(), "LEFT" | "RIGHT" | "BLANK" | "ZERO") {
438 return Err(fail(line, crate::messages::IWP0036, format!("JUSTIFY={w}: LEFT or RIGHT, BLANK or ZERO")));
439 }
440 }
441 let has = |w: &str| justify.iter().any(|j| j == w);
442 let color = ops.word("COLOR")?;
443 if let Some(c) = &color
444 && !matches!(c.as_str(), "BLUE" | "RED" | "PINK" | "GREEN" | "TURQUOISE" | "YELLOW" | "NEUTRAL" | "DEFAULT")
445 {
446 return Err(fail(line, crate::messages::IWP0037, format!("COLOR={c} is not a colour")));
447 }
448 let hilight = ops.word("HILIGHT")?;
449 if let Some(h) = &hilight
450 && !matches!(h.as_str(), "OFF" | "BLINK" | "REVERSE" | "UNDERLINE")
451 {
452 return Err(fail(line, crate::messages::IWP0038, format!("HILIGHT={h} is not a highlight")));
453 }
454 Ok(Field {
455 name,
456 line: line_no,
457 column,
458 length: length as u16,
459 attrb,
460 initial,
461 picin,
462 picout,
463 occurs,
464 group,
465 justify_right: if has("RIGHT") || has("LEFT") { has("RIGHT") } else { attrb.numeric },
466 fill_zero: if has("ZERO") || has("BLANK") { has("ZERO") } else { attrb.numeric },
467 color,
468 hilight,
469 })
470}
471
472fn position(ops: &Operands, map: &Map) ->Result<(u16, u16), Error> {
474 let bad = || fail(ops.line, crate::messages::IWP0039, "POS is an offset within the map, or (line,column) inside it");
475 let (lines, columns) = (u32::from(map.lines), u32::from(map.columns));
476 let offset = match ops.words("POS") {
477 None => map.fields.last().map_or(0, |f| (u32::from(f.line) - 1) * columns + u32::from(f.column) - 1 + u32::from(f.occurs) * (u32::from(f.length) + 1)),
478 Some(v) => match v.iter().map(|s| s.parse::<u32>()).collect::<Result<Vec<_>, _>>().map_err(|_| bad())?[..] {
479 [n] => n,
480 [l, c] if (1..=lines).contains(&l) && (1..=columns).contains(&c) => (l - 1) * columns + c - 1,
481 _ => return Err(bad()),
482 },
483 };
484 if offset >= lines * columns {
485 return Err(bad());
486 }
487 Ok(((offset / columns + 1) as u16, (offset % columns + 1) as u16))
488}
489
490pub fn symbolic_map(mapset: &Mapset) -> String {
492 let mut out = String::new();
493 for map in &mapset.maps {
494 if mapset.mode != Mode::Out {
495 structure(&mut out, map, true, mapset.mode);
496 }
497 if mapset.mode != Mode::In {
498 structure(&mut out, map, false, mapset.mode);
499 }
500 }
501 out
502}
503
504fn entry(out: &mut String, level: usize, name: &str, clauses: &str) {
506 let indent = if level == 1 { 7 } else { 11 + 3 * (level - 2) };
507 let sep = if level == 1 { " " } else { " " };
508 let head = format!("{:indent$}{level:02}{sep}{name}", "");
509 let line = if clauses.is_empty() { format!("{head}.") } else { format!("{head} {clauses}.") };
510 if line.len() <= 72 || clauses.is_empty() {
511 let _ = writeln!(out, "{line}");
512 } else {
513 let _ = writeln!(out, "{head}\n{:width$}{clauses}.", "", width = indent + 4);
514 }
515}
516
517fn structure(out: &mut String, map: &Map, input: bool, mode: Mode) {
518 let side = if input { 'I' } else { 'O' };
519 if input || mode == Mode::Out {
520 entry(out, 1, &format!("{}{side}", map.name), "");
521 } else {
522 entry(out, 1, &format!("{}O", map.name), &format!("REDEFINES {}I", map.name));
523 }
524 if map.tioapfx {
525 entry(out, 2, "FILLER", "PIC X(12)");
526 }
527 let mut group: Option<&str> = None;
528 for f in map.fields.iter().filter(|f| f.name.is_some()) {
529 let (level, lead) = match &f.group {
530 Some(g) => {
531 let first = group != Some(g.as_str());
532 if first {
533 entry(out, 2, g, "");
534 }
535 group = Some(g);
536 (3, first)
537 }
538 None => {
539 group = None;
540 if f.occurs > 1 {
541 let name = format!("{}{}", f.name.as_deref().unwrap_or_default(), if input { 'D' } else { 'G' });
542 entry(out, 2, &name, &format!("OCCURS {}", f.occurs));
543 (3, true)
544 } else {
545 (2, true)
546 }
547 }
548 };
549 if input {
550 input_items(out, map, f, level, lead);
551 } else {
552 output_items(out, map, f, level, lead, mode);
553 }
554 }
555}
556
557fn input_items(out: &mut String, map: &Map, f: &Field, level: usize, lead: bool) {
558 let n = f.name.as_deref().unwrap_or_default();
559 if lead {
560 entry(out, level, &format!("{n}L"), "COMP PIC S9(4)");
561 entry(out, level, &format!("{n}F"), "PICTURE X");
562 entry(out, level, "FILLER", &format!("REDEFINES {n}F"));
563 entry(out, level + 1, &format!("{n}A"), "PICTURE X");
564 let k = extended_attributes(map).len();
565 if k > 0 {
566 entry(out, level, "FILLER", &format!("PICTURE X({k})"));
567 }
568 }
569 let pic = f.picin.clone().unwrap_or_else(|| format!("X({})", f.length));
570 entry(out, level, &format!("{n}I"), &format!("PIC {pic}"));
571}
572
573fn output_items(out: &mut String, map: &Map, f: &Field, level: usize, lead: bool, mode: Mode) {
574 let n = f.name.as_deref().unwrap_or_default();
575 if lead {
576 if mode == Mode::InOut {
577 entry(out, level, "FILLER", "PICTURE X(3)");
578 } else {
579 entry(out, level, "FILLER", "PICTURE X(2)");
580 entry(out, level, &format!("{n}A"), "PICTURE X");
581 }
582 for (a, s, _) in EXTENDED {
583 if map.dsatts.iter().any(|d| d == a) {
584 entry(out, level, &format!("{n}{s}"), "PICTURE X");
585 }
586 }
587 }
588 let pic = f.picout.clone().unwrap_or_else(|| format!("X({})", f.length));
589 entry(out, level, &format!("{n}O"), &format!("PIC {pic}"));
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
597 fn slots_match_the_symbolic_map() {
598 let sets = parse(&realistic()).unwrap();
599 let map = &sets[0].maps[0];
600 let slots = slots(map, true);
601 let k = extended_attributes(map).len();
602 let first = slots[0];
603 assert_eq!(first.length_at(), Some(if map.tioapfx { 12 } else { 0 }));
604 assert_eq!(first.data, first.length_at().unwrap() + 3 + k);
605 for pair in slots.windows(2) {
606 let (a, b) = (pair[0], pair[1]);
607 let gap = if b.control.is_some() { 3 + k } else { 0 };
608 assert_eq!(b.data, a.data + a.size + gap);
609 }
610 }
611
612 fn cont(line: &str) -> String {
614 format!("{line:<71}X\n")
615 }
616
617 fn realistic() -> String {
618 let mut s = String::new();
619 s.push_str("* Demo mapset\n PRINT NOGEN\n");
620 s.push_str(&cont("DEMOSET DFHMSD TYPE=&SYSPARM,MODE=INOUT,LANG=COBOL,STORAGE=AUTO,"));
621 s.push_str(" CTRL=(FREEKB,FRSET),DSATTS=(COLOR,HILIGHT) SEQ00010\n");
622 s.push_str("DEMO DFHMDI SIZE=(24,80),LINE=1,COLUMN=1\n");
623 s.push_str(&cont(" DFHMDF POS=(1,30),LENGTH=15,ATTRB=(ASKIP,BRT),"));
624 s.push_str(" INITIAL='ACCOUNT ENQUIRY'\n");
625 s.push_str(&cont("NAME DFHMDF POS=(3,10),LENGTH=20,ATTRB=(UNPROT,IC,FSET),COLOR=RED,"));
626 s.push_str(" HILIGHT=UNDERLINE,INITIAL='O''NEIL'\n");
627 s.push_str(&cont("AMT DFHMDF POS=165,ATTRB=(NUM,BRT),PICIN='9(5)V99',"));
628 s.push_str(" PICOUT='ZZ,ZZ9.99',JUSTIFY=(RIGHT,ZERO)\n");
629 s.push_str("ROW DFHMDF POS=(6,2),LENGTH=8,OCCURS=3\n");
630 s.push_str("ADDR1 DFHMDF POS=(10,2),LENGTH=20,GRPNAME=ADDR,ATTRB=(UNPROT)\n");
631 s.push_str("ADDR2 DFHMDF POS=(11,2),LENGTH=20,GRPNAME=ADDR\n");
632 s.push_str(" DFHMSD TYPE=FINAL\n END\n");
633 s
634 }
635
636 #[test]
637 fn a_realistic_mapset_parses() {
638 let sets = parse(&realistic()).unwrap();
639 assert_eq!(sets.len(), 1);
640 let set = &sets[0];
641 assert_eq!((set.name.as_str(), set.mode), ("DEMOSET", Mode::InOut));
642 assert_eq!(set.ctrl, ["FREEKB", "FRSET"]);
643 let map = &set.maps[0];
644 assert_eq!((map.name.as_str(), map.lines, map.columns), ("DEMO", 24, 80));
645 assert!(map.tioapfx);
646 assert_eq!(map.dsatts, ["COLOR", "HILIGHT"]);
647 assert_eq!(map.fields.len(), 6);
648
649 let title = &map.fields[0];
650 assert_eq!((title.name.clone(), title.line, title.column, title.length), (None, 1, 30, 15));
651 assert_eq!(title.initial, Some(Initial::Text("ACCOUNT ENQUIRY".into())));
652 assert_eq!((title.attrb.protection, title.attrb.intensity), (Protection::Askip, Intensity::Brt));
653
654 let name = &map.fields[1];
655 assert_eq!(name.name.as_deref(), Some("NAME"));
656 assert_eq!((name.line, name.column), (3, 10));
657 assert!(name.attrb.cursor && name.attrb.fset && name.attrb.protection == Protection::Unprot);
658 assert_eq!((name.color.as_deref(), name.hilight.as_deref()), (Some("RED"), Some("UNDERLINE")));
659 assert_eq!(name.initial, Some(Initial::Text("O'NEIL".into())));
660 assert!(!name.justify_right && !name.fill_zero);
661
662 let amt = &map.fields[2];
663 assert_eq!((amt.line, amt.column), (3, 6));
664 assert_eq!((amt.picin.as_deref(), amt.picout.as_deref()), (Some("9(5)V99"), Some("ZZ,ZZ9.99")));
665 assert_eq!(amt.length, 9);
666 assert!(amt.attrb.numeric && amt.justify_right && amt.fill_zero);
667
668 assert_eq!(map.fields[3].occurs, 3);
669 assert_eq!(map.fields[4].group.as_deref(), Some("ADDR"));
670 assert_eq!(map.fields[5].group.as_deref(), Some("ADDR"));
671 }
672
673 #[test]
674 fn zero_length_is_for_an_unlabelled_field_only() {
675 let src = |label: &str| format!("M DFHMSD TYPE=MAP\nM1 DFHMDI SIZE=(24,80)\n{label:<9}DFHMDF POS=(6,33),LENGTH=0,ATTRB=ASKIP\n");
676 let map = &parse(&src("")).unwrap()[0].maps[0];
677 assert_eq!((map.fields[0].name.clone(), map.fields[0].length), (None, 0));
678 let err = parse(&src("AMT")).unwrap_err();
679 assert!(err.to_string().contains("LENGTH=0 is allowed only on an unlabelled field"), "{err}");
680 }
681
682 #[test]
683 fn a_quoted_string_continues_on_the_next_line() {
684 let head = " DFHMDF POS=1,LENGTH=20,INITIAL='";
685 let fill = "A".repeat(71 - head.len());
686 let src = format!("M DFHMSD TYPE=MAP\nM1 DFHMDI\n{}{}", cont(&format!("{head}{fill}")), " BC'\n");
687 let map = &parse(&src).unwrap()[0].maps[0];
688 assert_eq!(map.fields[0].initial, Some(Initial::Text(format!("{fill}BC"))));
689 }
690
691 #[test]
692 fn xinit_gives_bytes_and_several_mapsets_are_kept() {
693 let src = "S1 DFHMSD TYPE=MAP\nM1 DFHMDI\n DFHMDF POS=1,LENGTH=2,XINIT=C1C2\n DFHMSD TYPE=FINAL\nS2 DFHMSD TYPE=MAP,MODE=IN\n DFHMSD TYPE=FINAL\n";
694 let sets = parse(src).unwrap();
695 assert_eq!(sets.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["S1", "S2"]);
696 assert_eq!(sets[0].maps[0].fields[0].initial, Some(Initial::Bytes(vec![0xC1, 0xC2])));
697 assert_eq!(sets[1].mode, Mode::In);
698 assert_eq!(sets[0].mode, Mode::Out);
699 }
700
701 fn two_field_map() -> Mapset {
702 let src = "MAPS DFHMSD TYPE=MAP,MODE=INOUT,STORAGE=AUTO,DSATTS=(COLOR)\nMAP1 DFHMDI SIZE=(24,80)\nNAME DFHMDF POS=(1,1),LENGTH=10\nAMT DFHMDF POS=(2,1),PICIN='9(5)',PICOUT='ZZZZ9'\n DFHMSD TYPE=FINAL\n";
703 parse(src).unwrap().remove(0)
704 }
705
706 #[test]
707 fn the_symbolic_map_of_a_two_field_map() {
708 let expected = "\n 01 MAP1I.
709 02 FILLER PIC X(12).
710 02 NAMEL COMP PIC S9(4).
711 02 NAMEF PICTURE X.
712 02 FILLER REDEFINES NAMEF.
713 03 NAMEA PICTURE X.
714 02 FILLER PICTURE X(1).
715 02 NAMEI PIC X(10).
716 02 AMTL COMP PIC S9(4).
717 02 AMTF PICTURE X.
718 02 FILLER REDEFINES AMTF.
719 03 AMTA PICTURE X.
720 02 FILLER PICTURE X(1).
721 02 AMTI PIC 9(5).
722 01 MAP1O REDEFINES MAP1I.
723 02 FILLER PIC X(12).
724 02 FILLER PICTURE X(3).
725 02 NAMEC PICTURE X.
726 02 NAMEO PIC X(10).
727 02 FILLER PICTURE X(3).
728 02 AMTC PICTURE X.
729 02 AMTO PIC ZZZZ9.
730";
731 assert_eq!(symbolic_map(&two_field_map()).lines().collect::<Vec<_>>(), expected[1..].lines().collect::<Vec<_>>());
732 }
733
734 #[test]
735 fn output_only_occurs_and_groups() {
736 let src = "MAPS DFHMSD TYPE=MAP,TIOAPFX=NO\nM1 DFHMDI\nROW DFHMDF POS=1,LENGTH=4,OCCURS=2\nA1 DFHMDF POS=100,LENGTH=3,GRPNAME=G\nA2 DFHMDF POS=120,LENGTH=5,GRPNAME=G\n DFHMSD TYPE=FINAL\n";
737 let mapset = parse(src).unwrap().remove(0);
738 let expected = "\n 01 M1O.
739 02 ROWG OCCURS 2.
740 03 FILLER PICTURE X(2).
741 03 ROWA PICTURE X.
742 03 ROWO PIC X(4).
743 02 G.
744 03 FILLER PICTURE X(2).
745 03 A1A PICTURE X.
746 03 A1O PIC X(3).
747 03 A2O PIC X(5).
748";
749 assert_eq!(symbolic_map(&mapset), &expected[1..]);
750 let input = symbolic_map(&Mapset { mode: Mode::In, ..mapset });
751 assert!(input.contains(" 02 ROWD OCCURS 2.\n 03 ROWL COMP PIC S9(4).\n"), "{input}");
752 assert!(input.contains(" 02 G.\n 03 A1L COMP PIC S9(4).") && input.contains(" 03 A2I PIC X(5).\n"), "{input}");
753 assert!(!input.contains("A2L") && input.lines().all(|l| l.len() <= 72));
754 }
755
756 fn library(name: &str, files: &[(&str, String)]) -> Libraries {
757 let dir = std::env::temp_dir().join(format!("ironwork-bms-{}-{name}", std::process::id()));
758 std::fs::create_dir_all(&dir).unwrap();
759 for (file, text) in files {
760 std::fs::write(dir.join(file), text).unwrap();
761 }
762 Libraries::new(vec![dir])
763 }
764
765 fn offsets(program: &crate::ast::Program, record: &str) -> Vec<(String, u32)> {
767 let mut at = 0;
768 let mut inside = false;
769 let mut parent = 0;
770 let mut out: Vec<(String, u32)> = Vec::new();
771 for e in &program.working_storage {
772 match e.level {
773 1 => inside = e.name.as_deref() == Some(record),
774 2 if inside => {
775 let size = if e.usage.is_some() { 2 } else { e.picture.as_deref().map_or(0, picture_size) };
776 let name = e.name.clone().unwrap_or_default();
777 match &e.redefines {
778 Some(target) => {
779 parent = out.iter().find(|(n, _)| n == target).unwrap().1;
780 out.push((name, parent));
781 }
782 None => {
783 parent = at;
784 out.push((name, at));
785 at += size;
786 }
787 }
788 }
789 3 if inside => out.push((e.name.clone().unwrap_or_default(), parent)),
790 _ => {}
791 }
792 }
793 out
794 }
795
796 #[test]
797 fn a_program_copies_a_mapset_and_gets_its_layout() {
798 let bms = "DEMOMAP DFHMSD TYPE=&SYSPARM,MODE=INOUT,STORAGE=AUTO\nM1 DFHMDI SIZE=(24,80)\nNAME DFHMDF POS=(1,1),LENGTH=10\n DFHMSD TYPE=FINAL\n".to_owned();
799 let libs = library("layout", &[("DEMOMAP.bms", bms)]);
800 let program = " IDENTIFICATION DIVISION.\n PROGRAM-ID. P.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n COPY DEMOMAP.\n PROCEDURE DIVISION.\n GOBACK.\n";
801 let p = crate::parse_with(program, &libs).unwrap();
802 let at = |n: &str| offsets(&p, "M1I").into_iter().find(|(name, _)| name == n).unwrap().1;
803 assert_eq!((at("NAMEL"), at("NAMEF"), at("NAMEA"), at("NAMEI")), (12, 14, 14, 15));
804 assert!(p.working_storage.iter().any(|e| e.name.as_deref() == Some("M1O") && e.redefines.as_deref() == Some("M1I")));
805 assert_eq!(find_mapset(&libs, "DEMOMAP").unwrap().unwrap().maps[0].fields[0].name.as_deref(), Some("NAME"));
806 assert!(find_mapset(&libs, "NOPE").is_none());
807 }
808
809 #[test]
810 fn copy_of_a_broken_mapset_names_the_file() {
811 let libs = library("broken", &[("BAD.bms", "BAD DFHMSD TYPE=MAP\n FROBNICATE X=1\n".to_owned())]);
812 let err = crate::parse_with(" COPY BAD.\n", &libs).unwrap_err();
813 assert!(err.message.contains("unknown macro FROBNICATE") && err.message.contains("BAD.bms"), "{err}");
814 }
815
816 #[test]
817 fn dfhaid_has_values() {
818 let src = " IDENTIFICATION DIVISION.\n PROGRAM-ID. P.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n COPY DFHAID.\n COPY DFHBMSCA.\n PROCEDURE DIVISION.\n GOBACK.\n";
819 let p = crate::parse_with(src, &Libraries::default()).unwrap();
820 let entry = |n: &str| p.working_storage.iter().find(|e| e.name.as_deref() == Some(n)).unwrap();
821 assert_eq!(entry("DFHENTER").value, Some(crate::ast::Literal::Hex(vec![0x7D])));
822 assert_eq!(entry("DFHPF24").value, Some(crate::ast::Literal::Hex(vec![0x4C])));
823 assert_eq!(entry("DFHBMASK").value, Some(crate::ast::Literal::Hex(vec![0xF0])));
824 let erase = entry("DFHERASE");
825 assert_eq!((erase.level, erase.condition_values.len()), (88, 2));
826 }
827
828 #[test]
829 fn errors_point_at_the_line() {
830 let e = |src: &str| parse(src).unwrap_err();
831 assert!(e("S DFHMSD TYPE=MAP\n WIBBLE\n").message.contains("unknown macro WIBBLE"));
832 let outside = e("* c\nF DFHMDF POS=1,LENGTH=1\n");
833 assert_eq!(outside.pos.line, 2);
834 assert!(outside.message.contains("outside"));
835 let bad = e("S DFHMSD TYPE=MAP\nM DFHMDI SIZE=(2,10)\nF DFHMDF POS=(3,1),LENGTH=1\n");
836 assert_eq!(bad.pos.line, 3);
837 assert!(bad.message.contains("POS"));
838 assert!(e("S DFHMSD TYPE=MAP\nM DFHMDI\nF DFHMDF POS=ABC,LENGTH=1\n").message.contains("POS"));
839 assert!(e("S DFHMSD TYPE=MAP\nTOOLONGNAME DFHMDI\n").message.contains("longer than 7"));
840 assert!(e("S DFHMSD TYPE=MAP\nM DFHMDI\nF DFHMDF POS=1,LENGTH=1,OCCURS=2,GRPNAME=G\n").message.contains("GRPNAME"));
841 }
842}