1use crate::english::program::RefusalAtLine;
22use crate::english::vocab::{vocab_requires_check_capability, vocab_text_has_raw_form};
23use crate::english::Grammar;
24use crate::messages::{raise, Refusal};
25
26pub fn english_translate_text_to_vla(
30 program_text: &str,
31 prelude_text: &str,
32 vocab_texts: &[&str],
33) -> Result<String, RefusalAtLine> {
34 let g = load_grammar(prelude_text, vocab_texts)?;
35 g.translate_program_at(program_text).map(|t| t.vla)
36}
37
38pub fn english_translate_text_to_vba(
42 program_text: &str,
43 prelude_text: &str,
44 vocab_texts: &[&str],
45) -> Result<String, RefusalAtLine> {
46 let g = load_grammar(prelude_text, vocab_texts)?;
47 let t = g.translate_program_at(program_text)?;
48 crate::emit::compile(&t.vla, prelude_text).map_err(|refusal| RefusalAtLine { refusal, line: 0 })
49}
50
51pub fn english_build_xlsx(
56 program_text: &str,
57 prelude_text: &str,
58 vocab_texts: &[&str],
59) -> Result<Vec<u8>, RefusalAtLine> {
60 let g = load_grammar(prelude_text, vocab_texts)?;
61 let t = g.translate_program_at(program_text)?;
62 crate::build::build_xlsx(program_text, &t.vla, prelude_text, vocab_texts)
63 .map_err(|refusal| RefusalAtLine { refusal, line: 0 })
64}
65
66pub fn english_rebuild_xlsx(
73 file: &[u8],
74 prelude_text: &str,
75 vocab_texts: &[&str],
76) -> Result<crate::build::Rebuilt, RefusalAtLine> {
77 use crate::build::{build_workbook_with, own_part_text, Rebuilt};
78 use crate::sheet::merge::HostInfo;
79 use crate::sheet::ooxml::{sheet_part, sheet_xml, Render};
80
81 let read =
82 crate::build::read_build(file).map_err(|refusal| RefusalAtLine { refusal, line: 0 })?;
83 let partial = read.stamp.into.is_some();
84 let answer = |matches: bool, why: String| Rebuilt {
85 sentences: read.sentences,
86 built_by: read.stamp.version.clone(),
87 matches,
88 why,
89 partial,
90 };
91 if let Some(why) = read
92 .stamp
93 .disagreement(&read.program_text, prelude_text, vocab_texts)
94 {
95 return Ok(answer(false, why));
96 }
97 let version_note = || {
98 if read.stamp.version != crate::VERSION {
99 format!(
100 "it was built by Frazaro {} and this is Frazaro {}, which writes the file differently",
101 read.stamp.version,
102 crate::VERSION
103 )
104 } else {
105 "its parts are not what this core builds from these sentences: a host has saved it since, or it was built differently".to_string()
106 }
107 };
108 match &read.stamp.into {
109 None => {
110 let again = english_build_xlsx(&read.program_text, prelude_text, vocab_texts)?;
111 let matches = again == file;
112 Ok(answer(
113 matches,
114 if matches {
115 String::new()
116 } else {
117 version_note()
118 },
119 ))
120 }
121 Some(into) => {
122 let host = HostInfo {
125 label: "the workbook".to_string(),
126 sheet_names: read.host_sheets.clone(),
127 model_hex: into.model_hex.clone(),
128 style_base: into.style_base,
129 cm: Some(into.cm),
130 };
131 let g = load_grammar(prelude_text, vocab_texts)?;
132 let t = g.translate_program_at(&read.program_text)?;
133 let wb = build_workbook_with(
134 &read.program_text,
135 &t.vla,
136 prelude_text,
137 vocab_texts,
138 Some(&host),
139 )
140 .map_err(|refusal| RefusalAtLine { refusal, line: 0 })?;
141 let render = Render {
142 style_base: into.style_base,
143 cm: into.cm,
144 };
145 let mut matches = true;
146 for (i, sheet) in wb.sheets.iter().enumerate() {
147 let want = sheet_xml(sheet, false, &render);
148 if own_part_text(file, &sheet_part(i + 1)).as_deref() != Some(want.as_str()) {
149 matches = false;
150 }
151 }
152 if own_part_text(file, &sheet_part(wb.sheets.len() + 1)).is_some() {
153 matches = false; }
155 Ok(answer(
156 matches,
157 if matches {
158 String::new()
159 } else {
160 version_note()
161 },
162 ))
163 }
164 }
165}
166
167pub fn english_build_xlsx_into(
171 program_text: &str,
172 prelude_text: &str,
173 vocab_texts: &[&str],
174 model_bytes: Vec<u8>,
175 label: &str,
176) -> Result<Vec<u8>, RefusalAtLine> {
177 let g = load_grammar(prelude_text, vocab_texts)?;
178 let t = g.translate_program_at(program_text)?;
179 crate::build::build_xlsx_into(
180 program_text,
181 &t.vla,
182 prelude_text,
183 vocab_texts,
184 model_bytes,
185 label,
186 )
187 .map_err(|refusal| RefusalAtLine { refusal, line: 0 })
188}
189
190pub fn english_view(
202 program_text: &str,
203 prelude_text: &str,
204 vocab_texts: &[&str],
205 sheet: &str,
206 window: Option<&str>,
207) -> Result<String, RefusalAtLine> {
208 let g = load_grammar(prelude_text, vocab_texts)?;
209 let t = g.translate_program_at(program_text)?;
210 let at_line_0 = |refusal| RefusalAtLine { refusal, line: 0 };
211 let model = crate::build::build_workbook(program_text, &t.vla, prelude_text, vocab_texts)
212 .map_err(at_line_0)?;
213 let window = crate::view::window_of(&model, sheet, window).map_err(at_line_0)?;
214 let library = crate::excel::library();
215 let calc = vla_lang::calc::Calc::run(&model, &library);
216 Ok(crate::view::view_text_valued(&model, &window, &calc))
217}
218
219pub fn calc_rows(bytes: &[u8], label: &str) -> Result<String, Refusal> {
226 crate::reflect::calc::calc_text(bytes, label, &crate::excel::library())
227}
228
229pub fn function_histogram(bytes: &[u8], label: &str) -> Result<String, Refusal> {
233 crate::reflect::histogram::histogram_text(bytes, label)
234}
235
236pub fn reflect_relations(bytes: &[u8], label: &str) -> Result<String, Refusal> {
242 crate::reflect::reflect_text(bytes, label)
243}
244
245pub fn diff_relations(
251 old_bytes: &[u8],
252 old_label: &str,
253 new_bytes: &[u8],
254 new_label: &str,
255) -> Result<String, Refusal> {
256 crate::reflect::diff::diff_text(old_bytes, old_label, new_bytes, new_label)
257}
258
259pub fn audit_findings(bytes: &[u8], label: &str) -> Result<String, Refusal> {
264 crate::reflect::audit::audit_text(bytes, label)
265}
266
267fn load_grammar(prelude_text: &str, vocab_texts: &[&str]) -> Result<Grammar, RefusalAtLine> {
271 let mut g = Grammar::new(prelude_text);
272 for (i, text) in vocab_texts.iter().enumerate() {
273 g.load_vocabulary_text(text, &format!("vocab-{}", i + 1))
274 .map_err(|refusal| RefusalAtLine { refusal, line: 0 })?;
275 }
276 Ok(g)
277}
278
279pub fn vocab_gate(text: &str, source_name: &str, allow_raw: bool) -> Result<(), Refusal> {
285 vocab_requires_check_capability(text, source_name)?;
286 if !allow_raw && vocab_text_has_raw_form(text) {
287 return Err(raise(
288 "english-vocab-raw-consent-declined",
289 &[("source", source_name)],
290 ));
291 }
292 Ok(())
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 const PRELUDE: &str = include_str!("../../scripts/prelude.vla");
300 const ENGLISH: &str = include_str!("../../scripts/polyglotta/english.vla");
301 const ESPANOL: &str = include_str!("../../scripts/polyglotta/espanol.vla");
302 const DANSK: &str = include_str!("../../scripts/polyglotta/dansk.vla");
303 const DEUTSCHE: &str = include_str!("../../scripts/polyglotta/deutsche.vla");
304 const ESPERANTO: &str = include_str!("../../scripts/polyglotta/esperanto.vla");
305 const FRANCAIS: &str = include_str!("../../scripts/polyglotta/francais.vla");
306 const LATIN: &str = include_str!("../../scripts/polyglotta/latin.vla");
307 const PIRATE: &str = include_str!("../../scripts/polyglotta/pirate.vla");
308 const PROGRAM: &str = include_str!("../../scripts/instructions.txt");
309 const GOLDEN_VLA: &str = include_str!("../../scripts/instructions_golden.vla");
310 const GOLDEN_VBA: &str = include_str!("../../scripts/instructions_golden.vba");
311
312 fn normalized(s: &str) -> String {
313 s.replace("\r\n", "\n").trim_end_matches('\n').to_string()
314 }
315
316 #[test]
319 fn to_vla_reproduces_the_translate_golden() {
320 let got = english_translate_text_to_vla(PROGRAM, PRELUDE, &[ENGLISH])
321 .unwrap_or_else(|e| panic!("{}", e.refusal.text));
322 let want = GOLDEN_VLA
323 .split_once('\n')
324 .map(|(_, rest)| rest)
325 .unwrap_or("");
326 assert_eq!(normalized(&got), normalized(want));
327 }
328
329 #[test]
331 fn to_vba_reproduces_the_compile_golden() {
332 let got = english_translate_text_to_vba(PROGRAM, PRELUDE, &[ENGLISH])
333 .unwrap_or_else(|e| panic!("{}", e.refusal.text));
334 assert_eq!(normalized(&got), normalized(GOLDEN_VBA));
335 }
336
337 #[test]
338 fn a_phrasebook_refusal_names_the_browser_s_source() {
339 let e = english_translate_text_to_vla("Log 1.", PRELUDE, &[ENGLISH, "hello"]).unwrap_err();
340 assert_eq!(e.refusal.id, "english-vocab-expected-directive");
341 assert!(
345 e.refusal.text.starts_with("vocab-2 line 0: "),
346 "{}",
347 e.refusal.text
348 );
349 assert_eq!(e.line, 0);
350 }
351
352 #[test]
353 fn a_program_refusal_carries_its_line_and_to_vba_passes_it_through() {
354 let program = "Log 1.\nSet total to $5.";
355 let e = english_translate_text_to_vla(program, PRELUDE, &[ENGLISH]).unwrap_err();
356 assert_eq!(
357 (e.refusal.id.as_str(), e.line),
358 ("english-unknown-character", 2)
359 );
360 let e2 = english_translate_text_to_vba(program, PRELUDE, &[ENGLISH]).unwrap_err();
361 assert_eq!(e2, e);
362 let vba = english_translate_text_to_vba("Log 1.", PRELUDE, &[ENGLISH])
363 .unwrap_or_else(|e| panic!("{}", e.refusal.text));
364 assert!(vba.contains("Debug.Print 1"), "{vba}");
365 }
366
367 #[test]
371 fn every_dialect_loads_over_english_and_reads_its_example() {
372 let cases = [
373 ("dansk", DANSK, "Saet formlen \"=B2*2\" i cellen B3."),
374 (
375 "deutsche",
376 DEUTSCHE,
377 "Setze die Formel \"=B2*2\" in Zelle B3.",
378 ),
379 (
380 "espanol",
381 ESPANOL,
382 "Pon la formula \"=B2*2\" en la celda B3.",
383 ),
384 (
385 "esperanto",
386 ESPERANTO,
387 "Metu la formulon \"=B2*2\" en la chelon B3.",
388 ),
389 (
390 "francais",
391 FRANCAIS,
392 "Mets la formule \"=B2*2\" dans la cellule B3.",
393 ),
394 ("latin", LATIN, "Pone formulam \"=B2*2\" in cellula B3."),
395 (
396 "pirate",
397 PIRATE,
398 "Chart the course \"=B2*2\" onto the cell B3, arr.",
399 ),
400 ];
401 for (name, book, sentence) in cases {
402 let vla = english_translate_text_to_vla(sentence, PRELUDE, &[ENGLISH, book])
403 .unwrap_or_else(|e| panic!("{name}: {}", e.refusal.text));
404 assert!(
405 vla.contains("(range \"b3\")") && vla.contains("\"=B2*2\""),
406 "{name}: {vla}"
407 );
408 }
409 }
410
411 #[test]
412 fn the_gate_asks_capability_first_then_consent() {
413 let raw = "(english-vla \"char cell {r:cell}\" (raw \"Debug.Print 2\"))";
414 let e = vocab_gate(raw, "pasted.vla", false).unwrap_err();
415 assert_eq!(e.id, "english-vocab-raw-consent-declined");
416 assert!(
417 e.text.starts_with("'pasted.vla' was not loaded"),
418 "{}",
419 e.text
420 );
421 assert!(vocab_gate(raw, "pasted.vla", true).is_ok());
422 let both = "(requires-capability \"network\")\n(english-vla \"x\" (raw \"y\"))";
423 let e = vocab_gate(both, "pasted.vla", true).unwrap_err();
424 assert_eq!(e.id, "english-vocab-requires-capability-ungranted");
425 assert!(vocab_gate(
426 "(english-vla \"wobble {x:expr}\" (debug-print {x}))",
427 "p",
428 false
429 )
430 .is_ok());
431 }
432
433 #[test]
434 fn phrasebooks_load_in_the_order_given_and_the_text_path_is_ungated() {
435 let vla =
437 english_translate_text_to_vla("Pon 5 en la celda B2.", PRELUDE, &[ENGLISH, ESPANOL])
438 .unwrap_or_else(|e| panic!("{}", e.refusal.text));
439 assert!(vla.contains("(set! (range \"b2\") 5)"), "{vla}");
440 let book = "(english-vla \"char cell {r:cell}\" (raw \"Debug.Print 2\"))";
442 let vla = english_translate_text_to_vla("Char cell A1.", PRELUDE, &[book])
443 .unwrap_or_else(|e| panic!("{}", e.refusal.text));
444 assert!(vla.contains("(raw \"Debug.Print 2\")"), "{vla}");
445 }
446
447 const FIXTURE: &str = include_str!("../../scripts/build/fixture.txt");
448 const INTO: &str = include_str!("../../scripts/build/into.txt");
449
450 fn golden(text: &str) -> String {
452 text.replace("\r\n", "\n")
453 }
454
455 #[test]
458 fn the_view_goldens_are_reproduced() {
459 for (program, sheet, window, golden_text, path) in [
460 (
461 FIXTURE,
462 "Frazaro",
463 None,
464 include_str!("../../scripts/view/fixture_frazaro.vla"),
465 "scripts/view/fixture_frazaro.vla",
466 ),
467 (
468 FIXTURE,
469 "Output",
470 Some("A1:F20"),
471 include_str!("../../scripts/view/fixture_output.vla"),
472 "scripts/view/fixture_output.vla",
473 ),
474 (
475 FIXTURE,
476 "Output",
477 Some("B2:C3"),
478 include_str!("../../scripts/view/fixture_output_b2_c3.vla"),
479 "scripts/view/fixture_output_b2_c3.vla",
480 ),
481 (
482 FIXTURE,
483 "Data",
484 None,
485 include_str!("../../scripts/view/fixture_data.vla"),
486 "scripts/view/fixture_data.vla",
487 ),
488 (
489 INTO,
490 "Output",
491 None,
492 include_str!("../../scripts/view/into_output.vla"),
493 "scripts/view/into_output.vla",
494 ),
495 (
496 INTO,
497 "Checks",
498 None,
499 include_str!("../../scripts/view/into_checks.vla"),
500 "scripts/view/into_checks.vla",
501 ),
502 ] {
503 let got = english_view(program, PRELUDE, &[ENGLISH], sheet, window)
504 .unwrap_or_else(|r| panic!("{path}: {}", r.refusal));
505 let want = golden(golden_text);
506 if got != want {
507 let at = got
508 .lines()
509 .zip(want.lines())
510 .position(|(a, b)| a != b)
511 .map(|i| i + 1)
512 .unwrap_or(got.lines().count().min(want.lines().count()) + 1);
513 panic!(
514 "{path} differs at line {at}: got {:?}, want {:?}",
515 got.lines().nth(at - 1),
516 want.lines().nth(at - 1)
517 );
518 }
519 }
520 }
521
522 #[test]
527 fn the_view_of_a_built_model_is_the_reflect_of_its_file() {
528 use crate::kernel::Window;
529 use crate::reflect::print::quoted;
530 use crate::view::{view_text, whole};
531 for (program, label) in [(FIXTURE, "fixture.txt"), (INTO, "into.txt")] {
532 let vla = english_translate_text_to_vla(program, PRELUDE, &[ENGLISH])
533 .unwrap_or_else(|r| panic!("{label}: {}", r.refusal));
534 let model = crate::build::build_workbook(program, &vla, PRELUDE, &[ENGLISH])
535 .unwrap_or_else(|r| panic!("{label}: {r}"));
536 let bytes = crate::sheet::ooxml::workbook_bytes(&model).unwrap();
537 let reflected = crate::reflect::reflect_text(&bytes, label).unwrap();
538 let file_sheets: Vec<&str> = reflected
539 .lines()
540 .filter(|l| l.starts_with("(sheet "))
541 .collect();
542 let mut seen = 0;
543 for sheet in &model.sheets {
544 let view = view_text(
545 &model,
546 &Window {
547 sheet: sheet.name.clone(),
548 range: whole(sheet),
549 },
550 );
551 let view_sheets: Vec<&str> =
552 view.lines().filter(|l| l.starts_with("(sheet ")).collect();
553 assert_eq!(view_sheets, file_sheets, "{label}: the sheet rows");
554 let body: Vec<&str> = view
555 .lines()
556 .filter(|l| l.starts_with("(cell ") || l.starts_with("(formula "))
557 .collect();
558 let prefix_cell = format!("(cell {} ", quoted(&sheet.name));
559 let prefix_formula = format!("(formula {} ", quoted(&sheet.name));
560 let read: Vec<&str> = reflected
561 .lines()
562 .filter(|l| l.starts_with(&prefix_cell) || l.starts_with(&prefix_formula))
563 .collect();
564 assert_eq!(body, read, "{label}: sheet {}", sheet.name);
565 seen += body.len();
566 }
567 assert!(seen > 10, "{label}: the comparison covered {seen} rows");
568 }
569 }
570}