Skip to main content

vla_lang/
form.rs

1//! The datum: what the reader produces and every later stage walks.
2//!
3//! VLA.bas keeps a form as a `Variant`: a `String` for an atom, with a
4//! leading quote character marking a string literal, or a `Collection` for
5//! a list, whose source line is remembered beside it (`TagLine`/`FormLine`).
6//! Here that is one enum. A string literal holds its content, not the
7//! marker; [`Form::atom_text`] gives the VBA's own view back, the marker
8//! included, for the few places that read it verbatim.
9//!
10//! Equality is structural and ignores a list's line, as the VBA's own
11//! `DeepEqual` does: a line is where a form was read, not what it is.
12//!
13//! The helpers below are VLA.bas's datum helpers with their refusals: `Nth`,
14//! `NthList`, `SymText`, `HeadSym`, `ListHeadIs`, `StrLitContent`, each
15//! raising the same message id in the same situation.
16
17use crate::intrinsics::fold;
18use crate::messages::{raise, Refusal};
19
20/// One form: a symbol (a number is a symbol until someone asks), a string
21/// literal's content, or a list.
22#[derive(Clone, Debug)]
23pub enum Form {
24    Sym(String),
25    Str(String),
26    List(List),
27}
28
29/// A list, with the source line of its opening parenthesis: 0 when it was
30/// born in the prelude or made by a macro (S3.1's explicit zero).
31#[derive(Clone, Debug, Default)]
32pub struct List {
33    pub items: Vec<Form>,
34    pub line: u32,
35}
36
37impl PartialEq for Form {
38    fn eq(&self, other: &Form) -> bool {
39        match (self, other) {
40            (Form::Sym(a), Form::Sym(b)) => a == b,
41            (Form::Str(a), Form::Str(b)) => a == b,
42            (Form::List(a), Form::List(b)) => a == b,
43            _ => false,
44        }
45    }
46}
47
48impl PartialEq for List {
49    fn eq(&self, other: &List) -> bool {
50        self.items == other.items
51    }
52}
53
54impl Form {
55    pub fn sym(text: &str) -> Form {
56        Form::Sym(text.to_string())
57    }
58
59    pub fn string(content: &str) -> Form {
60        Form::Str(content.to_string())
61    }
62
63    /// A list with no line, as every list the macro system makes has none.
64    pub fn list(items: Vec<Form>) -> Form {
65        Form::List(List { items, line: 0 })
66    }
67
68    pub fn is_list(&self) -> bool {
69        matches!(self, Form::List(_))
70    }
71
72    /// VBA `IsSym`: an atom that is not a string literal.
73    pub fn is_sym(&self) -> bool {
74        matches!(self, Form::Sym(_))
75    }
76
77    /// VBA `IsKeywordArg`: a symbol that starts with `:` and is more than
78    /// the colon.
79    pub fn is_keyword_arg(&self) -> bool {
80        match self {
81            Form::Sym(s) => s.starts_with(':') && s.len() > 1,
82            _ => false,
83        }
84    }
85
86    /// The VBA's `CStr(v)` of an atom: a symbol's text, or a string
87    /// literal's content behind its quote marker. `None` for a list.
88    pub fn atom_text(&self) -> Option<String> {
89        match self {
90            Form::Sym(s) => Some(s.clone()),
91            Form::Str(s) => Some(format!("\"{s}")),
92            Form::List(_) => None,
93        }
94    }
95
96    pub fn as_list(&self) -> Option<&List> {
97        match self {
98            Form::List(l) => Some(l),
99            _ => None,
100        }
101    }
102
103    /// VBA `ListHeadIs`: a list whose first element is a symbol folding
104    /// equal to `name`.
105    pub fn head_is(&self, name: &str) -> bool {
106        match self {
107            Form::List(l) => l.head_is(name),
108            _ => false,
109        }
110    }
111}
112
113impl List {
114    pub fn new(items: Vec<Form>, line: u32) -> List {
115        List { items, line }
116    }
117
118    pub fn count(&self) -> usize {
119        self.items.len()
120    }
121
122    /// VBA `Nth`: element `i`, 1-based, refusing past the end.
123    pub fn nth(&self, i: usize) -> Result<&Form, Refusal> {
124        if i == 0 || i > self.items.len() {
125            return Err(raise("vla-form-missing-element", &[("n", &i.to_string())]));
126        }
127        Ok(&self.items[i - 1])
128    }
129
130    /// VBA `NthList`: element `i` as a list, refusing an atom there.
131    pub fn nth_list(&self, i: usize) -> Result<&List, Refusal> {
132        match self.nth(i)? {
133            Form::List(l) => Ok(l),
134            other => Err(raise(
135                "vla-expected-list-at-position",
136                &[
137                    ("n", &i.to_string()),
138                    ("value", &other.atom_text().unwrap_or_default()),
139                ],
140            )),
141        }
142    }
143
144    /// VBA `HeadSym`: the first element as a symbol's text.
145    pub fn head_sym(&self) -> Result<String, Refusal> {
146        if self.items.is_empty() {
147            return Err(raise("vla-empty-form", &[]));
148        }
149        sym_text(&self.items[0])
150    }
151
152    /// VBA `ListHeadIs` over this list.
153    pub fn head_is(&self, name: &str) -> bool {
154        match self.items.first() {
155            Some(Form::Sym(s)) => fold(s) == fold(name),
156            _ => false,
157        }
158    }
159
160    /// The list from its `i`th element on, 1-based: VBA's `VlaSlice` (cdr)
161    /// as a copy, since nothing on this path is long enough to share.
162    pub fn tail_from(&self, i: usize) -> List {
163        let start = i.saturating_sub(1).min(self.items.len());
164        List {
165            items: self.items[start..].to_vec(),
166            line: 0,
167        }
168    }
169}
170
171/// VBA `SymText`: the text of a symbol, refusing a list or a string.
172pub fn sym_text(f: &Form) -> Result<String, Refusal> {
173    match f {
174        Form::Sym(s) => Ok(s.clone()),
175        Form::Str(_) => Err(raise("vla-expected-symbol-got-string", &[])),
176        Form::List(_) => Err(raise("vla-expected-symbol-got-list", &[])),
177    }
178}
179
180/// VBA `StrLitContent`: the content of a string literal, refusing a list
181/// or a symbol.
182pub fn str_lit_content(f: &Form) -> Result<String, Refusal> {
183    match f {
184        Form::Str(s) => Ok(s.clone()),
185        Form::Sym(s) => Err(raise("vla-expected-string-got-other", &[("value", s)])),
186        Form::List(_) => Err(raise("vla-expected-string-got-list", &[])),
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn equality_is_structural_and_ignores_lines() {
196        let a = Form::List(List::new(vec![Form::sym("a"), Form::string("b")], 7));
197        let b = Form::List(List::new(vec![Form::sym("a"), Form::string("b")], 0));
198        assert_eq!(a, b);
199        assert_ne!(Form::sym("x"), Form::string("x"));
200        assert_ne!(Form::sym("x"), Form::list(vec![Form::sym("x")]));
201    }
202
203    #[test]
204    fn atom_text_is_the_vba_view() {
205        assert_eq!(Form::sym("x").atom_text().unwrap(), "x");
206        assert_eq!(Form::string("x").atom_text().unwrap(), "\"x");
207        assert!(Form::list(vec![]).atom_text().is_none());
208        assert!(Form::sym(":key").is_keyword_arg());
209        assert!(!Form::sym(":").is_keyword_arg());
210        assert!(!Form::string(":key").is_keyword_arg());
211    }
212
213    #[test]
214    fn helpers_refuse_with_the_catalogue_ids() {
215        let l = List::new(vec![Form::sym("f"), Form::string("s")], 1);
216        assert_eq!(l.head_sym().unwrap(), "f");
217        assert!(l.head_is("F"));
218        assert_eq!(l.nth(3).unwrap_err().id, "vla-form-missing-element");
219        assert_eq!(
220            l.nth(3).unwrap_err().text,
221            "form is missing required element 3"
222        );
223        assert_eq!(
224            l.nth_list(2).unwrap_err().id,
225            "vla-expected-list-at-position"
226        );
227        assert_eq!(
228            sym_text(&Form::string("s")).unwrap_err().id,
229            "vla-expected-symbol-got-string"
230        );
231        assert_eq!(
232            sym_text(&Form::list(vec![])).unwrap_err().id,
233            "vla-expected-symbol-got-list"
234        );
235        assert_eq!(
236            str_lit_content(&Form::sym("s")).unwrap_err().id,
237            "vla-expected-string-got-other"
238        );
239        assert_eq!(List::default().head_sym().unwrap_err().id, "vla-empty-form");
240        assert_eq!(l.tail_from(2).items, vec![Form::string("s")]);
241        assert_eq!(l.tail_from(5).items, vec![]);
242    }
243}