1use 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#[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#[derive(Clone, Debug, PartialEq)]
33pub struct RefusalAtLine {
34 pub refusal: Refusal,
35 pub line: u32,
36}
37
38pub 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
53fn 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 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 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 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 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 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(); 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(); self.cur_line = self.toks.line_at(pos);
151 if self.tok(pos) == PARA_TOK {
152 pos += 1; 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; 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(); pos += 5; 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 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 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)?; }
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)?; 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, ¶m_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)?; let declared = vec![p_name.clone()];
370 self.mask_vocab_of_word(&sub_name, true); 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)?; }
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)?; 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, ¶m_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 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 Ok(format!("{})\r\n", sb.trim_end_matches(['\r', '\n'])))
499 }
500
501 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 fn parse_define(&mut self, pos: &mut usize) -> Result<(), Refusal> {
520 *pos += 1; 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 *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 fn parse_use_library(&mut self, pos: &mut usize) -> Result<(), Refusal> {
550 *pos += 2; 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(()); }
570 self.include_lines
571 .push_str(&format!("(include \"{fname}\")\r\n"));
572 Ok(())
573 }
574
575 fn check_dup_action(&mut self, name: &str, name_pos: usize) -> Result<(), Refusal> {
577 let n = fold(name);
578 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 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(¶ms)),
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 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 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 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 #[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 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}