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