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