1use std::collections::{HashMap, HashSet};
14use std::fmt::Write as _;
15use std::rc::Rc;
16
17use super::Language;
18use super::diagnostics::{ErrorKind, Result, TranslationError, unsupported};
19use super::emit_common::{
20 CtorStyle, EmitOptions, EmitState, Emitted, mutual_groups, order_declarations, unthreaded,
21};
22use super::ir::{
23 ByZero, Case, DataDecl, Decl, Effect, Expr, FnDecl, Hints, LitValue, Main, Node, Param,
24 Pattern, Plan, Program, Prop, Semantics, TheoremDecl, rename_function, rename_main,
25 rename_theorem,
26};
27use super::output::thread_output;
28use super::proof::prop_functions;
29use super::surface::{BinaryOp, Flavor, Rounding, UnaryOp};
30use super::types::Type;
31
32mod arrays;
33mod expressions;
34mod floats;
35mod math;
36mod mutual;
37mod proofs;
38
39use self::arrays::{FLOAT_INDEX, LIST_AT};
40pub use self::floats::FLOAT_PRELUDE;
41use self::floats::{FLOAT_REM, FLOAT_SAME, JS_CONSOLE, JS_NUMBER};
42use self::math::MATH;
43use self::mutual::Mutual;
44
45const KEYWORDS: &[&str] = &[
46 "as",
47 "at",
48 "cofix",
49 "else",
50 "end",
51 "exists",
52 "exists2",
53 "fix",
54 "for",
55 "forall",
56 "fun",
57 "if",
58 "IF",
59 "in",
60 "let",
61 "match",
62 "mod",
63 "Prop",
64 "return",
65 "Set",
66 "SProp",
67 "then",
68 "Type",
69 "using",
70 "where",
71 "with",
72 "struct",
73 "measure",
74 "wf",
75 "Definition",
76 "Fixpoint",
77 "Function",
78 "Theorem",
79 "Lemma",
80 "Proof",
81 "Qed",
82 "Defined",
83 "Module",
84 "End",
85 "Inductive",
86 "Import",
87 "Require",
88 "From",
89 "Open",
90 "Scope",
91 "Eval",
92 "Compute",
93 "Check",
94 "Print",
95 "N",
97 "Z",
98 "String",
99 "EmptyString",
100 "negb",
101 "andb",
102 "orb",
103 "true",
104 "false",
105 "bool",
106 "string",
107 "list",
108 "nil",
109 "cons",
110 "unit",
111 "tt",
112 "nat",
113 "O",
114 "S",
115 "Bool",
116 "Ascii",
117 "main",
118 "lia",
119 "nia",
120 "float",
121 "PrimFloat",
122 "left",
124 "right",
125 "inleft",
126 "inright",
127 "Some",
128 "None",
129 "pair",
130 "inl",
131 "inr",
132 "exist",
133 "existT",
134 "I",
135 "conj",
136 "or_introl",
137 "or_intror",
138 "ex_intro",
139 "eq_refl",
140 "Eq",
141 "Lt",
142 "Gt",
143 "CompEq",
144 "CompLt",
145 "CompGt",
146 "xI",
147 "xO",
148 "xH",
149 "N0",
150 "Npos",
151 "Z0",
152 "Zpos",
153 "Zneg",
154 "ReflectT",
155 "ReflectF",
156 "identity_refl",
157];
158
159pub const ROCQ_PRELUDE: &[&str] =
161 &["From Stdlib Require Import NArith ZArith Lia Recdef String List Ascii."];
162
163const DIGITS: &str = r"Fixpoint ml_digits (fuel : nat) (n : N) (acc : string) : string :=
164 match fuel with
165 | O => acc
166 | S rest =>
167 let acc' := String (ascii_of_N (48 + N.modulo n 10)) acc in
168 if N.ltb n 10 then acc' else ml_digits rest (N.div n 10) acc'
169 end.
170Definition ml_N_to_string (n : N) : string := ml_digits (S (N.size_nat n)) n EmptyString.";
171
172const Z_TO_STRING: &str = r#"Definition ml_Z_to_string (z : Z) : string :=
173 if Z.ltb z 0 then String.append "-" (ml_N_to_string (Z.to_N (Z.opp z))) else ml_N_to_string (Z.to_N z)."#;
174
175const BOOL_TO_STRING: &str =
176 r#"Definition ml_bool_to_string (b : bool) : string := if b then "true" else "false"."#;
177
178const EUCLID: &str = r"(* Euclidean division, the rounding of Lean's Int / and %: the remainder is never negative. *)
179Definition ml_Z_ediv (a b : Z) : Z := Z.mul (Z.sgn b) (Z.div a (Z.abs b)).
180Definition ml_Z_emod (a b : Z) : Z := Z.modulo a (Z.abs b).";
181
182const TACTICS: &str = r"Ltac ml_N_norm := repeat first
183 [ rewrite N.pred_succ
184 | rewrite (proj2 (N.eqb_neq (N.succ _) 0) (N.neq_succ_0 _))
185 | rewrite N.eqb_refl ].
186Ltac ml_obligation := intros; repeat match goal with H : N.eqb _ _ = false |- _ => apply N.eqb_neq in H end; lia.
187Ltac ml_close := first [reflexivity | lia | nia | congruence].";
188
189const DECIDE: &str = r#"Ltac ml_prove := first
192 [ reflexivity | discriminate | exact I | lia
193 | split; ml_prove
194 | left; ml_prove
195 | right; ml_prove
196 | let H := fresh "H" in intro H; first [ml_prove | ml_refute H] ]
197with ml_refute H := first
198 [ discriminate H | exact H | lia
199 | let A := fresh "H" in let B := fresh "H" in destruct H as [A B]; first [ml_refute A | ml_refute B]
200 | let A := fresh "H" in let B := fresh "H" in destruct H as [A | B]; [ml_refute A | ml_refute B]
201 | apply H; ml_prove ].
202Ltac ml_decide := vm_compute; ml_prove."#;
203
204const FIX: &str = "(* The least fixed point of a one-step unfolding F, unfolded lazily: level k
205 nests 2^k calls of F before it would reach the fallback. *)
206Fixpoint ml_fix {A B : Type} (k : nat) (F : (A -> B) -> A -> B) (fallback : A -> B) : A -> B :=
207 match k with
208 | O => F fallback
209 | S k => fun a => ml_fix k F (ml_fix k F fallback) a
210 end.";
211
212const EMIT: &str = "(* The lines a step of main prints, before the lines the rest of main prints
213 and the message of the abort that stops it, if one does. *)
214Definition ml_emit (lines : list string) (rest : list string * option string) : list string * option string :=
215 (app lines (fst rest), snd rest).";
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
219enum Helper {
220 Digits,
221 ZToString,
222 BoolToString,
223 Euclid,
224 JsNumber,
225 JsConsole,
226 FloatSame,
227 FloatRem,
228 Math,
229 ListAt,
230 FloatIndex,
231 Fix,
232 Tactics,
233 Decide,
234 Emit,
235}
236
237impl Helper {
238 const ALL: [Self; 15] = [
239 Self::Digits,
240 Self::ZToString,
241 Self::BoolToString,
242 Self::Euclid,
243 Self::JsNumber,
244 Self::JsConsole,
245 Self::FloatSame,
246 Self::FloatRem,
247 Self::Math,
248 Self::ListAt,
249 Self::FloatIndex,
250 Self::Fix,
251 Self::Tactics,
252 Self::Decide,
253 Self::Emit,
254 ];
255
256 const fn text(self) -> &'static str {
257 match self {
258 Self::Digits => DIGITS,
259 Self::ZToString => Z_TO_STRING,
260 Self::BoolToString => BOOL_TO_STRING,
261 Self::Euclid => EUCLID,
262 Self::JsNumber => JS_NUMBER,
263 Self::JsConsole => JS_CONSOLE,
264 Self::FloatSame => FLOAT_SAME,
265 Self::FloatRem => FLOAT_REM,
266 Self::Math => MATH,
267 Self::ListAt => LIST_AT,
268 Self::FloatIndex => FLOAT_INDEX,
269 Self::Fix => FIX,
270 Self::Tactics => TACTICS,
271 Self::Decide => DECIDE,
272 Self::Emit => EMIT,
273 }
274 }
275}
276
277fn ident(name: &str) -> String {
278 let mut result: String = name
279 .chars()
280 .map(|c| {
281 if c.is_ascii_alphanumeric() || c == '_' || c == '\'' {
282 c
283 } else {
284 '_'
285 }
286 })
287 .collect();
288 if result.starts_with(|c: char| c.is_ascii_digit() || c == '\'') {
289 result = format!("x{result}");
290 }
291 if KEYWORDS.contains(&result.as_str()) {
292 result.push('_');
293 }
294 result
295}
296
297fn generated(name: &str) -> Vec<String> {
299 [
300 "_ind",
301 "_rect",
302 "_rec",
303 "_sind",
304 "_equation",
305 "_tcc",
306 "_terminate",
307 "_F",
308 "_graph",
309 "_complete",
310 "_correct",
311 "_ind_",
312 "_rect_",
313 ]
314 .iter()
315 .map(|suffix| format!("{name}{suffix}"))
316 .chain(std::iter::once(format!("R_{name}")))
317 .collect()
318}
319
320fn internal(message: String) -> TranslationError {
322 TranslationError::new(ErrorKind::Type, message, None)
323}
324
325pub fn emit_rocq(source: &Program) -> Result<Emitted> {
330 let program = &*thread_output(source)?;
331 let state = EmitState::new(
332 program,
333 Language::Rocq,
334 ident,
335 KEYWORDS,
336 EmitOptions {
337 ctor_style: CtorStyle::Module,
338 generated: Some(generated),
339 ..EmitOptions::default()
340 },
341 );
342 RocqEmitter {
343 program,
344 state,
345 helpers: HashSet::from([Helper::Tactics]),
346 nat_functions: HashMap::new(),
347 current: None,
348 uses_float: false,
349 }
350 .file()
351}
352
353struct Current {
355 full_name: String,
356 name: String,
357 recursive: bool,
358 general: bool,
360 fuel: bool,
362 mutual: Option<Rc<Mutual>>,
364}
365
366struct RocqEmitter<'p> {
367 program: &'p Program,
368 state: EmitState<'p>,
369 helpers: HashSet<Helper>,
370 nat_functions: HashMap<String, (usize, usize)>,
372 current: Option<Current>,
373 uses_float: bool,
374}
375
376impl RocqEmitter<'_> {
377 fn file(mut self) -> Result<Emitted> {
378 let program = self.program;
379 let mut blocks: Vec<String> = Vec::new();
380 let order = order_declarations(program, true)?;
381 let mut open_modules: Vec<String> = Vec::new();
382 let groups = mutual_groups(program);
383 for entry in order {
384 self.move_to(&mut open_modules, entry.module_path(), &mut blocks);
385 blocks.push(match (entry, groups.get(entry.full_name())) {
386 (_, Some(group)) if std::ptr::eq(group[0], entry) => self.mutual(group)?,
387 (_, Some(_)) => continue,
388 (Decl::Data(data), None) => self.data(entry, data)?,
389 (Decl::Fn(function), None) => self.function(entry, function)?,
390 (Decl::Theorem(theorem), None) => self.theorem(entry, theorem)?,
391 });
392 }
393 self.move_to(&mut open_modules, &[], &mut blocks);
394 let main_text = match &program.main {
395 Some(main) => Some(self.main(main)?),
396 None => None,
397 };
398 let helper_text = Helper::ALL.into_iter().filter(|helper| {
399 self.helpers.contains(helper)
400 || (*helper == Helper::Digits
401 && (self.helpers.contains(&Helper::ZToString)
402 || self.helpers.contains(&Helper::JsNumber)))
403 });
404 let mut lines: Vec<String> = vec![format!(
405 "(* Translated from {} by meta-language: portable core, Rocq target. *)",
406 program.source_language.as_str()
407 )];
408 let prelude = if self.uses_float {
409 FLOAT_PRELUDE
410 } else {
411 ROCQ_PRELUDE
412 };
413 lines.extend(prelude.iter().map(|line| (*line).to_owned()));
414 lines.push(String::new());
415 for helper in helper_text {
416 lines.push(helper.text().to_owned());
417 lines.push(String::new());
418 }
419 for block in blocks {
420 lines.push(block);
421 lines.push(String::new());
422 }
423 let entry = main_text.as_ref().map(|_| "main".to_owned());
424 if let Some(main_text) = main_text {
425 lines.push(main_text);
426 lines.push(String::new());
427 lines.push("Eval vm_compute in main.".to_owned());
428 lines.push(String::new());
429 }
430 Ok(self.state.finish(lines.join("\n"), entry))
431 }
432
433 fn move_to(&self, open_modules: &mut Vec<String>, path: &[String], blocks: &mut Vec<String>) {
435 let common = open_modules
436 .iter()
437 .zip(path)
438 .take_while(|(open, wanted)| open == wanted)
439 .count();
440 while open_modules.len() > common {
441 blocks.push(format!("End {}.", self.module_name(open_modules)));
442 open_modules.pop();
443 }
444 while open_modules.len() < path.len() {
445 open_modules.push(path[open_modules.len()].clone());
446 blocks.push(format!("Module {}.", self.module_name(open_modules)));
447 }
448 }
449
450 fn module_name(&self, path: &[String]) -> &str {
451 self.state.module_name(path).unwrap_or("undefined")
452 }
453
454 fn ty(&mut self, ty: &Type) -> Result<String> {
455 Ok(match ty {
456 Type::Nat => "N".to_owned(),
457 Type::Int => "Z".to_owned(),
458 Type::Fixed { signed, .. } => {
459 self.state
460 .machine_integer(ty, if *signed { "Z" } else { "N" });
461 if *signed { "Z" } else { "N" }.to_owned()
462 }
463 Type::Float => {
464 self.floats();
465 "float".to_owned()
466 }
467 Type::Bool => "bool".to_owned(),
468 Type::String => "string".to_owned(),
469 Type::Unit => "unit".to_owned(),
470 Type::Output => "list string".to_owned(),
471 Type::Data { name } => self.state.reference(name, "."),
472 Type::Array { element } => {
473 self.state.encode(
474 "arrays",
475 "a JavaScript array, which the portable core never mutates, is a Rocq list; a read walks it to the index",
476 );
477 format!("(list {})", self.ty(element)?)
478 }
479 other => return Err(internal(format!("no Rocq type for {}", other.kind()))),
480 })
481 }
482
483 fn data(&mut self, entry: &Decl, data: &DataDecl) -> Result<String> {
484 fn lists(ty: &Type) -> bool {
485 ty.element()
486 .is_some_and(|element| matches!(element, Type::Data { .. }) || lists(element))
487 }
488 let name = self.state.local_name(&data.full_name).to_owned();
489 let mut lines = vec![format!("Inductive {name} : Type :=")];
490 for ctor in &data.ctors {
491 let mut fields = String::new();
492 for field in &ctor.fields {
493 fields.push_str(&self.ty(&field.ty)?);
494 fields.push_str(" -> ");
495 }
496 let local = self.state.ctor_local(&data.full_name, &ctor.name);
497 lines.push(format!("| {local} : {fields}{name}"));
498 }
499 self.state.map(entry, &name);
500 let nested = data
502 .ctors
503 .iter()
504 .any(|ctor| ctor.fields.iter().any(|field| lists(&field.ty)));
505 if nested {
506 lines.insert(0, "#[warnings=\"-register-all\"]".to_owned());
507 }
508 Ok(lines.join("\n") + ".")
509 }
510
511 #[allow(clippy::too_many_arguments)]
516 fn fuel_recursion(
517 &mut self,
518 name: &str,
519 params: &[Param],
520 binders: &str,
521 result: &str,
522 text: &str,
523 decreasing: &str,
524 ret: &Type,
525 ) -> Result<String> {
526 self.state.encode(
527 "nat-fuel-recursion",
528 "a threaded function that decreases a natural through more than one call is a fixpoint over a fuel of one more than the natural, which every call spends as it decreases the natural, so the fuel outlasts every run",
529 );
530 let args: Vec<&str> = params.iter().map(|param| param.name.as_str()).collect();
531 Ok(format!(
532 "Definition {name}{binders} : {result} :=\n (fix ml_go (ml_fuel : nat){binders} {{struct ml_fuel}} : {result} :=\n match ml_fuel with\n | O => {}\n | S ml_fuel => {text}\n end) (S (N.to_nat {decreasing})) {}.",
533 self.inhabitant(ret)?,
534 args.join(" ")
535 ))
536 }
537
538 fn general_recursion(
543 &mut self,
544 name: &str,
545 params: &[Param],
546 binders: &str,
547 result: &str,
548 text: &str,
549 ret: &Type,
550 ) -> Result<String> {
551 self.helpers.insert(Helper::Fix);
552 self.state.encode(
553 "general-recursion",
554 "recursion without a termination argument Rocq could check is ml_fix over the function's one-step unfolding, which unfolds lazily to 2^64 nested calls: every run that terminates computes the same value, and Rocq accepts it as a plain Definition",
555 );
556 let mut domain = Vec::new();
557 for param in params {
558 domain.push(self.ty(¶m.ty)?);
559 }
560 let domain = if domain.is_empty() {
561 "unit".to_owned()
562 } else {
563 domain.join(" * ")
564 };
565 let names: Vec<&str> = params.iter().map(|param| param.name.as_str()).collect();
566 let tuple = format!("({})", names.join(", "));
567 let (input, unpack, args) = match names.as_slice() {
568 [] => ("_", String::new(), "tt"),
569 [only] => (*only, String::new(), *only),
570 _ => (
571 "ml_args",
572 format!("let '{tuple} := ml_args in "),
573 tuple.as_str(),
574 ),
575 };
576 Ok(format!(
577 "Definition {name}{binders} : {result} :=\n ml_fix 64 (fun (ml_rec : {domain} -> {result}) ({input} : {domain}) =>\n {unpack}{text})\n (fun _ => {}) {args}.",
578 self.inhabitant(ret)?
579 ))
580 }
581
582 fn function(&mut self, entry: &Decl, function: &FnDecl) -> Result<String> {
583 let (params, body) = rename_function(function, &ident, &self.state.local_reserved());
584 let name = self.state.local_name(&function.full_name).to_owned();
585 self.state.map(entry, &name);
586 let general = function.recursive && function.decreasing.is_none() && !function.mutual;
587 let fuel = !general
589 && function.recursive
590 && !function.mutual
591 && !function
592 .decreasing
593 .and_then(|index| params.get(index))
594 .is_some_and(|param| matches!(param.ty, Type::Data { .. }))
595 && matches!(&function.ret, Type::Data { name: ret } if matches!(self.program.declaration(ret), Some(Decl::Data(data)) if data.output))
596 && self_calls(&body, &function.full_name) > 1;
597 self.current = Some(Current {
598 full_name: function.full_name.clone(),
599 name: name.clone(),
600 recursive: function.recursive,
601 general,
602 fuel,
603 mutual: None,
604 });
605 let mut binders = String::new();
606 for param in ¶ms {
607 let ty = self.ty(¶m.ty)?;
608 let _ = write!(binders, " ({} : {ty})", param.name);
609 }
610 let result = self.ty(&function.ret)?;
611 let text = self.expr(&body)?;
612 self.current = None;
613 if !function.recursive {
614 return Ok(format!(
615 "Definition {name}{binders} : {result} :=\n {text}."
616 ));
617 }
618 let Some(index) = function.decreasing else {
619 return self.general_recursion(&name, ¶ms, &binders, &result, &text, &function.ret);
620 };
621 let decreasing = ¶ms[index];
622 if matches!(decreasing.ty, Type::Data { .. }) {
623 return Ok(format!(
624 "Fixpoint {name}{binders} {{struct {}}} : {result} :=\n {text}.",
625 decreasing.name
626 ));
627 }
628 if fuel {
629 return self.fuel_recursion(
630 &name,
631 ¶ms,
632 &binders,
633 &result,
634 &text,
635 &decreasing.name,
636 &function.ret,
637 );
638 }
639 self.nat_functions
640 .insert(function.full_name.clone(), (index, params.len()));
641 self.state.encode(
642 "nat-recursion",
643 "recursion that decreases a natural is a Rocq Function with measure N.to_nat; lia discharges the decrease obligations",
644 );
645 Ok(format!(
646 "Function {name}{binders} {{measure N.to_nat {}}} : {result} :=\n {text}.\nProof. all: ml_obligation. Defined.",
647 decreasing.name
648 ))
649 }
650}
651
652const fn op_name(op: BinaryOp) -> &'static str {
654 match op {
655 BinaryOp::Add => "add",
656 BinaryOp::Sub => "sub",
657 BinaryOp::Mul => "mul",
658 BinaryOp::Div => "div",
659 BinaryOp::Rem => "rem",
660 BinaryOp::Eq => "eq",
661 BinaryOp::Ne => "ne",
662 BinaryOp::Lt => "lt",
663 BinaryOp::Le => "le",
664 BinaryOp::Gt => "gt",
665 BinaryOp::Ge => "ge",
666 BinaryOp::And => "and",
667 BinaryOp::Or => "or",
668 BinaryOp::Concat => "concat",
669 BinaryOp::Plus => "plus",
670 }
671}
672
673fn self_calls(node: &Expr, name: &str) -> usize {
675 let own = usize::from(matches!(&node.node, Node::Call { func, .. } if func == name));
676 own + node
677 .children()
678 .into_iter()
679 .map(|child| self_calls(child, name))
680 .sum::<usize>()
681}
682
683fn collect_hint_functions(plan: &Plan) -> Vec<&String> {
684 match plan {
685 Plan::Close { hints } => hints.unfold.iter().collect(),
686 Plan::Induction(split) | Plan::Cases(split) => split
687 .cases
688 .iter()
689 .flat_map(|kase| collect_hint_functions(&kase.plan))
690 .collect(),
691 }
692}