Skip to main content

frazaro_core/english/
program.rs

1//! The program frame (PORT.6, slice 6e): VLA_SentenceEngine.bas's
2//! `EnglishToVla`, which turns a whole program into VLA text - Define's
3//! constants, `Use library`, the sheet-change and button-click handlers,
4//! the To definitions in their four shapes (a sub with its `with` parameters,
5//! `To get <name>:`, `To get <name> using ...:`, `To <name> of <p>:`), the
6//! main sub, and the frame around them (`vla-step`, `vla-problem`, the step
7//! table `BuildStepInfra` writes, the libraries and the carried macros) -
8//! with `BuildSub`, `ParseDefine`, `ParseUseLibrary`, `CheckDupAction`,
9//! `ValidateActionCalls` and G-PROLOG's three range lints.
10
11use super::grammar::Grammar;
12use super::matcher::{add_keyed, has_keyed, is_num_tok, is_str_tok, vla_string_lit, Parser};
13use super::prolog::ref_in_range;
14use super::tokenize::{line_suf, tokenize, PARA_TOK};
15use crate::intrinsics::fold;
16use crate::messages::{raise, Refusal};
17
18/// What `EnglishToVla` leaves beside its text: the lint warnings it added,
19/// LX.14's masking notes (`EnglishLastNotes`), IN.7's click handlers
20/// (`EnglishClickHandlerNames`/`Procs`) and the step texts.
21#[derive(Clone, Debug, Default)]
22pub struct Translation {
23    pub vla: String,
24    pub lint_warnings: Vec<String>,
25    pub mask_notes: Vec<String>,
26    pub click_handlers: Vec<(String, String)>,
27    pub step_texts: Vec<String>,
28}
29
30/// A refusal with the line `EnglishLastErrorLine()` reports after it: the
31/// tokenizer's, the sentence's, or a lint's cell line; 0 when none is known.
32#[derive(Clone, Debug, PartialEq)]
33pub struct RefusalAtLine {
34    pub refusal: Refusal,
35    pub line: u32,
36}
37
38/// VBA `ClickHandlerSlug`: a caption as an identifier, every other
39/// character an underscore.
40pub fn click_handler_slug(caption: &str) -> String {
41    caption
42        .chars()
43        .map(|c| {
44            if c.is_ascii_alphanumeric() || c == '_' {
45                c
46            } else {
47                '_'
48            }
49        })
50        .collect()
51}
52
53/// VBA `JoinNames`: ", "-joined, or "(none)".
54fn join_names(names: &[String]) -> String {
55    if names.is_empty() {
56        "(none)".to_string()
57    } else {
58        names.join(", ")
59    }
60}
61
62impl Grammar {
63    /// VBA `EnglishToVla`: a program's VLA text, with step tracking on.
64    pub fn translate_program(&self, text: &str) -> Result<Translation, Refusal> {
65        let mut parser = Parser::new(self);
66        parser.translate_program(text)
67    }
68
69    /// `EnglishToVla` with `EnglishLastErrorLine()` beside a refusal: the
70    /// line the IDE attributes it to.
71    pub fn translate_program_at(&self, text: &str) -> Result<Translation, RefusalAtLine> {
72        let mut parser = Parser::new(self);
73        let out = parser.translate_program(text);
74        out.map_err(|refusal| RefusalAtLine {
75            refusal,
76            line: parser.err_line,
77        })
78    }
79
80    /// VBA `EnglishToVba`: `VlaTranspile(EnglishToVla(text))`, the carried
81    /// macros riding inside the text.
82    pub fn translate_program_to_vba(&self, text: &str) -> Result<String, Refusal> {
83        let t = self.translate_program(text)?;
84        crate::emit::compile(&t.vla, &self.prelude)
85    }
86}
87
88impl<'g> Parser<'g> {
89    /// `EnglishToVla`'s own state, fresh for this translation.
90    fn reset_program_state(&mut self) {
91        self.step_count = 0;
92        self.step_texts.clear();
93        self.err_line = 0;
94        self.act_names.clear();
95        self.act_params.clear();
96        self.act_req.clear();
97        self.calls.clear();
98        self.rule_cells.clear();
99        self.asks.clear();
100        self.alias_names.clear();
101        self.alias_defs.clear();
102        self.loop_stack.clear();
103        self.try_count = 0;
104        self.in_func_def = false;
105        self.in_recovery = false;
106        self.saw_sheet_change_event = false;
107        self.click_captions.clear();
108        self.click_slugs.clear();
109        self.click_names.clear();
110        self.click_procs.clear();
111        self.user_fn_of.clear();
112        self.user_fn_nullary.clear();
113        self.masked_of.clear();
114        self.mask_notes.clear();
115        self.fn_act_names.clear();
116        self.fn_using_names.clear();
117        self.fn_using_req.clear();
118        self.assigned_lines.clear();
119        self.cur_line = 0;
120        self.dict_names.clear();
121        self.include_lines.clear();
122        self.include_seen.clear();
123        self.declared.clear();
124        self.assigned.clear();
125        self.lint_warnings.clear();
126        self.in_program = true;
127    }
128
129    /// VBA `EnglishToVla`.
130    pub fn translate_program(&mut self, text: &str) -> Result<Translation, Refusal> {
131        self.src_lines = text
132            .replace("\r\n", "\n")
133            .split('\n')
134            .map(str::to_string)
135            .collect(); // S3.2
136        let toks = match tokenize(text) {
137            Ok(t) => t,
138            Err(e) => {
139                self.err_line = e.line;
140                return Err(e.refusal);
141            }
142        };
143        self.set_tokens(toks);
144        self.reset_program_state();
145        let mut pos = 1usize;
146        let mut out_parts = String::new();
147        let mut main_stmts: Vec<String> = Vec::new();
148        while pos <= self.toks.count() {
149            self.claim.clear(); // claim tracking is per top-level sentence
150            self.cur_line = self.toks.line_at(pos);
151            if self.tok(pos) == PARA_TOK {
152                pos += 1; // blank lines at top level are decoration
153                continue;
154            }
155            let w = self.peek_word(pos);
156            if w == "define" {
157                self.parse_define(&mut pos)?;
158            } else if (w == "use" || w == "import")
159                && (self.word_at(pos + 1) == "library" || self.word_at(pos + 1) == "code")
160            {
161                self.parse_use_library(&mut pos)?;
162            } else if self.at_sheet_change_event(pos) {
163                pos += 4; // when sheet changes :
164                if self.saw_sheet_change_event {
165                    let loc = self.line_tag(pos - 1);
166                    return Err(raise("english-sheet-change-dup", &[("loc", &loc)]));
167                }
168                self.saw_sheet_change_event = true;
169                let sub =
170                    self.define_sub(&mut pos, "on:sheet-change", "", false, false, Vec::new())?;
171                out_parts.push_str(&sub);
172            } else if self.at_button_click_event(pos) {
173                let btn_caption = self.tok(pos + 1)[1..].to_string(); // strip the token's quote
174                pos += 5; // when "caption" is clicked :
175                if has_keyed(&self.click_captions, &btn_caption) {
176                    let loc = self.line_tag(pos - 1);
177                    return Err(raise(
178                        "english-click-handler-dup",
179                        &[("caption", &btn_caption), ("loc", &loc)],
180                    ));
181                }
182                add_keyed(&mut self.click_captions, &btn_caption);
183                let click_slug = click_handler_slug(&btn_caption);
184                if let Some(prior) = self.click_slugs.get(&fold(&click_slug)) {
185                    let prior = prior.clone();
186                    let loc = self.line_tag(pos - 1);
187                    return Err(raise(
188                        "english-click-handler-slug-collision",
189                        &[("a", &btn_caption), ("b", &prior), ("loc", &loc)],
190                    ));
191                }
192                self.click_slugs
193                    .insert(fold(&click_slug), btn_caption.clone());
194                let proc_name = format!("on:click:{click_slug}");
195                let sub = self.define_sub(&mut pos, &proc_name, "", false, false, Vec::new())?;
196                out_parts.push_str(&sub);
197                self.click_names.push(btn_caption);
198                self.click_procs.push(proc_name);
199            } else if w == "to" {
200                pos += 1;
201                self.parse_to_definition(&mut pos, &mut out_parts)?;
202            } else {
203                let s = self.parse_tracked(&mut pos, 1)?;
204                main_stmts.push(s);
205            }
206        }
207        self.validate_action_calls()?;
208        self.validate_relation_table_names()?;
209        self.validate_first_match_cells()?;
210        self.validate_question_ranges()?;
211        if !main_stmts.is_empty() {
212            let s = self.build_sub("main", &main_stmts, "", false)?;
213            out_parts.push_str(&s);
214            out_parts.push_str("\r\n");
215        }
216        if self.step_tracking && self.step_count > 0 {
217            out_parts = format!(
218                "(dim vla-step Long)\r\n\r\n{out_parts}{}",
219                self.build_step_infra()
220            );
221        }
222        if self.try_count > 0 {
223            out_parts = format!("(dim vla-problem String)\r\n\r\n{out_parts}");
224        }
225        if !self.alias_defs.is_empty() {
226            out_parts = format!("{}\r\n{out_parts}", self.alias_defs);
227        }
228        if !self.include_lines.is_empty() {
229            out_parts.push_str("\r\n; ---- libraries (G12) ----\r\n");
230            out_parts.push_str(&self.include_lines);
231        }
232        if self.g.vocab_macro_count() > 0 {
233            out_parts.push_str("\r\n; ---- carried by phrasebooks (L4) ----\r\n");
234            out_parts.push_str(&self.g.vocab_macros_text());
235            out_parts.push_str("\r\n\r\n");
236        }
237        self.in_program = false;
238        Ok(Translation {
239            vla: out_parts,
240            lint_warnings: self.lint_warnings.clone(),
241            mask_notes: self.mask_notes.clone(),
242            click_handlers: self
243                .click_names
244                .iter()
245                .cloned()
246                .zip(self.click_procs.iter().cloned())
247                .collect(),
248            step_texts: self.step_texts.clone(),
249        })
250    }
251
252    /// A sub or function body in its own scope of declared and assigned
253    /// names (the reference's `Set mDeclared = New Collection` and the
254    /// restore to the main scope), built with `BuildSub` and followed by a
255    /// blank line.
256    fn define_sub(
257        &mut self,
258        pos: &mut usize,
259        name: &str,
260        param_spec: &str,
261        as_func: bool,
262        func_body: bool,
263        declared: Vec<String>,
264    ) -> Result<String, Refusal> {
265        let saved_decl = std::mem::replace(&mut self.declared, declared);
266        let saved_asgn = std::mem::take(&mut self.assigned);
267        let saved_func = self.in_func_def;
268        self.in_func_def = func_body;
269        let body = self.parse_block(pos);
270        self.in_func_def = saved_func;
271        let built = match body {
272            Ok(b) => self.build_sub(name, &b, param_spec, as_func),
273            Err(e) => Err(e),
274        };
275        self.declared = saved_decl;
276        self.assigned = saved_asgn;
277        Ok(format!("{}\r\n", built?))
278    }
279
280    /// `EnglishToVla`'s To arm: an action, in one of its four shapes.
281    fn parse_to_definition(
282        &mut self,
283        pos: &mut usize,
284        out_parts: &mut String,
285    ) -> Result<(), Refusal> {
286        let mut get_nullary = false;
287        if self.peek_word(*pos) == "get" && !self.word_at(*pos + 1).is_empty() {
288            if self.tok(*pos + 2) == "using" || self.tok(*pos + 2) == "of" {
289                *pos += 1;
290            } else if self.tok(*pos + 2) == ":" {
291                *pos += 1;
292                get_nullary = true;
293            }
294        }
295        let sub_name = self.expect_word(pos, "an action name after 'To'")?;
296        self.check_name(&sub_name)?;
297        self.check_dup_action(&sub_name, *pos - 1)?;
298        if get_nullary {
299            self.expect_tok(pos, ":", &format!("':' after 'To get {sub_name}'"))?;
300            self.mask_vocab_of_word(&sub_name, false);
301            self.user_fn_nullary
302                .insert(fold(&sub_name), sub_name.clone());
303            self.fn_act_names.push(fold(&sub_name));
304            let s = self.define_sub(pos, &sub_name, "", true, true, Vec::new())?;
305            out_parts.push_str(&s);
306        } else if self.tok(*pos) == "using" {
307            *pos += 1;
308            let mut param_spec = String::new();
309            let mut sig_params: Vec<String> = Vec::new();
310            let mut sig_req: Vec<bool> = Vec::new();
311            let mut declared: Vec<String> = Vec::new();
312            let mut saw_default = false;
313            loop {
314                let p_name = self.expect_word(pos, "a parameter name after 'using'")?;
315                if (self.fn_of_target(&p_name).is_some()
316                    || self.fn_nullary_target(&p_name).is_some())
317                    && !self.g.is_maskable_fn_word(&p_name)
318                {
319                    let loc = self.line_tag(*pos - 1);
320                    return Err(raise(
321                        "english-param-name-taken",
322                        &[("name", &p_name), ("loc", &loc)],
323                    ));
324                }
325                let mut p_def = String::new();
326                if self.tok(*pos) == "of" {
327                    *pos += 1;
328                    p_def = self.parse_expr_req(pos)?;
329                    self.refuse_cell_shaped_read(&p_def, self.cur_line)?; // LX.13: a default is a value
330                }
331                if !p_def.is_empty() {
332                    saw_default = true;
333                } else if saw_default {
334                    let loc = self.line_tag(*pos - 1);
335                    return Err(raise(
336                        "english-param-missing-default",
337                        &[("name", &p_name), ("loc", &loc)],
338                    ));
339                }
340                if !param_spec.is_empty() {
341                    param_spec.push(' ');
342                }
343                if !p_def.is_empty() {
344                    param_spec.push_str(&format!("(optional {p_name} Variant {p_def})"));
345                } else {
346                    param_spec.push_str(&format!("({p_name} Variant)"));
347                }
348                self.check_name(&p_name)?; // MarkDeclared
349                add_keyed(&mut declared, &p_name);
350                sig_params.push(fold(&p_name));
351                sig_req.push(p_def.is_empty());
352                if self.tok(*pos) == "and" {
353                    *pos += 1;
354                } else {
355                    break;
356                }
357            }
358            self.expect_tok(pos, ":", "':' after the 'using' parameters")?;
359            self.fn_using_names.insert(fold(&sub_name), sig_params);
360            self.fn_using_req.insert(fold(&sub_name), sig_req);
361            self.fn_act_names.push(fold(&sub_name));
362            let s = self.define_sub(pos, &sub_name, &param_spec, true, true, declared)?;
363            out_parts.push_str(&s);
364        } else if self.tok(*pos) == "of" {
365            *pos += 1;
366            let p_name = self.expect_word(pos, "a parameter name after 'of'")?;
367            self.expect_tok(pos, ":", &format!("':' after 'To {sub_name} of {p_name}'"))?;
368            self.check_name(&p_name)?; // the parameter is never auto-dimmed
369            let declared = vec![p_name.clone()];
370            self.mask_vocab_of_word(&sub_name, true); // LX.14: a vocabulary's own word, kept to give back
371            self.user_fn_of.insert(fold(&sub_name), sub_name.clone());
372            self.fn_act_names.push(fold(&sub_name));
373            let s = self.define_sub(
374                pos,
375                &sub_name,
376                &format!("({p_name} Variant)"),
377                true,
378                true,
379                declared,
380            )?;
381            out_parts.push_str(&s);
382        } else {
383            let mut param_spec = String::new();
384            let mut sig_params: Vec<String> = Vec::new();
385            let mut sig_req: Vec<bool> = Vec::new();
386            let mut declared: Vec<String> = Vec::new();
387            let mut saw_default = false;
388            if self.tok(*pos) == "," {
389                *pos += 1;
390                self.expect_word_is(pos, "with")?;
391                loop {
392                    let mut p_opt = false;
393                    if self.tok(*pos) == "optional" {
394                        p_opt = true;
395                        *pos += 1;
396                    }
397                    let p_name = self.expect_word(pos, "a parameter name after 'with'")?;
398                    let mut p_def = String::new();
399                    if self.tok(*pos) == "of" {
400                        *pos += 1;
401                        p_def = self.parse_expr_req(pos)?;
402                        self.refuse_cell_shaped_read(&p_def, self.cur_line)?; // LX.13
403                    }
404                    if !param_spec.is_empty() {
405                        param_spec.push(' ');
406                    }
407                    if p_opt || !p_def.is_empty() {
408                        saw_default = true;
409                    } else if saw_default {
410                        let loc = self.line_tag(*pos - 1);
411                        return Err(raise(
412                            "english-param-required-after-optional",
413                            &[("name", &p_name), ("loc", &loc)],
414                        ));
415                    }
416                    if p_opt || !p_def.is_empty() {
417                        let def = if p_def.is_empty() {
418                            String::new()
419                        } else {
420                            format!(" {p_def}")
421                        };
422                        param_spec.push_str(&format!("(optional {p_name} Variant{def})"));
423                    } else {
424                        param_spec.push_str(&format!("({p_name} Variant)"));
425                    }
426                    self.check_name(&p_name)?; // params are never auto-dimmed
427                    add_keyed(&mut declared, &p_name);
428                    sig_params.push(fold(&p_name));
429                    sig_req.push(!(p_opt || !p_def.is_empty()));
430                    if self.tok(*pos) == "and" {
431                        *pos += 1;
432                    } else {
433                        break;
434                    }
435                }
436            }
437            self.expect_tok(pos, ":", "':' after the action name (or its 'with' clause)")?;
438            let s = self.define_sub(pos, &sub_name, &param_spec, false, false, declared)?;
439            out_parts.push_str(&s);
440            self.act_names.push(fold(&sub_name));
441            self.act_params.push(sig_params);
442            self.act_req.push(sig_req);
443        }
444        Ok(())
445    }
446
447    /// VBA `BuildSub`: the sub or function, its auto-dimmed names, and the
448    /// error handler when steps are tracked.
449    fn build_sub(
450        &mut self,
451        name: &str,
452        stmts: &[String],
453        param_spec: &str,
454        as_func: bool,
455    ) -> Result<String, Refusal> {
456        let instrument = self.step_tracking && !stmts.is_empty();
457        let mut sb = format!(
458            "({} {name} ({param_spec})\r\n",
459            if as_func { "function" } else { "sub" }
460        );
461        for e in self.assigned.clone() {
462            if has_keyed(&self.declared, &e) {
463                continue;
464            }
465            if has_keyed(&self.alias_names, &e) {
466                let line = self.assigned_lines.get(&fold(&e)).copied();
467                let loc = match line {
468                    Some(l) => {
469                        self.err_line = l;
470                        line_suf(l)
471                    }
472                    None => String::new(),
473                };
474                return Err(raise(
475                    "english-define-value-immutable",
476                    &[("name", &e), ("loc", &loc)],
477                ));
478            }
479            sb.push_str(&format!("  (dim {e})\r\n"));
480        }
481        if instrument {
482            sb.push_str("  (on-error goto vla-fail)\r\n");
483        }
484        for e in stmts {
485            sb.push_str(e);
486            sb.push_str("\r\n");
487        }
488        if instrument {
489            sb.push_str(if as_func {
490                "  (exit-function)\r\n"
491            } else {
492                "  (exit-sub)\r\n"
493            });
494            sb.push_str("  (label vla-fail)\r\n");
495            sb.push_str("  (vla-report-error)\r\n");
496        }
497        // StackCloser: the closing paren on the last line, then CRLF.
498        Ok(format!("{})\r\n", sb.trim_end_matches(['\r', '\n'])))
499    }
500
501    /// VBA `BuildStepInfra`: the error reporter and the step-text lookup.
502    fn build_step_infra(&self) -> String {
503        let mut sb = String::from("(sub vla-report-error ()\r\n");
504        sb.push_str("  (vlareportstop vla-step (vla-step-text vla-step) err.description))\r\n\r\n");
505        sb.push_str("(function vla-step-text ((byval n Long)) String\r\n");
506        sb.push_str("  (select n\r\n");
507        for (i0, t) in self.step_texts.iter().enumerate() {
508            sb.push_str(&format!(
509                "    (case ({}) (return {}))\r\n",
510                i0 + 1,
511                vla_string_lit(t)
512            ));
513        }
514        sb.push_str("    (case-else (return \"an unknown step\"))))\r\n");
515        sb
516    }
517
518    /// VBA `ParseDefine`: `Define <name> as <fixed value>.`, a constant.
519    fn parse_define(&mut self, pos: &mut usize) -> Result<(), Refusal> {
520        *pos += 1; // past "define"
521        let name = self.expect_word(pos, "a name after 'Define'")?;
522        self.check_name(&name)?;
523        self.expect_word_is(pos, "as")?;
524        let t = self.tok(*pos).to_string();
525        let val = if is_str_tok(&t) {
526            *pos += 1;
527            vla_string_lit(&t[1..])
528        } else if is_num_tok(&t)
529            || (!self.word_at(*pos).is_empty() && has_keyed(&self.alias_names, &t))
530        {
531            // a number, or an earlier alias
532            *pos += 1;
533            t
534        } else {
535            let context = self.sentence_context(*pos);
536            return Err(raise(
537                "english-define-needs-fixed-value",
538                &[("context", &context)],
539            ));
540        };
541        self.expect_tok(pos, ".", "'.' at the end of the sentence")?;
542        add_keyed(&mut self.alias_names, &name);
543        self.alias_defs
544            .push_str(&format!("(const {name} {val})\r\n"));
545        Ok(())
546    }
547
548    /// VBA `ParseUseLibrary` (G12): `Use library "name".`, spliced once.
549    fn parse_use_library(&mut self, pos: &mut usize) -> Result<(), Refusal> {
550        *pos += 2; // use|import library|code
551        if self.word_at(*pos) == "at" || self.word_at(*pos) == "from" {
552            *pos += 1;
553        }
554        let ftok = self.tok(*pos).to_string();
555        if !is_str_tok(&ftok) {
556            let context = self.sentence_context(*pos);
557            let loc = self.line_tag(*pos);
558            return Err(raise(
559                "english-library-name-not-quoted",
560                &[("context", &context), ("loc", &loc)],
561            ));
562        }
563        *pos += 1;
564        let fname = &ftok[1..];
565        self.expect_tok(pos, ".", "'.' after the library name")?;
566        let k = fold(fname);
567        if !self.include_seen.insert(k) {
568            return Ok(()); // seen before - splice once
569        }
570        self.include_lines
571            .push_str(&format!("(include \"{fname}\")\r\n"));
572        Ok(())
573    }
574
575    /// VBA `CheckDupAction` (B4): one name, one definition.
576    fn check_dup_action(&mut self, name: &str, name_pos: usize) -> Result<(), Refusal> {
577        let n = fold(name);
578        // A sub's name, a value action's name (every `using` action is one
579        // of those too), or a using action's: taken.
580        if self.act_names.iter().any(|e| e == &n)
581            || self.fn_act_names.iter().any(|e| e == &n)
582            || self.is_using_fn(&n)
583        {
584            let loc = self.line_tag(name_pos);
585            return Err(raise(
586                "english-action-name-taken",
587                &[("name", name), ("loc", &loc)],
588            ));
589        }
590        if (self.fn_of_target(&n).is_some() || self.fn_nullary_target(&n).is_some())
591            && !self.g.is_maskable_fn_word(&n)
592        {
593            let loc = self.line_tag(name_pos);
594            return Err(raise(
595                "english-action-name-means-something",
596                &[("name", name), ("loc", &loc)],
597            ));
598        }
599        Ok(())
600    }
601
602    /// VBA `ValidateActionCalls`: every recorded call against the actions
603    /// defined; an undefined action is a warning, a wrong argument a refusal.
604    fn validate_action_calls(&mut self) -> Result<(), Refusal> {
605        let calls = self.calls.clone();
606        for call in &calls {
607            let Some(idx) = self.act_names.iter().position(|a| a == &call.name) else {
608                self.lint_warnings.push(format!(
609                    "'{}' calls '{}', which is not defined in this file - if it is not defined elsewhere, running will fail",
610                    call.text, call.name
611                ));
612                continue;
613            };
614            let params = self.act_params[idx].clone();
615            let reqs = self.act_req[idx].clone();
616            for a in &call.args {
617                if !params.contains(a) {
618                    self.err_line = call.line;
619                    return Err(raise(
620                        "english-call-unknown-param",
621                        &[
622                            ("call", &call.text),
623                            ("action", &call.name),
624                            ("param", a),
625                            ("list", &join_names(&params)),
626                            ("loc", &line_suf(call.line)),
627                        ],
628                    ));
629                }
630            }
631            for (j, p) in params.iter().enumerate() {
632                if reqs[j] && !call.args.contains(p) {
633                    self.err_line = call.line;
634                    return Err(raise(
635                        "english-call-missing-param",
636                        &[
637                            ("call", &call.text),
638                            ("action", &call.name),
639                            ("param", p),
640                            ("loc", &line_suf(call.line)),
641                        ],
642                    ));
643                }
644            }
645        }
646        Ok(())
647    }
648
649    /// VBA `ValidateRelationTableNames` (G-PROLOG slice 3): a relation one
650    /// cell defines may not be a table another cell reads.
651    fn validate_relation_table_names(&mut self) -> Result<(), Refusal> {
652        let cells = self.rule_cells.clone();
653        for (ci, c) in cells.iter().enumerate() {
654            for (cj, other) in cells.iter().enumerate() {
655                if cj == ci {
656                    continue;
657                }
658                if other.tables.iter().any(|tb| tb == &c.rel) {
659                    self.err_line = c.line;
660                    return Err(raise(
661                        "english-program-relation-names-table",
662                        &[
663                            ("relation", &c.rel),
664                            ("cell", &c.addr.to_uppercase()),
665                            ("tablecell", &other.addr.to_uppercase()),
666                            ("loc", &line_suf(c.line)),
667                        ],
668                    ));
669                }
670            }
671        }
672        Ok(())
673    }
674
675    /// VBA `ValidateFirstMatchCells`: a first-match relation lives in one cell.
676    fn validate_first_match_cells(&mut self) -> Result<(), Refusal> {
677        let cells = self.rule_cells.clone();
678        for (ci, c) in cells.iter().enumerate() {
679            if !c.first {
680                continue;
681            }
682            for (cj, other) in cells.iter().enumerate() {
683                if cj != ci && other.rel == c.rel {
684                    self.err_line = other.line;
685                    return Err(raise(
686                        "english-program-first-match-split",
687                        &[
688                            ("relation", &c.rel),
689                            ("cell", &c.addr.to_uppercase()),
690                            ("othercell", &other.addr.to_uppercase()),
691                            ("loc", &line_suf(other.line)),
692                        ],
693                    ));
694                }
695            }
696        }
697        Ok(())
698    }
699
700    /// VBA `ValidateQuestionRanges`: every cell a question's relations
701    /// reach must sit in the rules range it names, when the cells can be
702    /// placed at all.
703    fn validate_question_ranges(&mut self) -> Result<(), Refusal> {
704        let asks = self.asks.clone();
705        let cells = self.rule_cells.clone();
706        for ask in &asks {
707            let mut reach: Vec<String> = vec![ask.rel.clone()];
708            let mut k = 0;
709            while k < reach.len() {
710                let nm = reach[k].clone();
711                let mut cells_of: Vec<String> = Vec::new();
712                let mut total = 0;
713                let mut inside = 0;
714                let mut missing = String::new();
715                let mut unplaced = false;
716                for c in cells.iter().filter(|c| c.rel == nm) {
717                    for bn in &c.bodies {
718                        if !reach.contains(bn) {
719                            reach.push(bn.clone());
720                        }
721                    }
722                    if !cells_of.contains(&c.addr) {
723                        cells_of.push(c.addr.clone());
724                        total += 1;
725                        match ref_in_range(&c.addr, &ask.range) {
726                            1 => inside += 1,
727                            0 => {
728                                if missing.is_empty() {
729                                    missing = c.addr.clone();
730                                }
731                            }
732                            _ => unplaced = true,
733                        }
734                    }
735                }
736                if !unplaced && inside > 0 && inside < total {
737                    self.err_line = ask.line;
738                    return Err(raise(
739                        "english-question-range-splits-relation",
740                        &[
741                            ("relation", &nm),
742                            ("cell", &missing.to_uppercase()),
743                            ("range", &ask.range.to_uppercase()),
744                            ("loc", &line_suf(ask.line)),
745                        ],
746                    ));
747                }
748                k += 1;
749            }
750        }
751        Ok(())
752    }
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    const PRELUDE: &str = include_str!("../../../scripts/prelude.vla");
760    const ENGLISH: &str = include_str!("../../../scripts/polyglotta/english.vla");
761    const PROGRAM: &str = include_str!("../../../scripts/instructions.txt");
762    const GOLDEN: &str = include_str!("../../../scripts/instructions_golden.vla");
763
764    fn english() -> Grammar {
765        let mut g = Grammar::new(PRELUDE);
766        g.load_vocabulary_text(ENGLISH, "english.vla").unwrap();
767        g
768    }
769
770    fn normalized(s: &str) -> String {
771        s.replace("\r\n", "\n").trim_end_matches('\n').to_string()
772    }
773
774    /// The treaty's oracle 1: `instructions.txt` to `instructions_golden.vla`
775    /// less its stamp line, after the treaty's normalization.
776    #[test]
777    fn the_translate_golden_is_reproduced() {
778        let g = english();
779        let t = g
780            .translate_program(PROGRAM)
781            .unwrap_or_else(|e| panic!("{}", e.text));
782        let want = normalized(GOLDEN.split_once('\n').map(|(_, rest)| rest).unwrap_or(""));
783        let got = normalized(&t.vla);
784        if got != want {
785            let at = got
786                .chars()
787                .zip(want.chars())
788                .position(|(a, b)| a != b)
789                .unwrap_or(got.chars().count().min(want.chars().count()));
790            let want_line = want.chars().take(at).filter(|c| *c == '\n').count() + 1;
791            let ctx = |s: &str| {
792                s.chars()
793                    .skip(at.saturating_sub(80))
794                    .take(240)
795                    .collect::<String>()
796            };
797            panic!(
798                "differs at char {at} (golden line {want_line}) of {}:\n  want: {}\n  got:  {}",
799                want.chars().count(),
800                ctx(&want),
801                ctx(&got)
802            );
803        }
804        assert!(t.lint_warnings.is_empty(), "{:?}", t.lint_warnings);
805    }
806
807    #[test]
808    fn the_frame_and_the_definitions_are_the_reference_s() {
809        let g = Grammar::new(PRELUDE);
810        let t = g
811            .translate_program("Define hot-pink as \"#FF69B4\".\n\nTo stamp, with row-number of 1 and value of \"ok\":\nSet x to value.\n\nTo tax of amount:\nGive back amount times 0.08.\n\nTo get vat-rate:\nGive back 0.2.\n\nTo get commission using sale of 1000 and rate of 5%:\nGive back sale times rate.\n\nSet total to tax of 100 plus vat-rate plus commission using sale of 10.\nStamp with row-number of 2.\n")
812            .unwrap_or_else(|e| panic!("{}", e.text));
813        let want = "(const hot-pink \"#FF69B4\")\r\n\r\n(dim vla-step Long)\r\n\r\n\
814(sub stamp ((optional row-number Variant 1) (optional value Variant \"ok\"))\r\n  (dim x)\r\n  (on-error goto vla-fail)\r\n    (set! vla-step 1)\r\n    (if (vlatraceon) (then (vlatracestep 1 (vla-step-text 1))))\r\n    (at-line 4\r\n    (set! x value))\r\n  (exit-sub)\r\n  (label vla-fail)\r\n  (vla-report-error))\r\n\r\n\
815(function tax ((amount Variant))\r\n  (on-error goto vla-fail)\r\n    (set! vla-step 2)\r\n    (if (vlatraceon) (then (vlatracestep 2 (vla-step-text 2))))\r\n    (at-line 7\r\n    (return (* amount 0.08)))\r\n  (exit-function)\r\n  (label vla-fail)\r\n  (vla-report-error))\r\n\r\n\
816(function vat-rate ()\r\n  (on-error goto vla-fail)\r\n    (set! vla-step 3)\r\n    (if (vlatraceon) (then (vlatracestep 3 (vla-step-text 3))))\r\n    (at-line 10\r\n    (return 0.2))\r\n  (exit-function)\r\n  (label vla-fail)\r\n  (vla-report-error))\r\n\r\n\
817(function commission ((optional sale Variant 1000) (optional rate Variant (/ 5 100)))\r\n  (on-error goto vla-fail)\r\n    (set! vla-step 4)\r\n    (if (vlatraceon) (then (vlatracestep 4 (vla-step-text 4))))\r\n    (at-line 13\r\n    (return (* sale rate)))\r\n  (exit-function)\r\n  (label vla-fail)\r\n  (vla-report-error))\r\n\r\n\
818(sub main ()\r\n  (dim total)\r\n  (on-error goto vla-fail)\r\n  (set! vla-step 5)\r\n  (if (vlatraceon) (then (vlatracestep 5 (vla-step-text 5))))\r\n  (at-line 15\r\n  (set! total (+ (+ (tax 100) (vat-rate)) (commission :sale 10))))\r\n  (set! vla-step 6)\r\n  (if (vlatraceon) (then (vlatracestep 6 (vla-step-text 6))))\r\n  (at-line 16\r\n  (stamp :row-number 2))\r\n  (exit-sub)\r\n  (label vla-fail)\r\n  (vla-report-error))\r\n\r\n\
819(sub vla-report-error ()\r\n  (vlareportstop vla-step (vla-step-text vla-step) err.description))\r\n\r\n(function vla-step-text ((byval n Long)) String\r\n  (select n\r\n    (case (1) (return \"Set x to value. [line 4]\"))\r\n    (case (2) (return \"Give back amount times 0.08. [line 7]\"))\r\n    (case (3) (return \"Give back 0.2. [line 10]\"))\r\n    (case (4) (return \"Give back sale times rate. [line 13]\"))\r\n    (case (5) (return \"Set total to tax of 100 plus vat-rate plus commission using sale of 10. [line 15]\"))\r\n    (case (6) (return \"Stamp with row-number of 2. [line 16]\"))\r\n    (case-else (return \"an unknown step\"))))\r\n";
820        assert_eq!(t.vla, want);
821    }
822
823    #[test]
824    fn the_program_level_refusals_are_the_reference_s() {
825        let g = Grammar::new(PRELUDE);
826        let id = |text: &str| g.translate_program(text).unwrap_err().id;
827        assert_eq!(
828            id("To greet:\nLog 1.\n\nTo greet:\nLog 2.\n"),
829            "english-action-name-taken"
830        );
831        assert_eq!(
832            id("To length of x:\nGive back 1.\n"),
833            "english-action-name-means-something"
834        );
835        assert_eq!(
836            id("To greet, with a of 1 and b:\nLog 1.\n"),
837            "english-param-required-after-optional"
838        );
839        assert_eq!(
840            id("To get f using a of 1 and b:\nGive back 1.\n"),
841            "english-param-missing-default"
842        );
843        assert_eq!(
844            id("Greet with x of 1.\n\nTo greet, with y of 1:\nLog y.\n"),
845            "english-call-unknown-param"
846        );
847        assert_eq!(
848            id("Greet.\n\nTo greet, with y:\nLog y.\n"),
849            "english-call-missing-param"
850        );
851        assert_eq!(
852            id("Define x as 5.\nSet x to 6.\n"),
853            "english-define-value-immutable"
854        );
855        assert_eq!(
856            id("Define x as total.\n"),
857            "english-define-needs-fixed-value"
858        );
859        assert_eq!(id("Use library x.\n"), "english-library-name-not-quoted");
860        assert_eq!(
861            id("When the sheet changes:\nLog 1.\n\nWhen the sheet changes:\nLog 2.\n"),
862            "english-sheet-change-dup"
863        );
864        assert_eq!(
865            id("When \"Go\" is clicked:\nLog 1.\n\nWhen \"Go\" is clicked:\nLog 2.\n"),
866            "english-click-handler-dup"
867        );
868        assert_eq!(
869            id("When \"Go!\" is clicked:\nLog 1.\n\nWhen \"Go?\" is clicked:\nLog 2.\n"),
870            "english-click-handler-slug-collision"
871        );
872        assert_eq!(
873            id("When Go is clicked:\nLog 1.\n"),
874            "english-click-handler-needs-quotes"
875        );
876        assert_eq!(
877            id("When \"Go\" is clikced:\nLog 1.\n"),
878            "english-clicked-misspelled"
879        );
880        let t = g.translate_program("Greet.\n").unwrap();
881        // The call's sentence is rendered from its folded tokens, as the
882        // reference renders it.
883        assert_eq!(t.lint_warnings, vec!["'greet.' calls 'greet', which is not defined in this file - if it is not defined elsewhere, running will fail".to_string()]);
884        let t = g.translate_program("Use library \"alien.vla\".\nUse library \"alien.vla\".\nWhen \"Go\" is clicked:\nLog 1.\n").unwrap();
885        assert!(
886            t.vla
887                .contains("; ---- libraries (G12) ----\r\n(include \"alien.vla\")\r\n"),
888            "{}",
889            t.vla
890        );
891        assert_eq!(
892            t.click_handlers,
893            vec![("Go".to_string(), "on:click:Go".to_string())]
894        );
895        assert!(
896            t.vla
897                .starts_with("(dim vla-step Long)\r\n\r\n(sub on:click:Go ()\r\n"),
898            "{}",
899            t.vla
900        );
901    }
902}