Skip to main content

inside_baseball/
config.rs

1use std::{
2    collections::{hash_map::Entry, HashMap},
3    error::Error,
4    fmt,
5    mem,
6};
7
8#[derive(Default)]
9pub struct Config {
10    pub global_names: Vec<Option<String>>,
11    pub global_types: Vec<Option<Type>>,
12    pub scripts: Vec<Script>,
13    pub rooms: Vec<Room>,
14    pub enums: Vec<Enum>,
15    pub enum_names: HashMap<String, EnumId>,
16    pub assocs: Vec<Assoc>,
17    pub assoc_names: HashMap<String, AssocId>,
18    pub assocs_pending_parse: Vec<String>, // temporary
19    pub suppress_preamble: bool,
20    pub aside: bool,
21}
22
23#[derive(Default)]
24pub struct Room {
25    pub vars: Vec<Var>,
26    pub scripts: Vec<Script>,
27}
28
29#[derive(Default)]
30pub struct Script {
31    pub name: Option<String>,
32    pub params: Option<u16>,
33    pub locals: Vec<Var>,
34    pub skip_do_blocks: bool,
35}
36
37#[derive(Default)]
38pub struct Var {
39    pub name: Option<String>,
40    pub ty: Option<Type>,
41}
42
43pub struct Enum {
44    pub name: String,
45    pub values: HashMap<i32, String>,
46}
47
48pub struct Assoc {
49    pub types: Vec<Type>,
50}
51
52pub type EnumId = usize;
53pub type AssocId = usize;
54
55pub enum Type {
56    Any,
57    Char,
58    Enum(EnumId),
59    Script,
60    Array {
61        item: Box<Type>,
62        y: Box<Type>,
63        x: Box<Type>,
64    },
65    AssocArray {
66        assoc: AssocId,
67        y: Box<Type>,
68        x: Box<Type>,
69    },
70}
71
72impl Config {
73    pub fn from_ini(ini: &str) -> Result<Self, Box<dyn Error>> {
74        let mut result = Self {
75            global_names: Vec::with_capacity(1024),
76            global_types: Vec::with_capacity(1024),
77            scripts: Vec::with_capacity(512),
78            rooms: Vec::with_capacity(64),
79            enums: Vec::with_capacity(64),
80            enum_names: HashMap::with_capacity(64),
81            assocs: Vec::with_capacity(64),
82            assoc_names: HashMap::with_capacity(64),
83            assocs_pending_parse: Vec::with_capacity(64),
84            suppress_preamble: false,
85            aside: false,
86        };
87        for (i, line) in ini.lines().enumerate() {
88            let cx = &ParseContext::LineIndex(i);
89            let line = line.split_once(';').map_or(line, |(a, _)| a); // Trim comments
90            let line = line.trim();
91            if line.is_empty() {
92                continue;
93            }
94            let (lhs, rhs) = line.split_once('=').ok_or_else(|| parse_err(cx))?;
95            let key = lhs.trim();
96            let value = rhs.trim();
97            let mut dots = key.split('.');
98            match dots.next() {
99                Some("enum") => {
100                    handle_enum_key(cx, &mut dots, value, &mut result)?;
101                }
102                Some("assoc") => {
103                    handle_assoc_key(&mut dots, value, &mut result, cx)?;
104                }
105                Some("global") => {
106                    let id = it_final(&mut dots, cx)?;
107                    let id: usize = id.parse().map_err(|_| parse_err(cx))?;
108                    let (name, type_) = parse_var_name_type(value, &result, cx)?;
109                    extend(&mut result.global_names, id);
110                    result.global_names[id] = Some(name.to_string());
111                    extend(&mut result.global_types, id);
112                    result.global_types[id] = type_;
113                }
114                Some("script") => {
115                    handle_script_key(cx, &mut dots, value, &mut result, |c| &mut c.scripts)?;
116                }
117                Some("room") => {
118                    let room = it_next(&mut dots, cx)?;
119                    let room: usize = room.parse().map_err(|_| parse_err(cx))?;
120                    extend(&mut result.rooms, room);
121                    match it_next(&mut dots, cx)? {
122                        "var" => {
123                            let var = it_final(&mut dots, cx)?;
124                            let var: usize = var.parse().map_err(|_| parse_err(cx))?;
125                            let (name, ty) = parse_var_name_type(value, &result, cx)?;
126                            extend(&mut result.rooms[room].vars, var);
127                            result.rooms[room].vars[var].name = Some(name.to_string());
128                            result.rooms[room].vars[var].ty = ty;
129                        }
130                        "script" => {
131                            handle_script_key(cx, &mut dots, value, &mut result, |c| {
132                                &mut c.rooms[room].scripts
133                            })?;
134                        }
135                        _ => {
136                            return Err(parse_err(cx));
137                        }
138                    }
139                }
140                _ => {
141                    return Err(parse_err(cx));
142                }
143            }
144        }
145
146        for comma_sep in mem::take(&mut result.assocs_pending_parse) {
147            let cx = &ParseContext::NearString(&comma_sep);
148            let types: Result<Vec<_>, _> = comma_sep
149                .split(',')
150                .map(|s| parse_type_or_empty_any(s.trim(), &result, cx))
151                .collect();
152            let types = types?;
153            result.assocs.push(Assoc { types });
154        }
155
156        check_conflicting_names(&result)?;
157
158        Ok(result)
159    }
160}
161
162fn handle_script_key<'a>(
163    cx: &ParseContext,
164    dots: &mut impl Iterator<Item = &'a str>,
165    mut value: &str,
166    config: &mut Config,
167    scripts_vec: impl Fn(&mut Config) -> &mut Vec<Script>,
168) -> Result<(), Box<dyn Error>> {
169    let script = it_next(dots, cx)?;
170    let script: usize = script.parse().map_err(|_| parse_err(cx))?;
171    // XXX: this wastes a bunch of memory since local scripts start at 2048
172    let scripts = scripts_vec(config);
173    extend(scripts, script);
174    match dots.next() {
175        None => {
176            // parse param count as in `func(2)`
177            if let Some(paren) = value.find('(') {
178                if *value.as_bytes().last().unwrap() != b')' {
179                    return Err(parse_err(cx));
180                }
181                let params = &value[paren + 1..value.len() - 1];
182                let params: u16 = params.parse().map_err(|_| parse_err(cx))?;
183                scripts[script].params = Some(params);
184                value = &value[..paren];
185            }
186            scripts[script].name = Some(value.to_string());
187        }
188        Some("local") => {
189            let local = it_final(dots, cx)?;
190            let local: usize = local.parse().map_err(|_| parse_err(cx))?;
191            let (name, ty) = parse_var_name_type(value, config, cx)?;
192            let scripts = scripts_vec(config);
193            extend(&mut scripts[script].locals, local);
194            scripts[script].locals[local].name = Some(name.to_string());
195            scripts[script].locals[local].ty = ty;
196        }
197        Some("disable_do_blocks") => {
198            it_end(dots, cx)?;
199            if value != "all" {
200                return Err(parse_err(cx));
201            }
202            scripts[script].skip_do_blocks = true;
203        }
204        Some(_) => {
205            return Err(parse_err(cx));
206        }
207    }
208    Ok(())
209}
210
211fn handle_enum_key<'a>(
212    cx: &ParseContext,
213    dots: &mut impl Iterator<Item = &'a str>,
214    value: &str,
215    config: &mut Config,
216) -> Result<(), Box<dyn Error>> {
217    let enum_name = it_next(dots, cx)?;
218    let const_value: i32 = it_final(dots, cx)?.parse().map_err(|_| parse_err(cx))?;
219    let const_name = value.to_string();
220
221    let enum_id = config
222        .enum_names
223        .get(enum_name)
224        .copied()
225        .unwrap_or_else(|| {
226            let id = config.enums.len();
227            config.enums.push(Enum {
228                name: enum_name.to_string(),
229                values: HashMap::new(),
230            });
231            config.enum_names.insert(enum_name.to_string(), id);
232            id
233        });
234    config.enums[enum_id].values.insert(const_value, const_name);
235    Ok(())
236}
237
238fn handle_assoc_key<'a>(
239    dots: &mut impl Iterator<Item = &'a str>,
240    value: &str,
241    config: &mut Config,
242    cx: &ParseContext,
243) -> Result<(), Box<dyn Error>> {
244    let name = it_next(dots, cx)?;
245    let id = config.assocs_pending_parse.len();
246    config.assocs_pending_parse.push(value.to_string());
247    config.assoc_names.insert(name.to_string(), id);
248    Ok(())
249}
250
251fn parse_type(s: &str, config: &Config, cx: &ParseContext) -> Result<Type, Box<dyn Error>> {
252    if s == "string" {
253        return Ok(Type::Array {
254            item: Box::new(Type::Char),
255            y: Box::new(Type::Any),
256            x: Box::new(Type::Any),
257        });
258    }
259    if s == "char" {
260        return Ok(Type::Char);
261    }
262    if s == "script" {
263        return Ok(Type::Script);
264    }
265    if let Some((item, y, x)) = parse_array(s) {
266        let y = Box::new(parse_type_or_empty_any(y.unwrap_or(""), config, cx)?);
267        let x = Box::new(parse_type_or_empty_any(x, config, cx)?);
268
269        if let Some(&assoc) = config.assoc_names.get(item) {
270            return Ok(Type::AssocArray { assoc, y, x });
271        }
272
273        let item = Box::new(parse_type_or_empty_any(item, config, cx)?);
274        return Ok(Type::Array { item, y, x });
275    }
276    if let Some(&enum_id) = config.enum_names.get(s) {
277        return Ok(Type::Enum(enum_id));
278    }
279    return Err(type_err(cx));
280}
281
282fn parse_type_or_empty_any(
283    s: &str,
284    config: &Config,
285    cx: &ParseContext,
286) -> Result<Type, Box<dyn Error>> {
287    if s.is_empty() {
288        return Ok(Type::Any);
289    }
290    parse_type(s, config, cx)
291}
292
293fn parse_array(s: &str) -> Option<(&str, Option<&str>, &str)> {
294    let (s, x) = parse_array_level(s)?;
295    match parse_array_level(s) {
296        None => Some((s, None, x)),
297        Some((s, y)) => Some((s, Some(y), x)),
298    }
299}
300
301fn parse_array_level(s: &str) -> Option<(&str, &str)> {
302    // This is how we write parsers down south
303    if !s.ends_with(']') {
304        return None;
305    }
306    let s = &s[..s.len() - 1];
307    let (o, i) = s.rsplit_once('[')?;
308    Some((o.trim_end(), i.trim()))
309}
310
311fn it_next<T>(it: &mut impl Iterator<Item = T>, cx: &ParseContext) -> Result<T, Box<dyn Error>> {
312    it.next().ok_or_else(|| parse_err(cx))
313}
314
315fn it_end<T>(it: &mut impl Iterator<Item = T>, cx: &ParseContext) -> Result<(), Box<dyn Error>> {
316    match it.next() {
317        Some(_) => return Err(parse_err(cx)),
318        None => Ok(()),
319    }
320}
321
322fn it_final<T>(it: &mut impl Iterator<Item = T>, cx: &ParseContext) -> Result<T, Box<dyn Error>> {
323    let result = it_next(it, cx);
324    it_end(it, cx)?;
325    result
326}
327
328fn extend<T: Default>(xs: &mut Vec<T>, upto: usize) {
329    if xs.len() < upto + 1 {
330        xs.resize_with(upto + 1, T::default);
331    }
332}
333
334enum ParseContext<'a> {
335    LineIndex(usize),
336    NearString(&'a str),
337}
338
339impl fmt::Display for ParseContext<'_> {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        match self {
342            Self::LineIndex(index) => {
343                let num = index + 1;
344                write!(f, "on line {num}")?;
345            }
346            Self::NearString(string) => {
347                write!(f, "near {string:?}")?;
348            }
349        }
350        Ok(())
351    }
352}
353
354fn parse_err(cx: &ParseContext) -> Box<dyn Error> {
355    format!("bad config {cx}").into()
356}
357
358fn type_err(cx: &ParseContext) -> Box<dyn Error> {
359    format!("bad type {cx}").into()
360}
361
362fn parse_var_name_type<'a>(
363    s: &'a str,
364    config: &Config,
365    cx: &ParseContext,
366) -> Result<(&'a str, Option<Type>), Box<dyn Error>> {
367    match s.split_once(':') {
368        None => Ok((s, None)),
369        Some((name, ty)) => {
370            let name = name.trim_end();
371            let ty = ty.trim_start();
372            let ty = parse_type(ty, config, cx)?;
373            Ok((name, Some(ty)))
374        }
375    }
376}
377
378fn check_conflicting_names(config: &Config) -> Result<(), Box<dyn Error>> {
379    let mut parents = Vec::new();
380    let mut global_seen = HashMap::with_capacity(1 << 10);
381    let mut room_seen = HashMap::with_capacity(64);
382    let mut script_seen = HashMap::with_capacity(16);
383
384    for (i, s) in config.scripts.iter().enumerate() {
385        if let Some(name) = &s.name {
386            check_dup(
387                &parents,
388                &mut global_seen,
389                name,
390                Some(i.try_into().unwrap()),
391            )?;
392        }
393    }
394
395    for name in config.global_names.iter().flatten() {
396        check_dup(&parents, &mut global_seen, name, None)?;
397    }
398
399    for enum_ in &config.enums {
400        for (&value, name) in &enum_.values {
401            check_dup(&parents, &mut global_seen, name, Some(value))?;
402        }
403    }
404
405    for script in &config.scripts {
406        script_seen.clear();
407        for var in &script.locals {
408            if let Some(name) = &var.name {
409                check_dup(&parents, &mut script_seen, name, None)?;
410            }
411        }
412    }
413
414    parents.push(&global_seen);
415
416    for room in &config.rooms {
417        room_seen.clear();
418
419        for var in &room.vars {
420            if let Some(name) = &var.name {
421                check_dup(&parents, &mut room_seen, name, None)?;
422            }
423        }
424
425        // Safety: this is popped at the end of the scope before room_seen is used
426        // again.
427        parents.push(unsafe { transmute_lifetime(&room_seen) });
428
429        for script in &room.scripts {
430            script_seen.clear();
431            for var in &script.locals {
432                if let Some(name) = &var.name {
433                    check_dup(&parents, &mut script_seen, name, None)?;
434                }
435            }
436        }
437
438        parents.pop();
439    }
440
441    Ok(())
442}
443
444unsafe fn transmute_lifetime<'a, T>(x: &T) -> &'a T {
445    mem::transmute::<&T, &T>(x)
446}
447
448fn check_dup<'a>(
449    parents: &[&HashMap<&str, Option<i32>>],
450    seen: &mut HashMap<&'a str, Option<i32>>,
451    name: &'a str,
452    value: Option<i32>,
453) -> Result<(), Box<dyn Error>> {
454    for parent in parents {
455        if parent.contains_key(name) {
456            return Err(format!("conflicting name {name:?}").into());
457        }
458    }
459    match seen.entry(name) {
460        Entry::Vacant(e) => {
461            e.insert(value);
462        }
463        Entry::Occupied(e) => {
464            let equal = e.get().is_some() && *e.get() == value;
465            if !equal {
466                return Err(format!("conflicting name {name:?}").into());
467            }
468        }
469    }
470    Ok(())
471}