1mod expr;
21mod formula;
22mod names;
23mod stmt;
24
25use std::collections::HashMap;
26
27use crate::expand::Expander;
28use crate::form::{str_lit_content, sym_text, Form, List};
29use crate::headtable::resolve_head_alias;
30use crate::intrinsics::fold;
31use crate::messages::{raise, Refusal};
32use crate::reader::{count_lf, parse_all, tokenize};
33
34pub use names::sym_name;
35
36pub const MAKEBUTTON_SUB: &str = "VlaCompiledMakeButton";
39
40#[derive(Clone, Debug)]
42struct SubDoc {
43 as_written: String,
44 doc: String,
45 kind: String,
46}
47
48#[derive(Default)]
50pub struct Compiler {
51 pub expander: Expander,
52 sub_docs: HashMap<String, SubDoc>,
53 func_name: String,
54 in_function: bool,
55 tco_name: String,
56 tco_params: Vec<String>,
57 proc_emitted: bool,
58 emit_line: u32,
59 at_line: u32,
60 gen_row: String,
61 slab_counter: u32,
62 slab_row_var: String,
63 slab_arr_var: String,
64 slab_i_var: String,
65}
66
67pub fn compile(source: &str, prelude: &str) -> Result<String, Refusal> {
71 Compiler::new().transpile(source, prelude)
72}
73
74impl Compiler {
75 pub fn new() -> Compiler {
76 Compiler::default()
77 }
78
79 pub fn transpile(&mut self, source: &str, prelude: &str) -> Result<String, Refusal> {
81 refuse_includes(source)?;
82 let line_offset = count_lf(prelude) as u32 + 1;
83 self.emit_line = 0;
84 self.at_line = 0;
85 self.gen_row.clear();
86 self.slab_counter = 0;
87 self.slab_row_var.clear();
88 let text = format!("{prelude}\r\n{source}");
89 let forms = parse_all(&tokenize(&text), line_offset)?;
90 self.expander = Expander::new();
91 self.sub_docs.clear();
92 self.proc_emitted = false;
93 self.func_name.clear();
94 self.in_function = false;
95
96 let body = self.expander.collect(forms)?;
98
99 let mut out = String::from("Option Explicit\r\n\r\n");
102 for f in &body {
103 let expanded = match self.expander.expand(f, 0) {
104 Ok(e) => e,
105 Err(e) => return Err(self.emit_fail(e)),
106 };
107 match self.emit_top(&expanded) {
108 Ok(text) => out.push_str(&text),
109 Err(e) => return Err(self.emit_fail(e)),
110 }
111 out.push_str("\r\n");
112 }
113 if out.contains(&format!("Call {MAKEBUTTON_SUB}(")) {
114 out.push_str("\r\n");
115 out.push_str(&make_button_helper_text());
116 }
117 Ok(out)
118 }
119
120 fn emit_fail(&self, e: Refusal) -> Refusal {
124 if self.emit_line > 0 && !e.text.contains("vla line") {
125 let suffix = format!(
126 " (near vla line {}{})",
127 self.emit_line,
128 self.at_line_suffix()
129 );
130 return e.with_suffix(&suffix);
131 }
132 e
133 }
134
135 fn at_line_suffix(&self) -> String {
137 if self.at_line > 0 {
138 format!(", source line {}", self.at_line)
139 } else {
140 String::new()
141 }
142 }
143
144 fn topo_guard(&self, what: &str) -> Result<(), Refusal> {
146 if self.proc_emitted {
147 return Err(raise("vla-module-level-decl-after-proc", &[("what", what)]));
148 }
149 Ok(())
150 }
151
152 fn record_sub_doc(&mut self, as_written: &str, doc: &str, kind: &str) {
154 self.sub_docs.insert(
155 fold(as_written),
156 SubDoc {
157 as_written: as_written.to_string(),
158 doc: doc.to_string(),
159 kind: kind.to_string(),
160 },
161 );
162 }
163
164 pub fn sub_doc(&self, name: &str) -> String {
166 self.sub_docs
167 .get(&fold(name))
168 .map(|d| d.doc.clone())
169 .unwrap_or_default()
170 }
171
172 fn sub_exists(&self, as_written: &str) -> bool {
174 self.sub_docs.contains_key(&fold(as_written))
175 }
176
177 pub fn procedures(&self) -> Vec<(String, String)> {
180 self.sub_docs
181 .values()
182 .map(|d| (d.as_written.clone(), d.kind.clone()))
183 .collect()
184 }
185
186 fn emit_top(&mut self, f: &Form) -> Result<String, Refusal> {
188 let Form::List(lst) = f else {
189 return Err(raise(
190 "vla-top-level-not-list",
191 &[("value", &f.atom_text().unwrap_or_default())],
192 ));
193 };
194 let h = resolve_head_alias(&fold(&lst.head_sym()?));
195 match h.as_str() {
196 "sub" => {
197 self.proc_emitted = true; self.emit_proc(lst, false, "Public")
199 }
200 "function" => {
201 self.proc_emitted = true;
202 self.emit_proc(lst, true, "Public")
203 }
204 "dim" => {
205 self.topo_guard("dim")?;
206 Ok(format!("{}\r\n", self.emit_dim_core(lst)?))
207 }
208 "const" => {
209 self.topo_guard("const")?;
210 Ok(format!("{}\r\n", self.emit_const_core(lst)?))
211 }
212 "raw" => Ok(format!("{}\r\n", str_lit_content(lst.nth(2)?)?)),
213 "type" => self.emit_type_def(lst, ""),
214 "enum" => self.emit_enum_def(lst, ""),
215 "public" | "private" => {
216 let vis = if h == "public" { "Public" } else { "Private" };
218 self.emit_visibility(lst, vis)
219 }
220 "begin" => {
221 let mut out = String::new();
222 for i in 2..=lst.count() {
223 out.push_str(&self.emit_top(lst.nth(i)?)?);
224 }
225 Ok(out)
226 }
227 "deflambda" => Err(raise("vla-deflambda-runs", &[])),
228 "include" => Err(raise("vla-include-must-stand-alone", &[])),
229 _ => Err(raise("vla-unknown-top-level-form", &[("head", &h)])),
230 }
231 }
232
233 fn emit_proc(&mut self, lst: &List, is_func: bool, vis: &str) -> Result<String, Refusal> {
236 let raw_name = sym_text(lst.nth(2)?)?;
237 if raw_name.contains(':') && !fold(&raw_name).starts_with("on:click:") {
239 return Err(raise(
240 "vla-interpreter-only-handler",
241 &[("name", &raw_name)],
242 ));
243 }
244 let name = sym_name(&raw_name)?;
245 let params = lst.nth_list(3)?;
246
247 let mut body_idx = 4;
248 let mut ret_type = String::from("Variant");
249 if is_func && lst.count() >= 4 {
250 if let Form::Sym(s) = lst.nth(4)? {
253 ret_type = sym_name(s)?;
254 body_idx = 5;
255 }
256 }
257
258 let mut pdoc = String::new();
261 if lst.count() > body_idx {
262 if let Form::Str(s) = lst.nth(body_idx)? {
263 pdoc = s.clone();
264 body_idx += 1;
265 }
266 }
267 self.record_sub_doc(&raw_name, &pdoc, if is_func { "function" } else { "sub" });
268
269 self.tco_name.clear();
272 self.tco_params.clear();
273 if is_func {
274 let mut all_by_val = true;
275 for pv in ¶ms.items {
276 match pv {
277 Form::List(pvl) => {
278 if fold(&pvl.head_sym()?) != "byval" {
279 all_by_val = false;
280 }
281 }
282 _ => all_by_val = false,
283 }
284 }
285 if all_by_val && tco_scan(lst, body_idx, &name)? {
286 self.tco_name = fold(&name);
287 for pv in ¶ms.items {
288 let Form::List(pvl) = pv else { unreachable!() };
289 self.tco_params.push(sym_name(&sym_text(pvl.nth(2)?)?)?);
290 }
291 }
292 }
293
294 self.func_name = name.clone();
295 self.in_function = is_func;
296
297 let kind = if is_func { "Function" } else { "Sub" };
298 let mut r = format!("{vis} {kind} {name}({})", self.emit_params(params)?);
299 if is_func {
300 r.push_str(&format!(" As {ret_type}"));
301 }
302 r.push_str("\r\n");
303 if !self.tco_name.is_empty() {
304 if self.tco_params.len() > 1 {
306 for ti in 1..=self.tco_params.len() {
307 r.push_str(&format!(" Dim vla_tco_{ti} As Variant\r\n"));
308 }
309 }
310 r.push_str("vla_tco:\r\n");
311 }
312 r.push_str(&self.emit_body(lst, body_idx, 1)?);
313 r.push_str(&format!("End {kind}\r\n"));
314 self.tco_name.clear();
315 self.tco_params.clear();
316 Ok(r)
317 }
318
319 fn emit_params(&self, params: &List) -> Result<String, Refusal> {
322 let mut parts: Vec<String> = Vec::with_capacity(params.count());
323 for p in ¶ms.items {
324 let one = match p {
325 Form::List(pl) => {
326 let ph = fold(&pl.head_sym()?);
327 match ph.as_str() {
328 "byval" => format!(
329 "ByVal {} As {}",
330 sym_name(&sym_text(pl.nth(2)?)?)?,
331 sym_name(&sym_text(pl.nth(3)?)?)?
332 ),
333 "byref" => format!(
334 "ByRef {} As {}",
335 sym_name(&sym_text(pl.nth(2)?)?)?,
336 sym_name(&sym_text(pl.nth(3)?)?)?
337 ),
338 "optional" => {
339 let mut one = format!(
340 "Optional {} As {}",
341 sym_name(&sym_text(pl.nth(2)?)?)?,
342 sym_name(&sym_text(pl.nth(3)?)?)?
343 );
344 if pl.count() >= 4 {
345 one.push_str(&format!(" = {}", self.emit_expr(pl.nth(4)?)?));
346 }
347 one
348 }
349 "paramarray" => format!(
350 "ParamArray {}() As Variant",
351 sym_name(&sym_text(pl.nth(2)?)?)?
352 ),
353 _ => format!(
354 "{} As {}",
355 sym_name(&sym_text(pl.nth(1)?)?)?,
356 sym_name(&sym_text(pl.nth(2)?)?)?
357 ),
358 }
359 }
360 _ => sym_name(&p.atom_text().unwrap_or_default())?,
362 };
363 parts.push(one);
364 }
365 Ok(parts.join(", "))
366 }
367
368 fn emit_body(&mut self, lst: &List, from_idx: usize, ind: usize) -> Result<String, Refusal> {
370 let mut out = String::new();
371 for i in from_idx..=lst.count() {
372 out.push_str(&self.emit_stmt(lst.nth(i)?, ind)?);
373 }
374 Ok(out)
375 }
376}
377
378fn tco_scan(lst: &List, from_idx: usize, self_name: &str) -> Result<bool, Refusal> {
380 for i in from_idx..=lst.count() {
381 if tco_scan_form(lst.nth(i)?, self_name)? {
382 return Ok(true);
383 }
384 }
385 Ok(false)
386}
387
388fn tco_scan_form(v: &Form, self_name: &str) -> Result<bool, Refusal> {
390 let Form::List(lst) = v else {
391 return Ok(false);
392 };
393 let Some(Form::Sym(head)) = lst.items.first() else {
394 return Ok(false);
395 };
396 let h = fold(head);
397 if h == "quote" {
398 return Ok(false); }
400 if h == "return" && lst.count() >= 2 {
401 if let Form::List(inner) = lst.nth(2)? {
402 if let Some(Form::Sym(callee)) = inner.items.first() {
403 if fold(&sym_name(callee)?) == fold(self_name) {
404 return Ok(true);
405 }
406 }
407 }
408 }
409 for e in &lst.items {
410 if tco_scan_form(e, self_name)? {
411 return Ok(true);
412 }
413 }
414 Ok(false)
415}
416
417pub fn make_button_helper_text() -> String {
420 [
421 format!("Private Sub {MAKEBUTTON_SUB}(ByVal vlaCaption As String, ByVal vlaPlace As Range, ByVal vlaProc As String)"),
422 " Dim vlaWs As Worksheet".to_string(),
423 " Set vlaWs = vlaPlace.Worksheet".to_string(),
424 " On Error Resume Next".to_string(),
425 " vlaWs.Buttons(vlaCaption).Delete".to_string(),
426 " On Error GoTo 0".to_string(),
427 " Dim vlaBtn As Button".to_string(),
428 " Set vlaBtn = vlaWs.Buttons.Add(vlaPlace.Left, vlaPlace.Top, 120, 24)".to_string(),
429 " vlaBtn.Caption = vlaCaption".to_string(),
430 " vlaBtn.Name = vlaCaption".to_string(),
431 " If Len(vlaProc) > 0 Then".to_string(),
432 " vlaBtn.OnAction = \"'\" & ThisWorkbook.Name & \"'!\" & vlaProc".to_string(),
433 " End If".to_string(),
434 "End Sub".to_string(),
435 String::new(),
436 ]
437 .join("\r\n")
438}
439
440fn refuse_includes(source: &str) -> Result<(), Refusal> {
444 if !fold(source).contains("(include") {
445 return Ok(());
446 }
447 for line in source.split('\n') {
448 if let Some(name) = include_line_file(line) {
449 return Err(raise(
450 "vla-include-cannot-read",
451 &[("name", &name), ("path", &name)],
452 ));
453 }
454 }
455 Ok(())
456}
457
458fn include_line_file(line: &str) -> Option<String> {
460 let t = line.trim();
461 let head = t.get(..9)?;
462 if fold(head) != "(include " || !t.ends_with(')') {
463 return None;
464 }
465 let inner = t[9..t.len() - 1].trim();
466 if inner.len() < 2 || !inner.starts_with('"') || !inner.ends_with('"') {
467 return None;
468 }
469 let name = &inner[1..inner.len() - 1];
470 if name.is_empty() || name.contains('"') {
471 return None;
472 }
473 Some(name.to_string())
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 const PRELUDE: &str = include_str!("../../../scripts/prelude.vla");
481 const GOLDEN_VLA: &str = include_str!("../../../scripts/instructions_golden.vla");
482 const GOLDEN_VBA: &str = include_str!("../../../scripts/instructions_golden.vba");
483
484 fn vba(src: &str) -> String {
485 compile(src, PRELUDE).unwrap_or_else(|e| panic!("{src}: {e}"))
486 }
487
488 fn refusal(src: &str) -> Refusal {
489 match compile(src, PRELUDE) {
490 Ok(text) => panic!("{src} compiled instead of refusing: {text}"),
491 Err(e) => e,
492 }
493 }
494
495 #[test]
496 fn a_sub_and_a_function_with_their_shapes() {
497 assert_eq!(
498 vba("(sub t () (set! x 1))"),
499 "Option Explicit\r\n\r\nPublic Sub t()\r\n x = 1 ' vla:1\r\nEnd Sub\r\n\r\n"
500 );
501 let f = vba("(function tax ((amount Variant)) (return (* amount 0.08)))");
502 assert!(f.contains("Public Function tax(amount As Variant) As Variant\r\n"));
503 assert!(f.contains(" tax = (amount * 0.08) ' vla:1\r\n Exit Function\r\n"));
504 let g = vba("(function f ((byval n Long)) Long \"the doc\" (return (+ n 1)))");
505 assert!(g.contains("Public Function f(ByVal n As Long) As Long\r\n"));
506 assert!(g.contains(" f = (n + 1) ' vla:1\r\n"));
507 let p = vba(
508 "(sub s ((a Long) (byref b String) (optional c Variant 1) (paramarray d) e) (exit-sub))",
509 );
510 assert!(p.contains(
511 "Public Sub s(a As Long, ByRef b As String, Optional c As Variant = 1, ParamArray d() As Variant, e)\r\n"
512 ));
513 }
514
515 #[test]
516 fn module_level_forms_and_the_topology_guard() {
517 let m = vba("(const k 5)\n(dim x Long)\n(private (dim m Long))\n(public (const q \"s\"))\n(type Pt (x Double) y)\n(enum E a (b 2))\n(raw \"' hello\")");
518 assert!(m.contains("Const k = 5\r\n\r\nDim x As Long\r\n\r\nPrivate m As Long\r\n\r\nPublic Const q = \"s\"\r\n\r\n"));
519 assert!(m.contains("Type Pt\r\n x As Double\r\n y As Variant\r\nEnd Type\r\n\r\n"));
520 assert!(m.contains("Enum E\r\n a\r\n b = 2\r\nEnd Enum\r\n\r\n' hello\r\n"));
521 let r = refusal("(sub t () (debug-print 1))\n(dim late Long)");
522 assert_eq!(r.id, "vla-module-level-decl-after-proc");
523 assert!(r.text.contains("must come before the first"));
524 assert_eq!(refusal("5").id, "vla-top-level-not-list");
525 assert_eq!(refusal("(nonsense 1)").id, "vla-unknown-top-level-form");
526 assert_eq!(refusal("(deflambda f (x) x)").id, "vla-deflambda-runs");
527 }
528
529 #[test]
530 fn a_formula_write_calls_the_runtime_sink() {
531 let f = vba("(sub t () (set! (. (range \"b3\") formula) \"=A1\"))");
535 assert!(
536 f.contains(" Call VlaSetFormula(range(\"b3\"), \"=A1\") ' vla:1\r\n"),
537 "{f}"
538 );
539 assert!(!f.contains("Formula2"), "{f}");
540 let g = vba("(sub t () (set! x (. (range \"b3\") formula)))");
541 assert!(
542 g.contains(" x = range(\"b3\").formula ' vla:1\r\n"),
543 "{g}"
544 );
545 let h = vba("(sub t () (set! (. (range \"b3\") value) \"=A1\"))");
546 assert!(
547 h.contains(" range(\"b3\").value = \"=A1\" ' vla:1\r\n"),
548 "{h}"
549 );
550 }
551
552 #[test]
553 fn the_refusal_names_the_nearest_mapped_line() {
554 let r = refusal("(sub t ()\n (set! x 1)\n (if))");
555 assert!(r.text.ends_with(" (near vla line 3)"), "{}", r.text);
556 let r = refusal("(sub t () (at-line 40 (set! x 1) (if)))");
557 assert!(
558 r.text.ends_with(" (near vla line 1, source line 40)"),
559 "{}",
560 r.text
561 );
562 }
563
564 #[test]
565 fn tail_calls_rebind_and_jump() {
566 let f = vba("(function count-down ((byval n Long) (byval acc Long)) Long (if (= n 0) (then (return acc)) (else (return (count-down (- n 1) (+ acc n))))))");
567 assert!(f.contains(
568 " Dim vla_tco_1 As Variant\r\n Dim vla_tco_2 As Variant\r\nvla_tco:\r\n"
569 ));
570 assert!(f.contains(" vla_tco_1 = (n - 1) ' vla:1\r\n vla_tco_2 = (acc + n)\r\n n = vla_tco_1\r\n acc = vla_tco_2\r\n GoTo vla_tco\r\n"));
571 let g = vba("(function loop-it ((byval n Long)) Long (if (= n 0) (then (return 0)) (else (return (loop-it (- n 1))))))");
572 assert!(g.contains("vla_tco:\r\n"));
573 assert!(g.contains(" n = (n - 1) ' vla:1\r\n GoTo vla_tco\r\n"));
574 let h = vba("(function r ((n Long)) Long (return (r (- n 1))))");
576 assert!(!h.contains("vla_tco"));
577 assert!(h.contains(" r = r((n - 1)) ' vla:1\r\n"));
578 }
579
580 #[test]
581 fn make_button_appends_its_helper_once() {
582 let t = vba("(sub on:click:go () (debug-print 1))\n(sub main () (make-button \"go\" (range \"A1\")))");
583 assert!(t.contains("Call VlaCompiledMakeButton(\"go\", range(\"A1\"), \"on_click_go\")"));
584 assert!(t.ends_with(&format!("\r\n{}", make_button_helper_text())));
585 assert_eq!(t.matches("Private Sub VlaCompiledMakeButton").count(), 1);
586 let u = vba("(sub main () (make-button \"go\" (range \"A1\")))");
587 assert!(u.contains(", \"\")"));
588 assert_eq!(
589 refusal("(sub on:sheet-change () (debug-print 1))").id,
590 "vla-interpreter-only-handler"
591 );
592 }
593
594 #[test]
595 fn an_include_line_is_refused_by_name_here() {
596 let r = refusal("(include \"lib.vla\")\n(sub t () (debug-print 1))");
597 assert_eq!(r.id, "vla-include-cannot-read");
598 assert!(r.text.contains("lib.vla"));
599 assert_eq!(
600 include_line_file(" (include \"a b.vla\") "),
601 Some("a b.vla".to_string())
602 );
603 assert_eq!(include_line_file("(include \"a\") x"), None);
604 assert_eq!(include_line_file("(include-all \"a\")"), None);
605 assert_eq!(
606 refusal("(sub t () (include \"x\"))").id,
607 "vla-include-must-stand-alone"
608 );
609 }
610
611 #[test]
612 fn the_golden_compiles_to_the_golden_from_its_second_line_on() {
613 let program = GOLDEN_VLA.split_once('\n').expect("a stamp line").1;
616 let got = compile(program, PRELUDE).unwrap_or_else(|e| panic!("{e}"));
617 let want = GOLDEN_VBA;
618 let norm = |s: &str| s.replace("\r\n", "\n").trim_end_matches('\n').to_string();
619 let (got, want) = (norm(&got), norm(want));
620 if got != want {
621 let at = got
622 .chars()
623 .zip(want.chars())
624 .position(|(a, b)| a != b)
625 .unwrap_or(got.len().min(want.len()));
626 let line = want[..at].matches('\n').count() + 1;
627 let context = |s: &str| {
628 let start = s[..at].rfind('\n').map(|i| i + 1).unwrap_or(0);
629 let end = s[at..].find('\n').map(|i| at + i).unwrap_or(s.len());
630 s[start..end].to_string()
631 };
632 panic!(
633 "differs at char {at} of {} (line {line}):\n want: {}\n got: {}",
634 want.len(),
635 context(&want),
636 context(&got)
637 );
638 }
639 }
640
641 #[test]
642 fn a_program_at_the_language_s_depth_limit_compiles_on_a_small_stack() {
643 let program = |n: usize| {
649 format!(
650 "(sub main () (debug-print {}1{}))",
651 "(+ 1 ".repeat(n),
652 ")".repeat(n)
653 )
654 };
655 let limit = crate::reader::FORM_DEPTH_LIMIT as usize;
656 std::thread::Builder::new()
657 .stack_size(1 << 20)
658 .spawn(move || {
659 let vba = compile(&program(limit - 2), "").unwrap_or_else(|e| panic!("{e}"));
660 assert!(vba.contains("Debug.Print (1 + (1 + "));
661 let past = compile(&program(limit - 1), "").unwrap_err();
662 assert_eq!(past.id, "vla-form-too-deep");
663 })
664 .unwrap()
665 .join()
666 .unwrap();
667 }
668}