Skip to main content

vla_lang/
formula_syntax.rs

1//! The formula syntax (VLA.4, 2026-10-17): a VLA expression written as a
2//! formula's text, and a formula's text read back as a VLA expression.
3//!
4//! [`write`] takes an expression the expander has finished with, `(if (> A1
5//! 3) "big" "small")`, and answers the formula's text in Excel's invariant
6//! spelling, `=IF(A1>3,"big","small")`: the spelling a file and the view
7//! record hold (ISO/IEC 29500-1, 18.17), English function names, `,`
8//! between arguments, `.` as the decimal mark, A1 references, and none of
9//! the `_xlfn.` prefixes a file writer adds (`sheet::strip_future_prefixes`
10//! is the reading side of that rule). [`read`] is its inverse, for tools and
11//! tests: formula text, with or without its `=`, to the canonical
12//! expression. The expander's `formula-expand` is the writer at expand time
13//! (`expand.rs`), so that a cartridge's source composes a formula from
14//! expressions and its macros, where a template macro could only fuse a
15//! flat one from texts (Alonzo's SPEC 7.8; KIT.1's "a macro's call is no
16//! argument of `&expand`").
17//!
18//! **Canonical, both ways.** The text is written with no space outside a
19//! text, the fewest parentheses Excel's precedence allows, every function
20//! name, truth value, error and column letter upper-case, and a sheet quoted
21//! only when `refers::quote_sheet` says it must be; a number and a text are
22//! written as they stand. The expression is read with lower-case heads, a
23//! left chain of one operator as one form of many operands, a sign on a
24//! number as the signed number, and a reference as the bare symbol it can
25//! be (`Input!B5`) or, where a symbol cannot hold it, `(ref "Q1 Data"
26//! "A1")`. So `read(write(v))` is `v` in that canonical form, and
27//! `write(read(f))` is `f` in the canonical text, byte for byte: the laws
28//! the tests hold, over every formula of the corpus as well as the cases.
29//! What is canonicalized away is what Excel computes the same: spaces,
30//! redundant parentheses, `sum(` for `SUM(`, `'Input'!B5` for `Input!B5`.
31//! What equivalence means here is the language's own reading: the
32//! evaluator's parser (`calc::formula::parse`) reads the text written and
33//! the text read as one tree. Nothing is claimed of Excel's values, of a
34//! function's existence or of its arity, which are evaluation's.
35//!
36//! **Precedence**, Excel's as `calc::formula` reads it, lowest first: the
37//! comparisons; `&`; `+` and `-`; `*` and `/`; `^`; a sign; `%`; every
38//! binary operator left-associative, so `2^3^2` is 64. A left operand is
39//! parenthesized when it binds looser than its operator, a right one when
40//! it binds looser or equally; a sign's operand when it is any binary
41//! operation, since a sign binds tighter than `^` (`(- (^ x 2))` is
42//! `-(x^2)`, and `(^ (- x) 2)` is `-x^2`, Excel's own trap); `%`'s when it
43//! is a sign or a binary operation. A form of many operands is the left
44//! fold VLA's own operators are (`EmitChain`, `EvalOpChain`): `(- a b c)`
45//! is `a-b-c`, and `(< a b c)` is `a<b<c`, which compares `a<b` with `c`.
46//!
47//! **The atoms.** A number is the formula's numeral, `5`, `1.50`, `.5`,
48//! `1e3`, as written, a `-` before it the canonical sign; VBA's own
49//! numerals (`&HFF`, `1d5`, `+5`) are refused, since a formula does not
50//! read them. A text is a text literal, `""` for a quote mark inside it,
51//! at most 255 characters, Excel's limit. `true` and `false` are `TRUE` and
52//! `FALSE`; an error is `(error "#N/A")`, as the rows spell a value, the
53//! seven a formula holds. A symbol the reference scanner reads whole as one
54//! reference or one name is that reference or that name, its column letters
55//! upper-cased and its sheet quoted by the one quoting rule; any other
56//! symbol is refused, never renamed (`tax-rate` would read as `tax` minus
57//! `rate`). Inside the syntax a string is always a text: a formula's text
58//! kept raw is the string a row holds outside it.
59//!
60//! **The heads.** The twelve operators and `%`; `error`, `ref` and
61//! `lambda`, the syntax's own forms; a head the head table declares no
62//! formula's (`set!`, `begin`, `.`, `new`; the table's formula column,
63//! F.7's declaration) refused; any other head a function call, its name
64//! letters, digits, `.` and `_` from a letter on. `if`, `not` and `mod`,
65//! VLA's own heads, have their arity checked; an Excel function's arity is
66//! Excel's. `quote`, an array constant, is outside this version.
67//!
68//! **What [`read`] refuses** is told apart: a text that is no formula at
69//! all (`vla-formula-read-malformed`), and a construct Excel reads that
70//! this version does not (`vla-formula-read-unsupported`): an array
71//! constant, `@`, an intersection, a union, a spill, a structured
72//! reference, a link into another workbook, an omitted argument, `:`
73//! between computed operands, a `_xlfn.` spelling, an error beyond the
74//! seven, a LAMBDA called in place, and a function named like a word the
75//! syntax keeps for its own forms. The reader has no VBA twin, as the
76//! evaluator has none: the add-in's formulas are read by Excel, and its two
77//! ids are raised by no VBA site. The writer has one (`VLA.bas`'s
78//! `FormulaSyntaxWrite`), held to this one by the expand golden.
79
80use crate::calc::formula::{LENGTH_LIMIT, NESTING_LIMIT};
81use crate::calc::value::ErrorKind;
82use crate::form::{Form, List};
83use crate::headtable::{self, FormulaSupport};
84use crate::intrinsics::{fold, is_numeric};
85use crate::messages::{raise, Refusal};
86use crate::printer::write_datum;
87use crate::reader::FORM_DEPTH_LIMIT;
88use crate::refers::{self, is_ident_char, quote_sheet, FormulaRef, Kind, Shape};
89use crate::sheet::column_letters;
90
91/// The longest text a formula's literal holds, Excel's own limit, in UTF-16
92/// code units as Excel and VBA's `Len` count them.
93pub const TEXT_LIMIT: usize = 255;
94
95// The precedence levels, lowest first.
96const CMP: u8 = 1;
97const CAT: u8 = 2;
98const ADD: u8 = 3;
99const MUL: u8 = 4;
100const POW: u8 = 5;
101const SIGN: u8 = 6;
102const PCT: u8 = 7;
103const ATOM: u8 = 8;
104
105/// The binary operators, as the head spells each and the text writes it.
106const BINARY: &[&str] = &[
107    "=", "<>", "<", ">", "<=", ">=", "&", "+", "-", "*", "/", "^",
108];
109
110fn binary_level(op: &str) -> u8 {
111    match op {
112        "=" | "<>" | "<" | ">" | "<=" | ">=" => CMP,
113        "&" => CAT,
114        "+" | "-" => ADD,
115        "*" | "/" => MUL,
116        _ => POW,
117    }
118}
119
120// ---------------------------------------------------------------------
121//  The writer
122// ---------------------------------------------------------------------
123
124/// An expression the expander has finished with, as a formula's canonical
125/// text with its leading `=`; refused by name when it is not one.
126pub fn write(expr: &Form) -> Result<String, Refusal> {
127    Ok(format!("={}", text(expr, 1)?))
128}
129
130/// The expression's level of precedence, as the writer will spell it.
131fn level(f: &Form) -> u8 {
132    match f {
133        Form::Sym(s) if is_negative_numeral(s) => SIGN,
134        Form::List(l) => {
135            let Some(Form::Sym(head)) = l.items.first() else {
136                return ATOM;
137            };
138            let h = fold(head);
139            let n = l.items.len() - 1;
140            match h.as_str() {
141                "+" | "-" if n == 1 => SIGN,
142                "%" if n == 1 => PCT,
143                op if n >= 2 && BINARY.contains(&op) => binary_level(op),
144                _ => ATOM,
145            }
146        }
147        _ => ATOM,
148    }
149}
150
151/// An operand, parenthesized when the level it sits at needs it: `right`
152/// for every operand of a binary operator after its first.
153fn operand(f: &Form, at: u8, right: bool, depth: u32) -> Result<String, Refusal> {
154    let t = text(f, depth + 1)?;
155    let l = level(f);
156    Ok(if l < at || (right && l == at) {
157        format!("({t})")
158    } else {
159        t
160    })
161}
162
163fn text(f: &Form, depth: u32) -> Result<String, Refusal> {
164    if depth > FORM_DEPTH_LIMIT {
165        return Err(raise(
166            "vla-form-too-deep",
167            &[("limit", &FORM_DEPTH_LIMIT.to_string()), ("loc", "")],
168        ));
169    }
170    match f {
171        Form::Str(s) => text_literal(s),
172        Form::Sym(s) => atom(s),
173        Form::List(l) => list(l, depth),
174    }
175}
176
177/// A text literal: its quote marks doubled, at most [`TEXT_LIMIT`] long.
178fn text_literal(s: &str) -> Result<String, Refusal> {
179    let n = s.encode_utf16().count();
180    if n > TEXT_LIMIT {
181        return Err(raise("vla-formula-text-too-long", &[("n", &n.to_string())]));
182    }
183    Ok(format!("\"{}\"", s.replace('"', "\"\"")))
184}
185
186/// A symbol: a numeral, a truth value, a reference or a name, each in its
187/// canonical spelling; anything else refused.
188fn atom(s: &str) -> Result<String, Refusal> {
189    if is_numeral(s) || is_negative_numeral(s) {
190        let unsigned = s.strip_prefix('-').unwrap_or(s);
191        if !unsigned.parse::<f64>().is_ok_and(f64::is_finite) {
192            return Err(raise("vla-formula-numeral-unspelled", &[("tok", s)]));
193        }
194        return Ok(s.to_string());
195    }
196    match fold(s).as_str() {
197        "true" => return Ok("TRUE".to_string()),
198        "false" => return Ok("FALSE".to_string()),
199        _ => {}
200    }
201    match whole_reference(s) {
202        Some(r) => match r.kind {
203            Kind::Cell | Kind::Range | Kind::Column | Kind::Row | Kind::ThreeD | Kind::Name => {
204                Ok(reference_text(&r))
205            }
206            _ => Err(raise(
207                "vla-formula-unsupported",
208                &[("construct", &construct_of(&r))],
209            )),
210        },
211        None if is_numeric(s) => Err(raise("vla-formula-numeral-unspelled", &[("tok", s)])),
212        None => Err(raise("vla-formula-atom-unspelled", &[("tok", s)])),
213    }
214}
215
216/// The one record the scanner reads over the whole of `s`, or none.
217fn whole_reference(s: &str) -> Option<FormulaRef> {
218    if s.is_empty() || s.starts_with('=') {
219        return None;
220    }
221    let mut found = refers::scan(s);
222    if found.len() != 1 {
223        return None;
224    }
225    let r = found.pop()?;
226    (r.pos == 0 && r.span == s.chars().count()).then_some(r)
227}
228
229/// What a construct outside this version is called in a refusal.
230fn construct_of(r: &FormulaRef) -> String {
231    let what = match r.kind {
232        Kind::Spill => "the spill reference",
233        Kind::Structured => "the structured reference",
234        Kind::External => "the reference into another workbook",
235        Kind::Broken => "the broken reference",
236        _ => "the reference",
237    };
238    format!("{what} {}", r.written)
239}
240
241/// A reference's canonical spelling: its sheet or span quoted only where it
242/// must be, its column letters upper-case, its `$` marks as written; a
243/// name as written behind its sheet.
244fn reference_text(r: &FormulaRef) -> String {
245    let part = match (r.kind, r.shape) {
246        (Kind::Name, _) | (_, Shape::None) => r.part.clone(),
247        (_, Shape::Cell) => cell_text(r.col1, r.col_abs1, r.row1, r.row_abs1),
248        (_, Shape::Range) => format!(
249            "{}:{}",
250            cell_text(r.col1, r.col_abs1, r.row1, r.row_abs1),
251            cell_text(r.col2, r.col_abs2, r.row2, r.row_abs2)
252        ),
253        (_, Shape::Column) => format!(
254            "{}{}:{}{}",
255            mark(r.col_abs1),
256            column_letters(r.col1),
257            mark(r.col_abs2),
258            column_letters(r.col2)
259        ),
260        (_, Shape::Row) => format!(
261            "{}{}:{}{}",
262            mark(r.row_abs1),
263            r.row1,
264            mark(r.row_abs2),
265            r.row2
266        ),
267    };
268    if r.sheet_name.is_empty() {
269        part
270    } else {
271        format!("{}!{part}", sheet_text(&r.sheet_name))
272    }
273}
274
275fn cell_text(col: u32, col_abs: bool, row: u32, row_abs: bool) -> String {
276    format!(
277        "{}{}{}{}",
278        mark(col_abs),
279        column_letters(col),
280        mark(row_abs),
281        row
282    )
283}
284
285fn mark(abs: bool) -> &'static str {
286    if abs {
287        "$"
288    } else {
289        ""
290    }
291}
292
293/// A sheet, or a span of sheets, as a qualifier spells it: a span is quoted
294/// whole when either end needs quotes, as Excel writes `'Jan 1:Dec'!A1`.
295fn sheet_text(sheet: &str) -> String {
296    match sheet.split_once(':') {
297        Some((first, last)) => {
298            if quote_sheet(first) == first && quote_sheet(last) == last {
299                format!("{first}:{last}")
300            } else {
301                format!(
302                    "'{}:{}'",
303                    first.replace('\'', "''"),
304                    last.replace('\'', "''")
305                )
306            }
307        }
308        None => quote_sheet(sheet),
309    }
310}
311
312/// A list: an operator, one of the syntax's own forms, or a call.
313fn list(l: &List, depth: u32) -> Result<String, Refusal> {
314    let Some(head_form) = l.items.first() else {
315        return Err(raise("vla-formula-head-not-a-function", &[("head", "()")]));
316    };
317    let Form::Sym(head) = head_form else {
318        return Err(raise(
319            "vla-formula-head-not-a-function",
320            &[("head", &write_datum(head_form))],
321        ));
322    };
323    let h = headtable::resolve_head_alias(&fold(head));
324    let args = &l.items[1..];
325    let n = args.len();
326    match h.as_str() {
327        "+" | "-" if n == 1 => Ok(format!("{h}{}", operand(&args[0], SIGN, false, depth)?)),
328        "%" if n == 1 => Ok(format!("{}%", operand(&args[0], PCT, false, depth)?)),
329        "%" => Err(arity(head, "one operand", n)),
330        op if BINARY.contains(&op) => {
331            if n < 2 {
332                return Err(arity(
333                    head,
334                    if op == "+" || op == "-" {
335                        "one operand or more"
336                    } else {
337                        "two operands or more"
338                    },
339                    n,
340                ));
341            }
342            let at = binary_level(op);
343            let mut out = operand(&args[0], at, false, depth)?;
344            for a in &args[1..] {
345                // `#N/A`, the one error that ends in neither `!` nor `?`,
346                // is read with a `/` after it as one token by the
347                // language's parser (`calc::formula`), so before a `/` it
348                // is parenthesized: `x*(#N/A)/y`.
349                if op == "/" && out.ends_with("#N/A") {
350                    out.truncate(out.len() - 4);
351                    out.push_str("(#N/A)");
352                }
353                out.push_str(op);
354                out.push_str(&operand(a, at, true, depth)?);
355            }
356            Ok(out)
357        }
358        "error" => error_literal(head, args),
359        "ref" => ref_form(head, args),
360        "lambda" => lambda(head, args, depth),
361        "quote" => Err(raise(
362            "vla-formula-unsupported",
363            &[("construct", "an array constant, (quote ...)")],
364        )),
365        _ => {
366            if let Some(row) = headtable::row(&h) {
367                if row.formula != FormulaSupport::Yes {
368                    return Err(raise("vla-formula-head-refused", &[("head", head)]));
369                }
370            }
371            let want = match h.as_str() {
372                "if" => (!(2..=3).contains(&n)).then_some("two or three arguments"),
373                "not" => (n != 1).then_some("one argument"),
374                "mod" => (n != 2).then_some("two arguments"),
375                _ => None,
376            };
377            if let Some(want) = want {
378                return Err(arity(head, want, n));
379            }
380            let name = if h == fold(head) {
381                head.as_str()
382            } else {
383                h.as_str()
384            };
385            if !is_function_name(name) {
386                return Err(raise("vla-formula-head-not-a-function", &[("head", head)]));
387            }
388            let mut parts = Vec::with_capacity(n);
389            for a in args {
390                parts.push(text(a, depth + 1)?);
391            }
392            Ok(format!(
393                "{}({})",
394                name.to_ascii_uppercase(),
395                parts.join(",")
396            ))
397        }
398    }
399}
400
401fn arity(head: &str, want: &str, n: usize) -> Refusal {
402    raise(
403        "vla-formula-arity",
404        &[("head", head), ("want", want), ("n", &n.to_string())],
405    )
406}
407
408/// `(error "#N/A")`: one of the seven errors a formula holds.
409fn error_literal(head: &str, args: &[Form]) -> Result<String, Refusal> {
410    let [Form::Str(t)] = args else {
411        return Err(arity(head, "one text, such as \"#N/A\"", args.len()));
412    };
413    match ErrorKind::parse(t) {
414        Some(k) => Ok(k.text().to_string()),
415        None => Err(raise(
416            "vla-formula-unsupported",
417            &[("construct", &format!("the error {t}"))],
418        )),
419    }
420}
421
422/// `(ref "Sheet" "A1")` or `(ref "A1")`: one reference or one name, built
423/// from a sheet and an address, the sheet quoted as it must be.
424fn ref_form(head: &str, args: &[Form]) -> Result<String, Refusal> {
425    let built = match args {
426        [Form::Str(part)] => part.clone(),
427        [Form::Str(sheet), Form::Str(part)] => format!("{}!{part}", sheet_text(sheet)),
428        _ => {
429            return Err(arity(
430                head,
431                "a sheet and an address as texts, or an address alone",
432                args.len(),
433            ))
434        }
435    };
436    match whole_reference(&built) {
437        Some(r)
438            if matches!(
439                r.kind,
440                Kind::Cell | Kind::Range | Kind::Column | Kind::Row | Kind::ThreeD | Kind::Name
441            ) =>
442        {
443            Ok(reference_text(&r))
444        }
445        _ => Err(raise("vla-formula-ref-invalid", &[("text", &built)])),
446    }
447}
448
449/// `(lambda (x y) body)`: Excel's `LAMBDA(x,y,body)`.
450fn lambda(head: &str, args: &[Form], depth: u32) -> Result<String, Refusal> {
451    let [params, body] = args else {
452        return Err(arity(head, "a list of parameters and one body", args.len()));
453    };
454    let Form::List(params) = params else {
455        return Err(raise(
456            "vla-formula-lambda-params",
457            &[("tok", &write_datum(params))],
458        ));
459    };
460    let mut out = String::from("LAMBDA(");
461    for p in &params.items {
462        let name = match p {
463            Form::Sym(s) => match whole_reference(s) {
464                Some(r) if r.kind == Kind::Name && r.sheet_name.is_empty() => s.clone(),
465                _ => return Err(raise("vla-formula-lambda-params", &[("tok", s)])),
466            },
467            other => {
468                return Err(raise(
469                    "vla-formula-lambda-params",
470                    &[("tok", &write_datum(other))],
471                ))
472            }
473        };
474        out.push_str(&name);
475        out.push(',');
476    }
477    out.push_str(&text(body, depth + 1)?);
478    out.push(')');
479    Ok(out)
480}
481
482/// The formula's numeral, unsigned: digits with an optional fraction, or a
483/// fraction alone, then an optional exponent with digits.
484pub fn is_numeral(s: &str) -> bool {
485    let b = s.as_bytes();
486    let mut i = 0;
487    let mut digits = 0;
488    while i < b.len() && b[i].is_ascii_digit() {
489        i += 1;
490        digits += 1;
491    }
492    if i < b.len() && b[i] == b'.' {
493        i += 1;
494        while i < b.len() && b[i].is_ascii_digit() {
495            i += 1;
496            digits += 1;
497        }
498    }
499    if digits == 0 {
500        return false;
501    }
502    if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
503        i += 1;
504        if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
505            i += 1;
506        }
507        let first = i;
508        while i < b.len() && b[i].is_ascii_digit() {
509            i += 1;
510        }
511        if i == first {
512            return false;
513        }
514    }
515    i == b.len()
516}
517
518fn is_negative_numeral(s: &str) -> bool {
519    s.strip_prefix('-').is_some_and(is_numeral)
520}
521
522/// A function's name as a formula spells one: a letter, then letters,
523/// digits, `.` and `_`.
524fn is_function_name(s: &str) -> bool {
525    let mut cs = s.chars();
526    cs.next().is_some_and(|c| c.is_ascii_alphabetic())
527        && cs.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
528}
529
530// ---------------------------------------------------------------------
531//  The reader
532// ---------------------------------------------------------------------
533
534/// A formula's text, with or without its `=`, as the canonical expression;
535/// refused as malformed or as a construct this version does not read.
536pub fn read(formula_text: &str) -> Result<Form, Refusal> {
537    let body = formula_text.strip_prefix('=').unwrap_or(formula_text);
538    if body.chars().count() > LENGTH_LIMIT {
539        return Err(malformed(
540            "it is longer than 8,192 characters, Excel's own limit",
541        ));
542    }
543    let toks = lex(formula_text)?;
544    if toks.is_empty() {
545        return Err(malformed("it is empty"));
546    }
547    let mut p = Reader {
548        toks,
549        at: 0,
550        depth: 0,
551    };
552    let e = p.comparison()?;
553    match p.peek() {
554        None => Ok(e),
555        Some(t) => Err(p.stray(t)),
556    }
557}
558
559fn malformed(what: &str) -> Refusal {
560    raise("vla-formula-read-malformed", &[("what", what)])
561}
562
563fn unsupported(construct: &str) -> Refusal {
564    raise("vla-formula-read-unsupported", &[("construct", construct)])
565}
566
567#[derive(Clone, Debug, PartialEq)]
568enum Tok {
569    /// A numeral, as written.
570    Num(String),
571    Text(String),
572    Bool(bool),
573    Err(ErrorKind),
574    /// A reference or a name, the scanner's record.
575    Ref(Box<FormulaRef>),
576    /// A name followed by `(`, as written; the `(` follows as its own token.
577    Func(String),
578    Op(&'static str),
579    Percent,
580    Open,
581    Close,
582    Comma,
583}
584
585/// The tokens of a formula's text, the references and names read by
586/// `refers::scan`, the one reading of them.
587fn lex(text: &str) -> Result<Vec<Tok>, Refusal> {
588    let s: Vec<char> = text.chars().collect();
589    let refs: Vec<FormulaRef> = refers::scan(text)
590        .into_iter()
591        .filter(|r| r.kind != Kind::Unreadable)
592        .collect();
593    let mut toks = Vec::new();
594    let mut i = usize::from(s.first() == Some(&'='));
595    let mut next_ref = 0;
596    while i < s.len() {
597        while next_ref < refs.len() && refs[next_ref].pos < i {
598            next_ref += 1;
599        }
600        if next_ref < refs.len() && refs[next_ref].pos == i {
601            let r = refs[next_ref].clone();
602            next_ref += 1;
603            i += r.span.max(1);
604            match r.kind {
605                Kind::Cell | Kind::Range | Kind::Column | Kind::Row | Kind::ThreeD | Kind::Name => {
606                    toks.push(Tok::Ref(Box::new(r)))
607                }
608                Kind::Broken if r.written == r.part && fold(&r.part) == "#ref!" => {
609                    toks.push(Tok::Err(ErrorKind::Ref))
610                }
611                _ => return Err(unsupported(&construct_of(&r))),
612            }
613            continue;
614        }
615        let c = s[i];
616        if c.is_whitespace() {
617            i += 1;
618        } else if c == '"' {
619            let mut out = String::new();
620            let mut k = i + 1;
621            let mut closed = false;
622            while k < s.len() {
623                if s[k] == '"' {
624                    if s.get(k + 1) == Some(&'"') {
625                        out.push('"');
626                        k += 2;
627                        continue;
628                    }
629                    closed = true;
630                    k += 1;
631                    break;
632                }
633                out.push(s[k]);
634                k += 1;
635            }
636            if !closed {
637                return Err(malformed("a text that never closes"));
638            }
639            if out.encode_utf16().count() > TEXT_LIMIT {
640                return Err(malformed(
641                    "a text longer than 255 characters, Excel's own limit",
642                ));
643            }
644            toks.push(Tok::Text(out));
645            i = k;
646        } else if c.is_ascii_digit()
647            || (c == '.' && s.get(i + 1).is_some_and(|d| d.is_ascii_digit()))
648        {
649            let mut k = i;
650            while k < s.len() && s[k].is_ascii_digit() {
651                k += 1;
652            }
653            if k < s.len() && s[k] == '.' {
654                k += 1;
655                while k < s.len() && s[k].is_ascii_digit() {
656                    k += 1;
657                }
658            }
659            if k < s.len() && (s[k] == 'e' || s[k] == 'E') {
660                let mut j = k + 1;
661                if j < s.len() && (s[j] == '+' || s[j] == '-') {
662                    j += 1;
663                }
664                let first = j;
665                while j < s.len() && s[j].is_ascii_digit() {
666                    j += 1;
667                }
668                if j > first {
669                    k = j;
670                }
671            }
672            let numeral: String = s[i..k].iter().collect();
673            if !numeral.parse::<f64>().is_ok_and(f64::is_finite) {
674                return Err(malformed(&format!(
675                    "the number {numeral}, which no double holds"
676                )));
677            }
678            toks.push(Tok::Num(numeral));
679            i = k;
680        } else if c == '#' {
681            let mut k = i + 1;
682            while k < s.len() && (s[k].is_ascii_alphanumeric() || s[k] == '/' || s[k] == '_') {
683                k += 1;
684            }
685            if matches!(s.get(k), Some('!') | Some('?')) {
686                k += 1;
687            }
688            let tok: String = s[i..k].iter().collect();
689            match ErrorKind::parse(&tok) {
690                Some(kind) => toks.push(Tok::Err(kind)),
691                None if fold(&tok).starts_with("#n/a/") => return Err(unsupported(
692                    "#N/A right before a /, which this version reads only parenthesized, (#N/A)",
693                )),
694                None => return Err(unsupported(&format!("the error {tok}"))),
695            }
696            i = k;
697        } else if c.is_ascii_alphabetic() || c == '_' || (c as u32) > 127 {
698            let mut k = i;
699            while k < s.len() && is_ident_char(s[k]) {
700                k += 1;
701            }
702            let tok: String = s[i..k].iter().collect();
703            if s.get(k) == Some(&'(') {
704                toks.push(Tok::Func(tok));
705            } else {
706                match fold(&tok).as_str() {
707                    "true" => toks.push(Tok::Bool(true)),
708                    "false" => toks.push(Tok::Bool(false)),
709                    _ => return Err(malformed(&format!("'{tok}', which reads as no name"))),
710                }
711            }
712            i = k;
713        } else {
714            let two: String = s[i..(i + 2).min(s.len())].iter().collect();
715            let (tok, len) = match two.as_str() {
716                "<>" => (Tok::Op("<>"), 2),
717                "<=" => (Tok::Op("<="), 2),
718                ">=" => (Tok::Op(">="), 2),
719                _ => match c {
720                    '<' => (Tok::Op("<"), 1),
721                    '>' => (Tok::Op(">"), 1),
722                    '=' => (Tok::Op("="), 1),
723                    '+' => (Tok::Op("+"), 1),
724                    '-' => (Tok::Op("-"), 1),
725                    '*' => (Tok::Op("*"), 1),
726                    '/' => (Tok::Op("/"), 1),
727                    '^' => (Tok::Op("^"), 1),
728                    '&' => (Tok::Op("&"), 1),
729                    '%' => (Tok::Percent, 1),
730                    '(' => (Tok::Open, 1),
731                    ')' => (Tok::Close, 1),
732                    ',' => (Tok::Comma, 1),
733                    '{' => return Err(unsupported("an array constant, {...}")),
734                    '@' => return Err(unsupported("the implicit intersection @")),
735                    ':' => {
736                        return Err(unsupported(
737                            "the range operator : between computed operands",
738                        ))
739                    }
740                    '[' => return Err(unsupported("a structured reference")),
741                    other => {
742                        return Err(malformed(&format!(
743                            "'{other}', which no formula holds there"
744                        )))
745                    }
746                },
747            };
748            toks.push(tok);
749            i += len;
750        }
751    }
752    Ok(toks)
753}
754
755struct Reader {
756    toks: Vec<Tok>,
757    at: usize,
758    depth: u32,
759}
760
761impl Reader {
762    fn peek(&self) -> Option<&Tok> {
763        self.toks.get(self.at)
764    }
765
766    fn take(&mut self) -> Option<Tok> {
767        let t = self.toks.get(self.at).cloned();
768        if t.is_some() {
769            self.at += 1;
770        }
771        t
772    }
773
774    fn peek_op(&self) -> Option<&'static str> {
775        match self.peek() {
776            Some(Tok::Op(op)) => Some(op),
777            _ => None,
778        }
779    }
780
781    /// `f` one level of nesting deeper, Excel's limit of 64 held.
782    fn nested<T>(&mut self, f: impl FnOnce(&mut Self) -> Result<T, Refusal>) -> Result<T, Refusal> {
783        if self.depth >= NESTING_LIMIT {
784            return Err(malformed("it nests deeper than 64, Excel's own limit"));
785        }
786        self.depth += 1;
787        let r = f(self);
788        self.depth -= 1;
789        r
790    }
791
792    /// The refusal of a token found where nothing more was wanted.
793    fn stray(&self, t: &Tok) -> Refusal {
794        match t {
795            Tok::Close => malformed("a ')' that opens nothing"),
796            Tok::Comma => malformed("a ',' outside a call"),
797            _ => malformed("two operands with no operator between them"),
798        }
799    }
800
801    /// A left-associative run of the operators `ops` over `next`, a run of
802    /// one operator read as one form of many operands.
803    fn binary(
804        &mut self,
805        ops: &[&'static str],
806        next: fn(&mut Self) -> Result<Form, Refusal>,
807    ) -> Result<Form, Refusal> {
808        let mut left = next(self)?;
809        while let Some(op) = self.peek_op() {
810            if !ops.contains(&op) {
811                break;
812            }
813            self.at += 1;
814            let right = next(self)?;
815            left = match left {
816                Form::List(mut l)
817                    if l.items.len() >= 3
818                        && matches!(l.items.first(), Some(Form::Sym(h)) if h == op) =>
819                {
820                    l.items.push(right);
821                    Form::List(l)
822                }
823                other => Form::list(vec![Form::sym(op), other, right]),
824            };
825        }
826        Ok(left)
827    }
828
829    fn comparison(&mut self) -> Result<Form, Refusal> {
830        self.binary(&["=", "<>", "<", ">", "<=", ">="], Self::concat)
831    }
832
833    fn concat(&mut self) -> Result<Form, Refusal> {
834        self.binary(&["&"], Self::additive)
835    }
836
837    fn additive(&mut self) -> Result<Form, Refusal> {
838        self.binary(&["+", "-"], Self::term)
839    }
840
841    fn term(&mut self) -> Result<Form, Refusal> {
842        self.binary(&["*", "/"], Self::power)
843    }
844
845    fn power(&mut self) -> Result<Form, Refusal> {
846        self.binary(&["^"], Self::unary)
847    }
848
849    fn unary(&mut self) -> Result<Form, Refusal> {
850        match self.peek_op() {
851            Some(op @ ("-" | "+")) => {
852                self.at += 1;
853                let x = self.nested(Self::unary)?;
854                Ok(match (op, x) {
855                    ("-", Form::Sym(n)) if is_numeral(&n) => Form::Sym(format!("-{n}")),
856                    (_, x) => Form::list(vec![Form::sym(op), x]),
857                })
858            }
859            _ => self.postfix(),
860        }
861    }
862
863    fn postfix(&mut self) -> Result<Form, Refusal> {
864        let mut e = self.primary()?;
865        let mut levels = 0;
866        while self.peek() == Some(&Tok::Percent) {
867            if self.depth + levels >= NESTING_LIMIT {
868                return Err(malformed("it nests deeper than 64, Excel's own limit"));
869            }
870            levels += 1;
871            self.at += 1;
872            e = Form::list(vec![Form::sym("%"), e]);
873        }
874        if let Some(t) = self.peek() {
875            if starts_operand(t) {
876                let both_refs = matches!(self.toks.get(self.at - 1), Some(Tok::Ref(_)))
877                    && matches!(t, Tok::Ref(_));
878                return Err(if both_refs {
879                    unsupported("the intersection of two references, a space between them")
880                } else {
881                    malformed("two operands with no operator between them")
882                });
883            }
884        }
885        Ok(e)
886    }
887
888    fn primary(&mut self) -> Result<Form, Refusal> {
889        let Some(t) = self.take() else {
890            return Err(malformed("it ends where an operand was wanted"));
891        };
892        match t {
893            Tok::Num(n) => Ok(Form::Sym(n)),
894            Tok::Text(s) => Ok(Form::Str(s)),
895            Tok::Bool(b) => Ok(Form::sym(if b { "true" } else { "false" })),
896            Tok::Err(k) => Ok(Form::list(vec![Form::sym("error"), Form::string(k.text())])),
897            Tok::Ref(r) => Ok(reference_form(&r)),
898            Tok::Func(name) => {
899                self.at += 1; // the `(` the lexer put after the name
900                self.nested(|p| p.call(name))
901            }
902            Tok::Open => {
903                let e = self.nested(Self::comparison)?;
904                match self.take() {
905                    Some(Tok::Close) => Ok(e),
906                    Some(Tok::Comma) => Err(unsupported("the union of references, (A1,B2)")),
907                    Some(other) => Err(self.stray(&other)),
908                    None => Err(malformed("a '(' that never closes")),
909                }
910            }
911            Tok::Close => Err(malformed("a ')' where an operand was wanted")),
912            Tok::Comma => Err(malformed("a ',' where an operand was wanted")),
913            Tok::Op(op) => Err(malformed(&format!(
914                "the operator {op} where an operand was wanted"
915            ))),
916            Tok::Percent => Err(malformed("a '%' where an operand was wanted")),
917        }
918    }
919
920    /// A call's arguments after its `(`, through its `)`.
921    fn call(&mut self, name: String) -> Result<Form, Refusal> {
922        let folded = fold(&name);
923        if folded.starts_with("_xl") {
924            return Err(unsupported(&format!("the file's spelling {name}")));
925        }
926        if !is_function_name(&name) {
927            return Err(unsupported(&format!("the function name {name}")));
928        }
929        let reserved = matches!(folded.as_str(), "error" | "ref" | "quote")
930            || headtable::row(&folded).is_some_and(|r| r.formula != FormulaSupport::Yes);
931        if reserved {
932            return Err(unsupported(&format!(
933                "a function named {name}, a word the syntax keeps for a form of its own"
934            )));
935        }
936        let mut args = Vec::new();
937        if self.peek() == Some(&Tok::Close) {
938            self.at += 1;
939        } else {
940            loop {
941                if matches!(self.peek(), Some(Tok::Comma) | Some(Tok::Close)) {
942                    return Err(unsupported("an omitted argument"));
943                }
944                args.push(self.comparison()?);
945                match self.take() {
946                    Some(Tok::Comma) => continue,
947                    Some(Tok::Close) => break,
948                    Some(other) => return Err(self.stray(&other)),
949                    None => return Err(malformed(&format!("{name}( never closes"))),
950                }
951            }
952        }
953        if self.peek() == Some(&Tok::Open) {
954            return Err(unsupported(
955                "a function called in place, such as LAMBDA(x,x)(1)",
956            ));
957        }
958        let n = args.len();
959        let bad = match folded.as_str() {
960            "if" => (!(2..=3).contains(&n)).then_some("IF takes two or three arguments"),
961            "not" => (n != 1).then_some("NOT takes one argument"),
962            "mod" => (n != 2).then_some("MOD takes two arguments"),
963            "lambda" => (n == 0).then_some("LAMBDA takes at least its body"),
964            _ => None,
965        };
966        if let Some(bad) = bad {
967            return Err(malformed(bad));
968        }
969        if folded == "lambda" {
970            let body = args.pop().expect("LAMBDA's body, counted above");
971            let mut params = Vec::with_capacity(args.len());
972            for a in args {
973                match &a {
974                    Form::Sym(s)
975                        if whole_reference(s)
976                            .is_some_and(|r| r.kind == Kind::Name && r.sheet_name.is_empty()) =>
977                    {
978                        params.push(a)
979                    }
980                    _ => return Err(malformed("a LAMBDA parameter that is no name")),
981                }
982            }
983            return Ok(Form::list(vec![
984                Form::sym("lambda"),
985                Form::list(params),
986                body,
987            ]));
988        }
989        let mut items = Vec::with_capacity(n + 1);
990        items.push(Form::Sym(folded));
991        items.extend(args);
992        Ok(Form::list(items))
993    }
994}
995
996fn starts_operand(t: &Tok) -> bool {
997    matches!(
998        t,
999        Tok::Num(_)
1000            | Tok::Text(_)
1001            | Tok::Bool(_)
1002            | Tok::Err(_)
1003            | Tok::Ref(_)
1004            | Tok::Func(_)
1005            | Tok::Open
1006    )
1007}
1008
1009/// A reference or a name as the canonical expression holds it: the bare
1010/// symbol of its canonical spelling, or `(ref sheet part)` where a symbol
1011/// cannot hold that spelling.
1012fn reference_form(r: &FormulaRef) -> Form {
1013    let canonical = reference_text(r);
1014    if canonical
1015        .chars()
1016        .any(|c| matches!(c, ' ' | '\t' | '\r' | '\n' | '(' | ')' | ';' | '"'))
1017    {
1018        let part = match canonical.rfind('!') {
1019            Some(at) => canonical[at + 1..].to_string(),
1020            None => canonical.clone(),
1021        };
1022        Form::list(vec![
1023            Form::sym("ref"),
1024            Form::string(&r.sheet_name),
1025            Form::string(&part),
1026        ])
1027    } else {
1028        Form::Sym(canonical)
1029    }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035    use crate::reader::read_forms;
1036
1037    fn w(src: &str) -> String {
1038        write(&read_forms(src).unwrap()[0]).unwrap_or_else(|e| panic!("{src}: {}", e.text))
1039    }
1040
1041    fn w_err(src: &str) -> String {
1042        write(&read_forms(src).unwrap()[0]).unwrap_err().id
1043    }
1044
1045    fn r(text: &str) -> String {
1046        write_datum(&read(text).unwrap_or_else(|e| panic!("{text}: {}", e.text)))
1047    }
1048
1049    fn r_err(text: &str) -> String {
1050        read(text).unwrap_err().id
1051    }
1052
1053    #[test]
1054    fn the_atoms_are_written_in_their_canonical_spelling() {
1055        assert_eq!(w("5"), "=5");
1056        assert_eq!(w("1.50"), "=1.50");
1057        assert_eq!(w(".5"), "=.5");
1058        assert_eq!(w("1e3"), "=1e3");
1059        assert_eq!(w("-1"), "=-1");
1060        assert_eq!(w("\"say \\\"hi\\\"\""), "=\"say \"\"hi\"\"\"");
1061        assert_eq!(w("\"\""), "=\"\"");
1062        assert_eq!(w("True"), "=TRUE");
1063        assert_eq!(w("(error \"#div/0!\")"), "=#DIV/0!");
1064        assert_eq!(w("a1"), "=A1");
1065        assert_eq!(w("$a$1:B$2"), "=$A$1:B$2");
1066        assert_eq!(w("e:E"), "=E:E");
1067        assert_eq!(w("5:$7"), "=5:$7");
1068        assert_eq!(w("'Input'!b5"), "=Input!B5");
1069        assert_eq!(w("Tally.last!B16"), "=Tally.last!B16");
1070        assert_eq!(w("'2026'!A1"), "='2026'!A1");
1071        assert_eq!(w("Jan:Dec!A1"), "=Jan:Dec!A1");
1072        assert_eq!(w("Rate"), "=Rate");
1073        assert_eq!(w("Model!Local"), "=Model!Local");
1074        assert_eq!(w("(ref \"Q1 Data\" \"a1\")"), "='Q1 Data'!A1");
1075        assert_eq!(w("(ref \"Input\" \"B5\")"), "=Input!B5");
1076        assert_eq!(w("(ref \"C3\")"), "=C3");
1077    }
1078
1079    #[test]
1080    fn precedence_is_written_with_the_fewest_parentheses() {
1081        assert_eq!(w("(+ 1 (* 2 3))"), "=1+2*3");
1082        assert_eq!(w("(* (+ 1 2) 3)"), "=(1+2)*3");
1083        assert_eq!(w("(- a (- b c))"), "=a-(b-c)");
1084        assert_eq!(w("(- a b c)"), "=a-b-c");
1085        assert_eq!(w("(- (- a b) c)"), "=a-b-c");
1086        assert_eq!(w("(^ 2 (^ 3 2))"), "=2^(3^2)");
1087        assert_eq!(w("(^ 2 3 2)"), "=2^3^2");
1088        assert_eq!(w("(- (^ x 2))"), "=-(x^2)");
1089        assert_eq!(w("(^ (- x) 2)"), "=-x^2");
1090        assert_eq!(w("(^ -2 2)"), "=-2^2");
1091        assert_eq!(w("(% -5)"), "=(-5)%");
1092        assert_eq!(w("(- (% 5))"), "=-5%");
1093        assert_eq!(w("(% (+ a b))"), "=(a+b)%");
1094        assert_eq!(w("(+ x)"), "=+x");
1095        assert_eq!(w("(= (& 1 2) \"12\")"), "=1&2=\"12\"");
1096        assert_eq!(w("(& (= a b) c)"), "=(a=b)&c");
1097        assert_eq!(w("(< a b c)"), "=a<b<c");
1098        assert_eq!(w("(* a -1)"), "=a*-1");
1099        assert_eq!(w("(- 2 -1)"), "=2--1");
1100        assert_eq!(w("(- (- x))"), "=--x");
1101        // `#N/A` before a `/` is parenthesized, the language's parser
1102        // reading `#N/A/x` as one token; the generator's seed 222 found it.
1103        assert_eq!(w("(/ (error \"#N/A\") x)"), "=(#N/A)/x");
1104        assert_eq!(w("(/ (^ 2 (error \"#N/A\")) x)"), "=2^(#N/A)/x");
1105        assert_eq!(w("(/ x (error \"#N/A\") y)"), "=x/(#N/A)/y");
1106        assert_eq!(w("(/ x (error \"#N/A\"))"), "=x/#N/A");
1107        assert_eq!(w("(/ (error \"#NUM!\") x)"), "=#NUM!/x");
1108    }
1109
1110    #[test]
1111    fn calls_and_the_syntax_s_own_forms() {
1112        assert_eq!(
1113            w("(if (> A1 3) (stdev.s A1:A9) (pi))"),
1114            "=IF(A1>3,STDEV.S(A1:A9),PI())"
1115        );
1116        assert_eq!(w("(SUM a)"), "=SUM(a)");
1117        assert_eq!(w("(si (> a 1) 1 0)"), "=IF(a>1,1,0)");
1118        assert_eq!(w("(log10 LOG10)"), "=LOG10(LOG10)");
1119        assert_eq!(
1120            w("(let x 1 (lambda (y) (+ x y)))"),
1121            "=LET(x,1,LAMBDA(y,x+y))"
1122        );
1123        assert_eq!(w("(lambda () 1)"), "=LAMBDA(1)");
1124        assert_eq!(
1125            w("(and true (not b) (or c d))"),
1126            "=AND(TRUE,NOT(b),OR(c,d))"
1127        );
1128        assert_eq!(w("(mod n 2)"), "=MOD(n,2)");
1129        assert_eq!(w("(xor (type A1) 1)"), "=XOR(TYPE(A1),1)");
1130        assert_eq!(w("(row)"), "=ROW()");
1131        assert_eq!(w("\"=A1+1\""), "=\"=A1+1\"");
1132    }
1133
1134    #[test]
1135    fn the_writer_refuses_what_it_cannot_spell_by_name() {
1136        assert_eq!(w_err("tax-rate"), "vla-formula-atom-unspelled");
1137        assert_eq!(w_err("A1+B1"), "vla-formula-atom-unspelled");
1138        assert_eq!(w_err("#N/A"), "vla-formula-atom-unspelled");
1139        assert_eq!(w_err("(f :key 1)"), "vla-formula-atom-unspelled");
1140        assert_eq!(w_err("&HFF"), "vla-formula-numeral-unspelled");
1141        assert_eq!(w_err("1d5"), "vla-formula-numeral-unspelled");
1142        assert_eq!(w_err("+5"), "vla-formula-numeral-unspelled");
1143        assert_eq!(w_err("1e999"), "vla-formula-numeral-unspelled");
1144        assert_eq!(
1145            w_err("((lambda (x) x) 1)"),
1146            "vla-formula-head-not-a-function"
1147        );
1148        assert_eq!(w_err("(\"sum\" 1)"), "vla-formula-head-not-a-function");
1149        assert_eq!(w_err("(a-b 1)"), "vla-formula-head-not-a-function");
1150        assert_eq!(
1151            w_err("(_xlfn.xlookup 1)"),
1152            "vla-formula-head-not-a-function"
1153        );
1154        assert_eq!(w_err("(set! x 1)"), "vla-formula-head-refused");
1155        assert_eq!(w_err("(fijar! x 1)"), "vla-formula-head-refused");
1156        assert_eq!(w_err("(new x)"), "vla-formula-head-refused");
1157        assert_eq!(w_err("(if (> x 1) (then 2))"), "vla-formula-head-refused");
1158        assert_eq!(w_err("(+)"), "vla-formula-arity");
1159        assert_eq!(w_err("(* a)"), "vla-formula-arity");
1160        assert_eq!(w_err("(% a b)"), "vla-formula-arity");
1161        assert_eq!(w_err("(not a b)"), "vla-formula-arity");
1162        assert_eq!(w_err("(if c)"), "vla-formula-arity");
1163        assert_eq!(w_err("(mod a)"), "vla-formula-arity");
1164        assert_eq!(w_err("(lambda x)"), "vla-formula-arity");
1165        assert_eq!(w_err("(quote (1 2))"), "vla-formula-unsupported");
1166        assert_eq!(w_err("(error \"#SPILL!\")"), "vla-formula-unsupported");
1167        assert_eq!(w_err("A1#"), "vla-formula-unsupported");
1168        assert_eq!(w_err("(lambda ((x)) x)"), "vla-formula-lambda-params");
1169        assert_eq!(w_err("(lambda (A1) A1)"), "vla-formula-lambda-params");
1170        assert_eq!(w_err("(ref \"Data\" \"A1+1\")"), "vla-formula-ref-invalid");
1171        let long = format!("\"{}\"", "a".repeat(256));
1172        assert_eq!(w_err(&long), "vla-formula-text-too-long");
1173        assert_eq!(w(&format!("\"{}\"", "a".repeat(255))).len(), 258);
1174    }
1175
1176    #[test]
1177    fn the_reader_gives_the_canonical_expression() {
1178        assert_eq!(r("=5"), "5");
1179        assert_eq!(r("=1.50"), "1.50");
1180        assert_eq!(r("=-1"), "-1");
1181        assert_eq!(r("=--1"), "(- -1)");
1182        assert_eq!(r("=-(1)"), "-1");
1183        assert_eq!(r("=-5%"), "(- (% 5))");
1184        assert_eq!(r("=(-5)%"), "(% -5)");
1185        assert_eq!(r("=\"say \"\"hi\"\"\""), "\"say \\\"hi\\\"\"");
1186        assert_eq!(r("=true"), "true");
1187        assert_eq!(r("=#n/a"), "(error \"#N/A\")");
1188        assert_eq!(r("=a1"), "A1");
1189        assert_eq!(r("='Input'!B5"), "Input!B5");
1190        assert_eq!(r("='Q1 Data'!A1"), "(ref \"Q1 Data\" \"A1\")");
1191        assert_eq!(r("= A1 + 1"), "(+ A1 1)");
1192        assert_eq!(r("=1+2+3"), "(+ 1 2 3)");
1193        assert_eq!(r("=1+(2+3)"), "(+ 1 (+ 2 3))");
1194        assert_eq!(r("=(1+2)+3"), "(+ 1 2 3)");
1195        assert_eq!(r("=-a-b"), "(- (- a) b)");
1196        assert_eq!(r("=a<b<c"), "(< a b c)");
1197        assert_eq!(r("=sum(a1:b2)"), "(sum A1:B2)");
1198        assert_eq!(r("=LAMBDA(x,y,x+y)"), "(lambda (x y) (+ x y))");
1199        assert_eq!(r("=LET(x,1,x+1)"), "(let x 1 (+ x 1))");
1200        assert_eq!(r("=PI()"), "(pi)");
1201        assert_eq!(r("=#REF!"), "(error \"#REF!\")");
1202    }
1203
1204    #[test]
1205    fn the_reader_tells_malformed_from_unsupported() {
1206        for bad in [
1207            "=",
1208            "=1+",
1209            "=(1",
1210            "=)",
1211            "=\"abc",
1212            "=1 2",
1213            "=SUM(1,2",
1214            "=IF(1)",
1215            "=NOT(1,2)",
1216        ] {
1217            assert_eq!(r_err(bad), "vla-formula-read-malformed", "{bad}");
1218        }
1219        for valid in [
1220            "={1,2;3,4}",
1221            "=@A1:A3",
1222            "=A1 B1",
1223            "=SUM((A1,B2))",
1224            "=A1#",
1225            "=SUM(Sales[Amount])",
1226            "=[Rates.xlsx]Sheet1!A1",
1227            "=IF(A1,,B1)",
1228            "=_xlfn.XLOOKUP(1,A:A,B:B)",
1229            "=A1:INDEX(B:B,5)",
1230            "=#SPILL!",
1231            "=LAMBDA(x,x)(1)",
1232            "=Sheet1!#REF!",
1233            "=CALL(1)",
1234            "=REF(1)",
1235        ] {
1236            assert_eq!(r_err(valid), "vla-formula-read-unsupported", "{valid}");
1237        }
1238        let long = format!("=\"{}\"", "a".repeat(256));
1239        assert_eq!(r_err(&long), "vla-formula-read-malformed");
1240    }
1241
1242    /// F.7's test (VLA.4): no row of the head table is undeclared in a
1243    /// formula; the writer refuses every head the table refuses, by name;
1244    /// and every head it declares a formula's is written, or refused only
1245    /// for its shape or as outside this version (`quote`).
1246    #[test]
1247    fn every_head_of_the_table_is_declared_and_the_writer_holds_to_it() {
1248        for row in headtable::rows() {
1249            assert_ne!(
1250                row.formula,
1251                FormulaSupport::Undeclared,
1252                "{} is undeclared",
1253                row.symbol
1254            );
1255            let form = Form::list(vec![Form::sym(&row.symbol), Form::sym("1"), Form::sym("2")]);
1256            match (row.formula, write(&form)) {
1257                (FormulaSupport::Refused, Err(e)) => {
1258                    assert_eq!(e.id, "vla-formula-head-refused", "{}", row.symbol)
1259                }
1260                (FormulaSupport::Refused, Ok(t)) => panic!("{} written as {t}", row.symbol),
1261                (_, Ok(_)) => {}
1262                (_, Err(e)) => assert!(
1263                    matches!(
1264                        e.id.as_str(),
1265                        "vla-formula-arity"
1266                            | "vla-formula-unsupported"
1267                            | "vla-formula-lambda-params"
1268                    ),
1269                    "{}: {}",
1270                    row.symbol,
1271                    e.id
1272                ),
1273            }
1274        }
1275    }
1276
1277    #[test]
1278    fn writing_and_reading_are_inverse_on_the_canonical_forms() {
1279        for text in [
1280            "=IF(B11=1,Day.last!$B$3+$B$6,Hustle.last!B13)",
1281            "=Tally.last!B1+IF(AND(Input!B1=1,Input.last!B1=0),1,0)",
1282            "=INT(B15/60)&\":\"&IF(MOD(B15,60)=0,\"00\",\"30\")",
1283            "=(COLUMN()-Gate!$A2)^2<=Gate!$B2",
1284            "='Q1 Data'!A1+'2026'!B2",
1285            "=-(x^2)+-x^2",
1286            "=(-5)%+-5%",
1287            "=a-(b-c)",
1288            "=2^(3^2)",
1289        ] {
1290            let v = read(text).unwrap();
1291            assert_eq!(write(&v).unwrap(), text, "{text}");
1292            assert_eq!(read(&write(&v).unwrap()).unwrap(), v, "{text}");
1293        }
1294    }
1295}