Skip to main content

vla_lang/
headtable.rs

1//! The head table: `VLA_HeadTable.bas`'s catalogue of the core forms, read
2//! as data (PORT.5, slice 5c).
3//!
4//! One row per head symbol: its aliases, arity, interpreter routine, VBA
5//! routine, its standing in the formula dialect, and whether it is
6//! export-only. The VBA is the source; `tools/export_headtable.ps1` writes
7//! `core/data/headtable.vla` from it and `tools/check_data_exports.ps1` fails
8//! when the two drift. As in the VBA, the one live column is the aliases:
9//! [`alias_map`] feeds [`resolve_head_alias`], the lookup `EmitTop`,
10//! `EmitStmt`, `EmitExpr` and `EmitFormula` run before their dispatch, so an
11//! aliased head (`fijar!` for `set!`) reaches the arm its canonical spelling
12//! would. The other columns are the catalogue they are in the VBA, not the
13//! dispatch.
14
15use std::collections::HashMap;
16use std::sync::OnceLock;
17
18use crate::form::Form;
19use crate::intrinsics::fold;
20use crate::reader::read_forms;
21
22/// The formula column's three states, as the VBA's honesty note names them.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum FormulaSupport {
25    Yes,
26    Refused,
27    Undeclared,
28}
29
30/// One row of the table.
31#[derive(Clone, Debug, PartialEq)]
32pub struct HeadRow {
33    pub symbol: String,
34    pub aliases: Vec<String>,
35    pub arity: String,
36    pub interp_routine: String,
37    pub vba_routine: String,
38    pub formula: FormulaSupport,
39    pub export_only: bool,
40}
41
42const TABLE_TEXT: &str = include_str!("../data/headtable.vla");
43
44fn parse_rows() -> Vec<HeadRow> {
45    let text = TABLE_TEXT.replace("\r\n", "\n");
46    let forms = match read_forms(&text) {
47        Ok(forms) => forms,
48        Err(_) => return Vec::new(),
49    };
50    let mut rows = Vec::with_capacity(forms.len());
51    for form in &forms {
52        let Form::List(l) = form else { continue };
53        if !l.head_is("head") || l.items.len() != 8 {
54            continue;
55        }
56        let (
57            Form::Sym(symbol),
58            Form::List(aliases),
59            Form::Str(arity),
60            Form::Str(interp),
61            Form::Str(vba),
62            Form::Sym(formula),
63            Form::Sym(export_only),
64        ) = (
65            &l.items[1],
66            &l.items[2],
67            &l.items[3],
68            &l.items[4],
69            &l.items[5],
70            &l.items[6],
71            &l.items[7],
72        )
73        else {
74            continue;
75        };
76        let formula = match formula.as_str() {
77            "yes" => FormulaSupport::Yes,
78            "refused" => FormulaSupport::Refused,
79            "undeclared" => FormulaSupport::Undeclared,
80            _ => continue,
81        };
82        let aliases = aliases
83            .items
84            .iter()
85            .filter_map(|a| match a {
86                Form::Sym(s) => Some(s.clone()),
87                _ => None,
88            })
89            .collect();
90        rows.push(HeadRow {
91            symbol: symbol.clone(),
92            aliases,
93            arity: arity.clone(),
94            interp_routine: interp.clone(),
95            vba_routine: vba.clone(),
96            formula,
97            export_only: export_only == "true",
98        });
99    }
100    rows
101}
102
103/// VBA `VlaHeadTableRows`: every row, in the table's order.
104pub fn rows() -> &'static [HeadRow] {
105    static ROWS: OnceLock<Vec<HeadRow>> = OnceLock::new();
106    ROWS.get_or_init(parse_rows)
107}
108
109/// VBA `VlaHeadTableCount`.
110pub fn count() -> usize {
111    rows().len()
112}
113
114/// VBA `VlaHeadTableRow`: a row by its symbol or by any of its aliases,
115/// compared folded.
116pub fn row(symbol: &str) -> Option<&'static HeadRow> {
117    let key = fold(symbol);
118    rows().iter().find(|r| fold(&r.symbol) == key).or_else(|| {
119        rows()
120            .iter()
121            .find(|r| r.aliases.iter().any(|a| fold(a) == key))
122    })
123}
124
125/// VBA `VlaHeadTableAliasMap`: each alias, folded, to its canonical symbol
126/// as written. Built once; the VBA rebuilds it per compile, to the same
127/// map.
128pub fn alias_map() -> &'static HashMap<String, String> {
129    static MAP: OnceLock<HashMap<String, String>> = OnceLock::new();
130    MAP.get_or_init(|| {
131        let mut m = HashMap::new();
132        for r in rows() {
133            for a in &r.aliases {
134                m.insert(fold(a), r.symbol.clone());
135            }
136        }
137        m
138    })
139}
140
141/// VBA `ResolveHeadAlias`: a folded head to its canonical spelling, or
142/// itself when it is not an alias.
143pub fn resolve_head_alias(folded_head: &str) -> String {
144    match alias_map().get(folded_head) {
145        Some(canon) => canon.clone(),
146        None => folded_head.to_string(),
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn the_table_is_read_whole() {
156        // 65 rows at IN5.0; the same floor is held on the file by
157        // tools/check_data_exports.ps1.
158        assert_eq!(count(), 65);
159        assert_eq!(rows()[0].symbol, "sub");
160        assert_eq!(rows()[0].vba_routine, "EmitProc");
161        assert_eq!(rows()[64].symbol, "not");
162        let yes = rows()
163            .iter()
164            .filter(|r| r.formula == FormulaSupport::Yes)
165            .count();
166        let refused = rows()
167            .iter()
168            .filter(|r| r.formula == FormulaSupport::Refused)
169            .count();
170        let undeclared = rows()
171            .iter()
172            .filter(|r| r.formula == FormulaSupport::Undeclared)
173            .count();
174        assert_eq!((yes, refused, undeclared), (18, 9, 38));
175    }
176
177    #[test]
178    fn export_only_is_exactly_raw_deflambda_lambda() {
179        // TestHeadTableExportOnly's pin (VLA_Tests.bas), held here too.
180        let set: Vec<&str> = rows()
181            .iter()
182            .filter(|r| r.export_only)
183            .map(|r| r.symbol.as_str())
184            .collect();
185        assert_eq!(set, vec!["raw", "deflambda", "lambda"]);
186    }
187
188    #[test]
189    fn the_three_aliases_resolve_and_nothing_else_does() {
190        assert_eq!(resolve_head_alias("fijar!"), "set!");
191        assert_eq!(resolve_head_alias("si"), "if");
192        assert_eq!(resolve_head_alias("depurar"), "debug-print");
193        assert_eq!(resolve_head_alias("set!"), "set!");
194        assert_eq!(resolve_head_alias("unknown"), "unknown");
195        assert_eq!(alias_map().len(), 3);
196        assert_eq!(row("FIJAR!").unwrap().symbol, "set!");
197        assert_eq!(row("Mod").unwrap().arity, "2 (a, b)");
198        assert!(row("nope").is_none());
199    }
200}