1pub mod ir;
48
49mod generate;
50
51pub(crate) mod emit;
52pub(crate) mod lower;
53pub(crate) mod map;
54
55pub use canton_lf;
59
60pub use generate::{GenerateError, Options, Runtime, Stats, default_crate_name, generate};
61pub use lower::{
62 ExternalPackages, Selection, SkippedType, lower_dar, lower_dar_selecting, lower_dar_with,
63 lower_packages_selecting,
64};
65
66use proc_macro2::TokenStream;
67
68#[derive(Debug)]
75pub struct CodegenError(syn::Error);
76
77impl std::fmt::Display for CodegenError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "generated source is not valid Rust: {}", self.0)
80 }
81}
82
83impl std::error::Error for CodegenError {}
84
85use crate::ir::{Crate, DataType, Module, Record, Template};
86
87const MODULE_PREAMBLE: &str = concat!(
91 "#![allow(non_camel_case_types, non_snake_case, unused_imports, dead_code, unused_variables, clippy::all)]\n",
92 "//! Generated Daml bindings — do not edit by hand.\n\n",
93 "use canton_daml as rt;\n\n",
94);
95
96const CRATE_LINTS: &str = "#![forbid(unsafe_code)]\n\
104 #![allow(non_camel_case_types, non_snake_case, unused_imports, clippy::all)]\n";
105
106fn crate_docs(krate: &Crate) -> String {
110 use std::fmt::Write as _;
111
112 let mut docs = String::new();
113 docs.push_str("//! Typed Rust bindings generated from a Daml archive (DAR).\n//!\n");
114 docs.push_str(
115 "//! **Generated file — do not edit by hand.** Regenerate with\n\
116 //! `dpm-codegen-rust` from the source this crate's `Cargo.toml` records\n\
117 //! (a DAR, or a participant's package payloads); edits are lost.\n//!\n\
118 //! Each Daml package in the DAR's dependency closure is one top-level\n\
119 //! module, and each Daml module a submodule under it, so cross-package\n\
120 //! references resolve and names never collide. Templates carry typed\n\
121 //! choices; the runtime traits they implement (`Template`, `Choice`,\n\
122 //! `Contract`) and the command builders live in `canton-daml`, which the\n\
123 //! generated code imports as `rt`.\n//!\n\
124 //! # Packages in this crate\n//!\n",
125 );
126 for package in &krate.packages {
127 let modules = package.modules.len();
128 let plural = if modules == 1 { "module" } else { "modules" };
129 let _ = writeln!(docs, "//! - [`{}`] — {modules} Daml {plural}", package.name);
130 }
131 docs.push('\n');
132 docs
133}
134
135fn format_items(tokens: TokenStream) -> Result<String, CodegenError> {
141 let file: syn::File = syn::parse2(tokens).map_err(CodegenError)?;
142 Ok(prettyplease::unparse(&file))
143}
144
145pub fn generate_record(record: &Record) -> Result<String, CodegenError> {
150 format_items(emit::record_items(record))
151}
152
153pub fn generate_data_type(data_type: &DataType) -> Result<String, CodegenError> {
158 format_items(emit::data_type(data_type))
159}
160
161pub fn generate_template(template: &Template) -> Result<String, CodegenError> {
167 format_items(emit::template(template))
168}
169
170pub fn generate_interface(interface: &crate::ir::Interface) -> Result<String, CodegenError> {
176 format_items(emit::interface(interface))
177}
178
179pub fn generate_module(module: &Module) -> Result<String, CodegenError> {
186 let body = format_items(emit::module_items(module))?;
187 Ok(format!("{MODULE_PREAMBLE}{body}"))
188}
189
190pub fn generate_crate(krate: &Crate) -> Result<String, CodegenError> {
197 let body = format_items(emit::crate_items(krate))?;
198 Ok(format!("{CRATE_LINTS}{}{body}", crate_docs(krate)))
199}
200
201#[cfg(test)]
202#[allow(clippy::unwrap_used, clippy::expect_used)]
203mod tests {
204 use super::*;
205 use crate::ir::{DamlType, Field, Record, TypeRef, Variant, VariantConstructor};
206
207 fn field(label: &str, ty: DamlType) -> Field {
208 Field {
209 label: label.to_string(),
210 ty,
211 }
212 }
213
214 #[test]
215 fn generates_valid_rust_for_a_record() {
216 let record = Record {
217 name: "AppInstallRequest".to_string(),
218 type_params: Vec::new(),
219 fields: vec![
220 field("provider", DamlType::Party),
221 field("installId", DamlType::Text),
222 field("amount", DamlType::Numeric(10)),
223 field(
224 "cid",
225 DamlType::ContractId(Box::new(DamlType::Ref(TypeRef::local(
226 "Foo",
227 Vec::new(),
228 )))),
229 ),
230 field("tags", DamlType::List(Box::new(DamlType::Text))),
231 field("note", DamlType::Optional(Box::new(DamlType::Text))),
232 field("type", DamlType::Text),
234 ],
235 };
236
237 let src = generate_record(&record).expect("generator emits valid Rust");
238 syn::parse_file(&src).expect("output must be valid Rust");
240
241 assert!(src.contains("pub struct AppInstallRequest"), "{src}");
243 assert!(src.contains("pub provider: rt::Party"), "{src}");
244 assert!(
245 src.contains("pub install_id: ::std::string::String"),
246 "{src}"
247 );
248 assert!(src.contains("pub amount: rt::Numeric"), "{src}");
249 assert!(src.contains("rt::ContractId<Foo>"), "{src}");
250 assert!(src.contains("Vec<::std::string::String>"), "{src}");
251 assert!(src.contains("Option<::std::string::String>"), "{src}");
252 assert!(src.contains("r#type: ::std::string::String"), "{src}");
253 }
254
255 #[test]
256 fn only_a_renamed_field_carries_a_doc() {
257 let record = Record {
258 name: "Payload".to_string(),
259 type_params: Vec::new(),
260 fields: vec![
261 field("owner", DamlType::Party),
262 field("acceptArg", DamlType::Text),
263 field("type", DamlType::Text),
264 ],
265 };
266
267 let src = generate_record(&record).unwrap();
268 syn::parse_file(&src).unwrap();
269 assert!(src.contains("Daml field `acceptArg`"), "{src}");
272 assert!(!src.contains("Daml field `owner`"), "{src}");
275 assert!(src.contains("r#type"), "{src}");
277 assert!(!src.contains("Daml field `type`"), "{src}");
278 }
279
280 #[test]
281 fn generated_decoders_attach_the_field_name_to_a_failure() {
282 let record = Record {
286 name: "Payload".to_string(),
287 type_params: Vec::new(),
288 fields: vec![
289 field("owner", DamlType::Party),
290 field("note", DamlType::Optional(Box::new(DamlType::Text))),
291 ],
292 };
293 let src = generate_record(&record).unwrap();
294 syn::parse_file(&src).unwrap();
295
296 assert!(src.contains(r#".map_err(|e| e.at("owner"))"#), "{src}");
298 assert!(src.contains(r#".map_err(|e| e.at("note"))"#), "{src}");
299 assert!(
303 src.contains(r#"rt::required_field(value, 0usize, "owner")?)"#),
304 "{src}"
305 );
306 }
307
308 #[test]
309 fn record_emits_value_codecs() {
310 let record = Record {
311 name: "Payload".to_string(),
312 type_params: Vec::new(),
313 fields: vec![
314 field("owner", DamlType::Party),
315 field("count", DamlType::Int64),
316 ],
317 };
318
319 let src = generate_record(&record).unwrap();
320 syn::parse_file(&src).unwrap();
321 assert!(src.contains("impl rt::ToValue for Payload"), "{src}");
322 assert!(src.contains("impl rt::FromValue for Payload"), "{src}");
323 assert!(src.contains("rt::record("), "{src}");
324 assert!(
327 src.contains("rt::required_field(value, 0usize, \"owner\")"),
328 "{src}"
329 );
330 }
331
332 #[test]
333 fn optional_fields_decode_as_absent_tolerant() {
334 let record = Record {
338 name: "Payload".to_string(),
339 type_params: Vec::new(),
340 fields: vec![
341 field("owner", DamlType::Party),
342 field("note", DamlType::Optional(Box::new(DamlType::Text))),
343 ],
344 };
345
346 let src = generate_record(&record).unwrap();
347 syn::parse_file(&src).unwrap();
348 assert!(
349 src.contains("rt::optional_field(value, 1usize, \"note\")"),
350 "{src}"
351 );
352 assert!(
353 src.contains("rt::required_field(value, 0usize, \"owner\")"),
354 "{src}"
355 );
356 }
357
358 #[test]
359 fn nested_optional_maps_to_nested_opt() {
360 let record = Record {
361 name: "R".to_string(),
362 type_params: Vec::new(),
363 fields: vec![
364 field("single", DamlType::Optional(Box::new(DamlType::Text))),
365 field(
366 "nested",
367 DamlType::Optional(Box::new(DamlType::Optional(Box::new(DamlType::Text)))),
368 ),
369 ],
370 };
371
372 let src = generate_record(&record).unwrap();
373 syn::parse_file(&src).unwrap();
374 assert!(
376 src.contains("pub single: ::core::option::Option<::std::string::String>"),
377 "{src}"
378 );
379 assert!(
381 src.contains(
382 "pub nested: ::core::option::Option<rt::NestedOpt<::std::string::String>>"
383 ),
384 "{src}"
385 );
386 }
387
388 #[test]
389 fn record_derives_the_json_codec() {
390 let record = Record {
391 name: "Payload".to_string(),
392 type_params: Vec::new(),
393 fields: vec![field("installId", DamlType::Text)],
394 };
395
396 let src = generate_record(&record).unwrap();
397 syn::parse_file(&src).unwrap();
398 assert!(src.contains("rt::serde::Serialize"), "{src}");
399 assert!(src.contains("rt::serde::Deserialize"), "{src}");
400 assert!(src.contains("serde(crate = \"rt::serde\")"), "{src}");
401 assert!(src.contains("serde(rename = \"installId\")"), "{src}");
403 assert!(
404 src.contains("pub install_id: ::std::string::String"),
405 "{src}"
406 );
407 }
408
409 #[test]
410 fn variant_generates_a_rust_enum() {
411 use crate::ir::{Variant, VariantConstructor};
412
413 let variant = Variant {
414 name: "Shape".to_string(),
415 type_params: Vec::new(),
416 constructors: vec![
417 VariantConstructor {
418 name: "Circle".to_string(),
419 payload: Some(DamlType::Numeric(10)),
420 },
421 VariantConstructor {
422 name: "Point".to_string(),
423 payload: None,
424 },
425 ],
426 };
427
428 let src = generate_data_type(&DataType::Variant(variant)).unwrap();
429 syn::parse_file(&src).unwrap();
430 assert!(src.contains("pub enum Shape"), "{src}");
431 assert!(src.contains("Circle(rt::Numeric)"), "{src}");
432 assert!(src.contains("Point(rt::Unit)"), "{src}");
436 }
437
438 #[test]
439 fn enum_generates_a_c_like_enum() {
440 use crate::ir::Enum;
441
442 let enumeration = Enum {
443 name: "DayOfWeek".to_string(),
444 constructors: vec!["Monday".to_string(), "Tuesday".to_string()],
445 };
446
447 let src = generate_data_type(&DataType::Enum(enumeration)).unwrap();
448 syn::parse_file(&src).unwrap();
449 assert!(src.contains("pub enum DayOfWeek"), "{src}");
450 assert!(src.contains("Monday,"), "{src}");
451 assert!(src.contains("Copy"), "{src}");
453 }
454
455 #[test]
456 fn template_generates_payload_and_typed_choices() {
457 use crate::ir::{Choice, Template, TypeRef};
458
459 let template = Template {
460 name: "AppInstall".to_string(),
461 module_name: "Licensing.AppInstall".to_string(),
462 package_id: "abc123".to_string(),
463 package_name: "app-install".to_string(),
464 fields: vec![field("provider", DamlType::Party)],
465 choices: vec![Choice {
466 name: "Accept".to_string(),
467 consuming: true,
468 argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
469 returns: DamlType::ContractId(Box::new(DamlType::Ref(TypeRef::local(
470 "AppInstalled",
471 Vec::new(),
472 )))),
473 }],
474 key: None,
475 };
476
477 let src = generate_template(&template).unwrap();
478 syn::parse_file(&src).unwrap();
479 assert!(src.contains("pub struct AppInstall"), "{src}");
481 assert!(src.contains("pub provider: rt::Party"), "{src}");
482 assert!(
484 src.contains("impl rt::Choice<AppInstall> for AppInstall_Accept"),
485 "{src}"
486 );
487 assert!(
488 src.contains("type Return = rt::ContractId<AppInstalled>"),
489 "{src}"
490 );
491 assert!(
492 src.contains("const NAME: &'static str = \"Accept\""),
493 "{src}"
494 );
495 assert!(src.contains("const CONSUMING: bool = true"), "{src}");
496 assert!(src.contains("impl rt::Template for AppInstall"), "{src}");
498 assert!(
499 src.contains("const MODULE_NAME: &'static str = \"Licensing.AppInstall\""),
500 "{src}"
501 );
502 assert!(
503 src.contains("const ENTITY_NAME: &'static str = \"AppInstall\""),
504 "{src}"
505 );
506 assert!(
507 src.contains("const PACKAGE_NAME: &'static str = \"app-install\""),
508 "{src}"
509 );
510 }
511
512 #[test]
513 fn a_template_documents_its_identity_and_its_choices() {
514 use crate::ir::{Choice, Template, TypeRef};
515
516 let template = Template {
517 name: "AppInstall".to_string(),
518 module_name: "Licensing.AppInstall".to_string(),
519 package_id: "abc123".to_string(),
520 package_name: "app-install".to_string(),
521 fields: vec![field("provider", DamlType::Party)],
522 choices: vec![
523 Choice {
524 name: "Accept".to_string(),
525 consuming: true,
526 argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
527 returns: DamlType::Unit,
528 },
529 Choice {
530 name: "Peek".to_string(),
531 consuming: false,
532 argument: DamlType::Ref(TypeRef::local("AppInstall_Peek", Vec::new())),
533 returns: DamlType::Unit,
534 },
535 ],
536 key: None,
537 };
538
539 let src = generate_template(&template).unwrap();
540 syn::parse_file(&src).unwrap();
541 assert!(
544 src.contains("The Daml template `Licensing.AppInstall:AppInstall`"),
545 "{src}"
546 );
547 assert!(
548 src.contains("`#app-install:Licensing.AppInstall:AppInstall`"),
549 "{src}"
550 );
551 assert!(src.contains("- `Accept` — consuming"), "{src}");
552 assert!(src.contains("- `Peek` — non-consuming"), "{src}");
553 assert!(!src.contains("/**"), "{src}");
558 }
559
560 #[test]
565 fn hostile_names_cannot_open_a_doctest_in_generated_docs() {
566 use crate::ir::{Choice, Template};
567
568 let payload = "```\nfn main() { std::process::exit(7) }\n```";
569 let template = Template {
570 name: "Deal".to_string(),
571 module_name: "Market".to_string(),
572 package_id: "abc123".to_string(),
573 package_name: format!("market\n{payload}"),
574 fields: vec![field(&format!("owner\n {payload}"), DamlType::Party)],
575 choices: vec![Choice {
576 name: format!("Accept\n{payload}\n"),
577 consuming: true,
578 argument: DamlType::Unit,
579 returns: DamlType::Unit,
580 }],
581 key: None,
582 };
583
584 let src = generate_template(&template).unwrap();
585 syn::parse_file(&src).unwrap();
586 assert!(!src.contains("/**"), "{src}");
587 for line in src.lines() {
590 assert!(!line.trim_start().starts_with("```"), "{src}");
591 assert!(
592 !line.trim_start().starts_with("///") || !line.contains('\n'),
593 "{src}"
594 );
595 }
596 assert!(!src.contains("/// ```"), "{src}");
597 }
598
599 #[test]
600 fn keyed_template_emits_with_key_impl() {
601 use crate::ir::{Template, TypeRef};
602
603 let template = Template {
604 name: "Account".to_string(),
605 module_name: "Bank.Account".to_string(),
606 package_id: "abc123".to_string(),
607 package_name: "bank".to_string(),
608 fields: vec![field("owner", DamlType::Party)],
609 choices: Vec::new(),
610 key: Some(DamlType::Ref(TypeRef::local("AccountKey", Vec::new()))),
612 };
613
614 let src = generate_template(&template).unwrap();
615 syn::parse_file(&src).unwrap();
616 assert!(src.contains("impl rt::WithKey for Account"), "{src}");
617 assert!(src.contains("type Key = AccountKey"), "{src}");
618 }
619
620 #[test]
621 fn interface_emits_identity_view_and_choice_impls() {
622 use crate::ir::{Choice, Interface, TypeRef};
623
624 let interface = Interface {
625 name: "Holding".to_string(),
626 module_name: "Splice.Api.Token.HoldingV1".to_string(),
627 package_id: "abc123".to_string(),
628 package_name: "splice-api-token-holding".to_string(),
629 view: Some(DamlType::Ref(TypeRef::local("HoldingView", Vec::new()))),
630 choices: vec![Choice {
631 name: "Transfer".to_string(),
632 consuming: true,
633 argument: DamlType::Ref(TypeRef::local("Holding_Transfer", Vec::new())),
634 returns: DamlType::Unit,
635 }],
636 };
637
638 let src = generate_interface(&interface).unwrap();
639 syn::parse_file(&src).unwrap();
640 assert!(src.contains("impl rt::Contract for Holding"), "{src}");
642 assert!(src.contains("impl rt::Interface for Holding"), "{src}");
643 assert!(src.contains("type View = HoldingView"), "{src}");
644 assert!(
646 src.contains("impl rt::Choice<Holding> for Holding_Transfer"),
647 "{src}"
648 );
649 }
650
651 #[test]
652 fn keyless_template_emits_no_with_key_impl() {
653 use crate::ir::Template;
654
655 let template = Template {
656 name: "Note".to_string(),
657 module_name: "M".to_string(),
658 package_id: "p".to_string(),
659 package_name: "m".to_string(),
660 fields: vec![field("text", DamlType::Text)],
661 choices: Vec::new(),
662 key: None,
663 };
664
665 let src = generate_template(&template).unwrap();
666 syn::parse_file(&src).unwrap();
667 assert!(
668 !src.contains("WithKey"),
669 "keyless template has no key impl: {src}"
670 );
671 }
672
673 #[test]
674 fn module_wraps_items_with_the_runtime_preamble() {
675 use crate::ir::{Choice, Enum, Module, Template};
676
677 let module = Module {
678 data_types: vec![DataType::Enum(Enum {
679 name: "Color".to_string(),
680 constructors: vec!["Red".to_string(), "Green".to_string()],
681 })],
682 templates: vec![Template {
683 name: "AppInstall".to_string(),
684 module_name: "Licensing.AppInstall".to_string(),
685 package_id: "abc123".to_string(),
686 package_name: "app-install".to_string(),
687 fields: vec![field("provider", DamlType::Party)],
688 choices: vec![Choice {
689 name: "Accept".to_string(),
690 consuming: true,
691 argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
692 returns: DamlType::Unit,
693 }],
694 key: None,
695 }],
696 interfaces: Vec::new(),
697 };
698
699 let src = generate_module(&module).unwrap();
700 syn::parse_file(&src).unwrap();
702 assert!(src.contains("use canton_daml as rt;"), "{src}");
703 assert!(src.contains("#![allow(non_camel_case_types"), "{src}");
704 assert!(src.contains("pub enum Color"), "{src}");
705 assert!(src.contains("pub struct AppInstall"), "{src}");
706 assert!(
707 src.contains("impl rt::Choice<AppInstall> for AppInstall_Accept"),
708 "{src}"
709 );
710 }
711
712 #[test]
713 fn generic_record_carries_type_params() {
714 let record = Record {
715 name: "Pair".to_string(),
716 type_params: vec!["a".to_string(), "b".to_string()],
717 fields: vec![
718 field("fst", DamlType::Var("a".to_string())),
719 field("snd", DamlType::Var("b".to_string())),
720 ],
721 };
722
723 let src = generate_record(&record).unwrap();
724 syn::parse_file(&src).unwrap();
725 assert!(src.contains("pub struct Pair<A, B>"), "{src}");
727 assert!(src.contains("pub fst: A"), "{src}");
728 }
729 #[test]
738 fn every_keyword_in_the_list_is_escaped_in_a_field_name() {
739 for (daml, expected) in [
742 ("type", "r#type:"),
743 ("match", "r#match:"),
744 ("gen", "r#gen:"),
745 ("try", "r#try:"),
746 ("async", "r#async:"),
747 ("box", "r#box:"),
748 ("crate", "crate_:"),
749 ("self", "self_:"),
750 ] {
751 let record = Record {
752 name: "Keyworded".to_string(),
753 type_params: Vec::new(),
754 fields: vec![field(daml, DamlType::Text)],
755 };
756 let src = generate_record(&record).expect("generator emits valid Rust");
757 assert!(
758 src.contains(expected),
759 "a Daml field named `{daml}` should emit `{expected}`:\n{src}"
760 );
761 assert!(
764 src.contains(&format!("#[serde(rename = \"{daml}\")]")),
765 "the wire label `{daml}` must survive:\n{src}"
766 );
767 }
768 }
769 #[test]
779 fn a_phantom_type_parameter_is_not_forced_to_carry_a_codec() {
780 let mut record = Record::new("Wrapper");
781 record.type_params = vec!["a".to_string(), "b".to_string()];
782 record.fields = vec![Field::new("value", DamlType::Var("b".to_string()))];
784
785 let src = generate_data_type(&DataType::Record(record)).unwrap();
786 assert!(
787 src.contains("B: rt::ToValue"),
788 "the used parameter is bounded: {src}"
789 );
790 assert!(
791 !src.contains("A: rt::ToValue"),
792 "the phantom parameter must not be bounded: {src}"
793 );
794 assert!(src.contains("pub struct Wrapper<A, B>"), "{src}");
796 assert!(src.contains("PhantomData"), "{src}");
797 }
798
799 #[test]
802 fn a_variants_phantom_parameter_is_not_bounded_either() {
803 let mut variant = Variant::new("Choice");
804 variant.type_params = vec!["a".to_string(), "b".to_string()];
805 variant.constructors = vec![
806 VariantConstructor::with_payload("Some", DamlType::Var("b".to_string())),
807 VariantConstructor::new("None"),
808 ];
809
810 let src = generate_data_type(&DataType::Variant(variant)).unwrap();
811 assert!(src.contains("B: rt::ToValue"), "{src}");
812 assert!(!src.contains("A: rt::ToValue"), "{src}");
813 }
814}