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::{ExternalPackages, SkippedType, lower_dar, lower_dar_with};
62
63use proc_macro2::TokenStream;
64
65#[derive(Debug)]
72pub struct CodegenError(syn::Error);
73
74impl std::fmt::Display for CodegenError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "generated source is not valid Rust: {}", self.0)
77 }
78}
79
80impl std::error::Error for CodegenError {}
81
82use crate::ir::{Crate, DataType, Module, Record, Template};
83
84const MODULE_PREAMBLE: &str = concat!(
88 "#![allow(non_camel_case_types, non_snake_case, unused_imports, dead_code, unused_variables, clippy::all)]\n",
89 "//! Generated Daml bindings — do not edit by hand.\n\n",
90 "use canton_daml as rt;\n\n",
91);
92
93const CRATE_LINTS: &str =
98 "#![allow(non_camel_case_types, non_snake_case, unused_imports, clippy::all)]\n";
99
100fn crate_docs(krate: &Crate) -> String {
104 use std::fmt::Write as _;
105
106 let mut docs = String::new();
107 docs.push_str("//! Typed Rust bindings generated from a Daml archive (DAR).\n//!\n");
108 docs.push_str(
109 "//! **Generated file — do not edit by hand.** Regenerate with\n\
110 //! `dpm-codegen-rust --dar <the DAR> --out <this crate>`; edits are lost.\n//!\n\
111 //! Each Daml package in the DAR's dependency closure is one top-level\n\
112 //! module, and each Daml module a submodule under it, so cross-package\n\
113 //! references resolve and names never collide. Templates carry typed\n\
114 //! choices; the runtime traits they implement (`Template`, `Choice`,\n\
115 //! `Contract`) and the command builders live in `canton-daml`, which the\n\
116 //! generated code imports as `rt`.\n//!\n\
117 //! # Packages in this crate\n//!\n",
118 );
119 for package in &krate.packages {
120 let modules = package.modules.len();
121 let plural = if modules == 1 { "module" } else { "modules" };
122 let _ = writeln!(docs, "//! - [`{}`] — {modules} Daml {plural}", package.name);
123 }
124 docs.push('\n');
125 docs
126}
127
128fn format_items(tokens: TokenStream) -> Result<String, CodegenError> {
134 let file: syn::File = syn::parse2(tokens).map_err(CodegenError)?;
135 Ok(prettyplease::unparse(&file))
136}
137
138pub fn generate_record(record: &Record) -> Result<String, CodegenError> {
143 format_items(emit::record_items(record))
144}
145
146pub fn generate_data_type(data_type: &DataType) -> Result<String, CodegenError> {
151 format_items(emit::data_type(data_type))
152}
153
154pub fn generate_template(template: &Template) -> Result<String, CodegenError> {
160 format_items(emit::template(template))
161}
162
163pub fn generate_interface(interface: &crate::ir::Interface) -> Result<String, CodegenError> {
169 format_items(emit::interface(interface))
170}
171
172pub fn generate_module(module: &Module) -> Result<String, CodegenError> {
179 let body = format_items(emit::module_items(module))?;
180 Ok(format!("{MODULE_PREAMBLE}{body}"))
181}
182
183pub fn generate_crate(krate: &Crate) -> Result<String, CodegenError> {
190 let body = format_items(emit::crate_items(krate))?;
191 Ok(format!("{CRATE_LINTS}{}{body}", crate_docs(krate)))
192}
193
194#[cfg(test)]
195#[allow(clippy::unwrap_used, clippy::expect_used)]
196mod tests {
197 use super::*;
198 use crate::ir::{DamlType, Field, Record, TypeRef, Variant, VariantConstructor};
199
200 fn field(label: &str, ty: DamlType) -> Field {
201 Field {
202 label: label.to_string(),
203 ty,
204 }
205 }
206
207 #[test]
208 fn generates_valid_rust_for_a_record() {
209 let record = Record {
210 name: "AppInstallRequest".to_string(),
211 type_params: Vec::new(),
212 fields: vec![
213 field("provider", DamlType::Party),
214 field("installId", DamlType::Text),
215 field("amount", DamlType::Numeric(10)),
216 field(
217 "cid",
218 DamlType::ContractId(Box::new(DamlType::Ref(TypeRef::local(
219 "Foo",
220 Vec::new(),
221 )))),
222 ),
223 field("tags", DamlType::List(Box::new(DamlType::Text))),
224 field("note", DamlType::Optional(Box::new(DamlType::Text))),
225 field("type", DamlType::Text),
227 ],
228 };
229
230 let src = generate_record(&record).expect("generator emits valid Rust");
231 syn::parse_file(&src).expect("output must be valid Rust");
233
234 assert!(src.contains("pub struct AppInstallRequest"), "{src}");
236 assert!(src.contains("pub provider: rt::Party"), "{src}");
237 assert!(
238 src.contains("pub install_id: ::std::string::String"),
239 "{src}"
240 );
241 assert!(src.contains("pub amount: rt::Numeric"), "{src}");
242 assert!(src.contains("rt::ContractId<Foo>"), "{src}");
243 assert!(src.contains("Vec<::std::string::String>"), "{src}");
244 assert!(src.contains("Option<::std::string::String>"), "{src}");
245 assert!(src.contains("r#type: ::std::string::String"), "{src}");
246 }
247
248 #[test]
249 fn only_a_renamed_field_carries_a_doc() {
250 let record = Record {
251 name: "Payload".to_string(),
252 type_params: Vec::new(),
253 fields: vec![
254 field("owner", DamlType::Party),
255 field("acceptArg", DamlType::Text),
256 field("type", DamlType::Text),
257 ],
258 };
259
260 let src = generate_record(&record).unwrap();
261 syn::parse_file(&src).unwrap();
262 assert!(src.contains("Daml field `acceptArg`"), "{src}");
265 assert!(!src.contains("Daml field `owner`"), "{src}");
268 assert!(src.contains("r#type"), "{src}");
270 assert!(!src.contains("Daml field `type`"), "{src}");
271 }
272
273 #[test]
274 fn generated_decoders_attach_the_field_name_to_a_failure() {
275 let record = Record {
279 name: "Payload".to_string(),
280 type_params: Vec::new(),
281 fields: vec![
282 field("owner", DamlType::Party),
283 field("note", DamlType::Optional(Box::new(DamlType::Text))),
284 ],
285 };
286 let src = generate_record(&record).unwrap();
287 syn::parse_file(&src).unwrap();
288
289 assert!(src.contains(r#".map_err(|e| e.at("owner"))"#), "{src}");
291 assert!(src.contains(r#".map_err(|e| e.at("note"))"#), "{src}");
292 assert!(
296 src.contains(r#"rt::required_field(value, 0usize, "owner")?)"#),
297 "{src}"
298 );
299 }
300
301 #[test]
302 fn record_emits_value_codecs() {
303 let record = Record {
304 name: "Payload".to_string(),
305 type_params: Vec::new(),
306 fields: vec![
307 field("owner", DamlType::Party),
308 field("count", DamlType::Int64),
309 ],
310 };
311
312 let src = generate_record(&record).unwrap();
313 syn::parse_file(&src).unwrap();
314 assert!(src.contains("impl rt::ToValue for Payload"), "{src}");
315 assert!(src.contains("impl rt::FromValue for Payload"), "{src}");
316 assert!(src.contains("rt::record("), "{src}");
317 assert!(
320 src.contains("rt::required_field(value, 0usize, \"owner\")"),
321 "{src}"
322 );
323 }
324
325 #[test]
326 fn optional_fields_decode_as_absent_tolerant() {
327 let record = Record {
331 name: "Payload".to_string(),
332 type_params: Vec::new(),
333 fields: vec![
334 field("owner", DamlType::Party),
335 field("note", DamlType::Optional(Box::new(DamlType::Text))),
336 ],
337 };
338
339 let src = generate_record(&record).unwrap();
340 syn::parse_file(&src).unwrap();
341 assert!(
342 src.contains("rt::optional_field(value, 1usize, \"note\")"),
343 "{src}"
344 );
345 assert!(
346 src.contains("rt::required_field(value, 0usize, \"owner\")"),
347 "{src}"
348 );
349 }
350
351 #[test]
352 fn nested_optional_maps_to_nested_opt() {
353 let record = Record {
354 name: "R".to_string(),
355 type_params: Vec::new(),
356 fields: vec![
357 field("single", DamlType::Optional(Box::new(DamlType::Text))),
358 field(
359 "nested",
360 DamlType::Optional(Box::new(DamlType::Optional(Box::new(DamlType::Text)))),
361 ),
362 ],
363 };
364
365 let src = generate_record(&record).unwrap();
366 syn::parse_file(&src).unwrap();
367 assert!(
369 src.contains("pub single: ::core::option::Option<::std::string::String>"),
370 "{src}"
371 );
372 assert!(
374 src.contains(
375 "pub nested: ::core::option::Option<rt::NestedOpt<::std::string::String>>"
376 ),
377 "{src}"
378 );
379 }
380
381 #[test]
382 fn record_derives_the_json_codec() {
383 let record = Record {
384 name: "Payload".to_string(),
385 type_params: Vec::new(),
386 fields: vec![field("installId", DamlType::Text)],
387 };
388
389 let src = generate_record(&record).unwrap();
390 syn::parse_file(&src).unwrap();
391 assert!(src.contains("rt::serde::Serialize"), "{src}");
392 assert!(src.contains("rt::serde::Deserialize"), "{src}");
393 assert!(src.contains("serde(crate = \"rt::serde\")"), "{src}");
394 assert!(src.contains("serde(rename = \"installId\")"), "{src}");
396 assert!(
397 src.contains("pub install_id: ::std::string::String"),
398 "{src}"
399 );
400 }
401
402 #[test]
403 fn variant_generates_a_rust_enum() {
404 use crate::ir::{Variant, VariantConstructor};
405
406 let variant = Variant {
407 name: "Shape".to_string(),
408 type_params: Vec::new(),
409 constructors: vec![
410 VariantConstructor {
411 name: "Circle".to_string(),
412 payload: Some(DamlType::Numeric(10)),
413 },
414 VariantConstructor {
415 name: "Point".to_string(),
416 payload: None,
417 },
418 ],
419 };
420
421 let src = generate_data_type(&DataType::Variant(variant)).unwrap();
422 syn::parse_file(&src).unwrap();
423 assert!(src.contains("pub enum Shape"), "{src}");
424 assert!(src.contains("Circle(rt::Numeric)"), "{src}");
425 assert!(src.contains("Point(rt::Unit)"), "{src}");
429 }
430
431 #[test]
432 fn enum_generates_a_c_like_enum() {
433 use crate::ir::Enum;
434
435 let enumeration = Enum {
436 name: "DayOfWeek".to_string(),
437 constructors: vec!["Monday".to_string(), "Tuesday".to_string()],
438 };
439
440 let src = generate_data_type(&DataType::Enum(enumeration)).unwrap();
441 syn::parse_file(&src).unwrap();
442 assert!(src.contains("pub enum DayOfWeek"), "{src}");
443 assert!(src.contains("Monday,"), "{src}");
444 assert!(src.contains("Copy"), "{src}");
446 }
447
448 #[test]
449 fn template_generates_payload_and_typed_choices() {
450 use crate::ir::{Choice, Template, TypeRef};
451
452 let template = Template {
453 name: "AppInstall".to_string(),
454 module_name: "Licensing.AppInstall".to_string(),
455 package_id: "abc123".to_string(),
456 package_name: "app-install".to_string(),
457 fields: vec![field("provider", DamlType::Party)],
458 choices: vec![Choice {
459 name: "Accept".to_string(),
460 consuming: true,
461 argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
462 returns: DamlType::ContractId(Box::new(DamlType::Ref(TypeRef::local(
463 "AppInstalled",
464 Vec::new(),
465 )))),
466 }],
467 key: None,
468 };
469
470 let src = generate_template(&template).unwrap();
471 syn::parse_file(&src).unwrap();
472 assert!(src.contains("pub struct AppInstall"), "{src}");
474 assert!(src.contains("pub provider: rt::Party"), "{src}");
475 assert!(
477 src.contains("impl rt::Choice<AppInstall> for AppInstall_Accept"),
478 "{src}"
479 );
480 assert!(
481 src.contains("type Return = rt::ContractId<AppInstalled>"),
482 "{src}"
483 );
484 assert!(
485 src.contains("const NAME: &'static str = \"Accept\""),
486 "{src}"
487 );
488 assert!(src.contains("const CONSUMING: bool = true"), "{src}");
489 assert!(src.contains("impl rt::Template for AppInstall"), "{src}");
491 assert!(
492 src.contains("const MODULE_NAME: &'static str = \"Licensing.AppInstall\""),
493 "{src}"
494 );
495 assert!(
496 src.contains("const ENTITY_NAME: &'static str = \"AppInstall\""),
497 "{src}"
498 );
499 assert!(
500 src.contains("const PACKAGE_NAME: &'static str = \"app-install\""),
501 "{src}"
502 );
503 }
504
505 #[test]
506 fn a_template_documents_its_identity_and_its_choices() {
507 use crate::ir::{Choice, Template, TypeRef};
508
509 let template = Template {
510 name: "AppInstall".to_string(),
511 module_name: "Licensing.AppInstall".to_string(),
512 package_id: "abc123".to_string(),
513 package_name: "app-install".to_string(),
514 fields: vec![field("provider", DamlType::Party)],
515 choices: vec![
516 Choice {
517 name: "Accept".to_string(),
518 consuming: true,
519 argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
520 returns: DamlType::Unit,
521 },
522 Choice {
523 name: "Peek".to_string(),
524 consuming: false,
525 argument: DamlType::Ref(TypeRef::local("AppInstall_Peek", Vec::new())),
526 returns: DamlType::Unit,
527 },
528 ],
529 key: None,
530 };
531
532 let src = generate_template(&template).unwrap();
533 syn::parse_file(&src).unwrap();
534 assert!(
537 src.contains("The Daml template `Licensing.AppInstall:AppInstall`"),
538 "{src}"
539 );
540 assert!(
541 src.contains("`#app-install:Licensing.AppInstall:AppInstall`"),
542 "{src}"
543 );
544 assert!(src.contains("- `Accept` — consuming"), "{src}");
545 assert!(src.contains("- `Peek` — non-consuming"), "{src}");
546 assert!(!src.contains("/**"), "{src}");
551 }
552
553 #[test]
554 fn keyed_template_emits_with_key_impl() {
555 use crate::ir::{Template, TypeRef};
556
557 let template = Template {
558 name: "Account".to_string(),
559 module_name: "Bank.Account".to_string(),
560 package_id: "abc123".to_string(),
561 package_name: "bank".to_string(),
562 fields: vec![field("owner", DamlType::Party)],
563 choices: Vec::new(),
564 key: Some(DamlType::Ref(TypeRef::local("AccountKey", Vec::new()))),
566 };
567
568 let src = generate_template(&template).unwrap();
569 syn::parse_file(&src).unwrap();
570 assert!(src.contains("impl rt::WithKey for Account"), "{src}");
571 assert!(src.contains("type Key = AccountKey"), "{src}");
572 }
573
574 #[test]
575 fn interface_emits_identity_view_and_choice_impls() {
576 use crate::ir::{Choice, Interface, TypeRef};
577
578 let interface = Interface {
579 name: "Holding".to_string(),
580 module_name: "Splice.Api.Token.HoldingV1".to_string(),
581 package_id: "abc123".to_string(),
582 package_name: "splice-api-token-holding".to_string(),
583 view: Some(DamlType::Ref(TypeRef::local("HoldingView", Vec::new()))),
584 choices: vec![Choice {
585 name: "Transfer".to_string(),
586 consuming: true,
587 argument: DamlType::Ref(TypeRef::local("Holding_Transfer", Vec::new())),
588 returns: DamlType::Unit,
589 }],
590 };
591
592 let src = generate_interface(&interface).unwrap();
593 syn::parse_file(&src).unwrap();
594 assert!(src.contains("impl rt::Contract for Holding"), "{src}");
596 assert!(src.contains("impl rt::Interface for Holding"), "{src}");
597 assert!(src.contains("type View = HoldingView"), "{src}");
598 assert!(
600 src.contains("impl rt::Choice<Holding> for Holding_Transfer"),
601 "{src}"
602 );
603 }
604
605 #[test]
606 fn keyless_template_emits_no_with_key_impl() {
607 use crate::ir::Template;
608
609 let template = Template {
610 name: "Note".to_string(),
611 module_name: "M".to_string(),
612 package_id: "p".to_string(),
613 package_name: "m".to_string(),
614 fields: vec![field("text", DamlType::Text)],
615 choices: Vec::new(),
616 key: None,
617 };
618
619 let src = generate_template(&template).unwrap();
620 syn::parse_file(&src).unwrap();
621 assert!(
622 !src.contains("WithKey"),
623 "keyless template has no key impl: {src}"
624 );
625 }
626
627 #[test]
628 fn module_wraps_items_with_the_runtime_preamble() {
629 use crate::ir::{Choice, Enum, Module, Template};
630
631 let module = Module {
632 data_types: vec![DataType::Enum(Enum {
633 name: "Color".to_string(),
634 constructors: vec!["Red".to_string(), "Green".to_string()],
635 })],
636 templates: vec![Template {
637 name: "AppInstall".to_string(),
638 module_name: "Licensing.AppInstall".to_string(),
639 package_id: "abc123".to_string(),
640 package_name: "app-install".to_string(),
641 fields: vec![field("provider", DamlType::Party)],
642 choices: vec![Choice {
643 name: "Accept".to_string(),
644 consuming: true,
645 argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
646 returns: DamlType::Unit,
647 }],
648 key: None,
649 }],
650 interfaces: Vec::new(),
651 };
652
653 let src = generate_module(&module).unwrap();
654 syn::parse_file(&src).unwrap();
656 assert!(src.contains("use canton_daml as rt;"), "{src}");
657 assert!(src.contains("#![allow(non_camel_case_types"), "{src}");
658 assert!(src.contains("pub enum Color"), "{src}");
659 assert!(src.contains("pub struct AppInstall"), "{src}");
660 assert!(
661 src.contains("impl rt::Choice<AppInstall> for AppInstall_Accept"),
662 "{src}"
663 );
664 }
665
666 #[test]
667 fn generic_record_carries_type_params() {
668 let record = Record {
669 name: "Pair".to_string(),
670 type_params: vec!["a".to_string(), "b".to_string()],
671 fields: vec![
672 field("fst", DamlType::Var("a".to_string())),
673 field("snd", DamlType::Var("b".to_string())),
674 ],
675 };
676
677 let src = generate_record(&record).unwrap();
678 syn::parse_file(&src).unwrap();
679 assert!(src.contains("pub struct Pair<A, B>"), "{src}");
681 assert!(src.contains("pub fst: A"), "{src}");
682 }
683 #[test]
692 fn every_keyword_in_the_list_is_escaped_in_a_field_name() {
693 for (daml, expected) in [
696 ("type", "r#type:"),
697 ("match", "r#match:"),
698 ("gen", "r#gen:"),
699 ("try", "r#try:"),
700 ("async", "r#async:"),
701 ("box", "r#box:"),
702 ("crate", "crate_:"),
703 ("self", "self_:"),
704 ] {
705 let record = Record {
706 name: "Keyworded".to_string(),
707 type_params: Vec::new(),
708 fields: vec![field(daml, DamlType::Text)],
709 };
710 let src = generate_record(&record).expect("generator emits valid Rust");
711 assert!(
712 src.contains(expected),
713 "a Daml field named `{daml}` should emit `{expected}`:\n{src}"
714 );
715 assert!(
718 src.contains(&format!("#[serde(rename = \"{daml}\")]")),
719 "the wire label `{daml}` must survive:\n{src}"
720 );
721 }
722 }
723 #[test]
733 fn a_phantom_type_parameter_is_not_forced_to_carry_a_codec() {
734 let mut record = Record::new("Wrapper");
735 record.type_params = vec!["a".to_string(), "b".to_string()];
736 record.fields = vec![Field::new("value", DamlType::Var("b".to_string()))];
738
739 let src = generate_data_type(&DataType::Record(record)).unwrap();
740 assert!(
741 src.contains("B: rt::ToValue"),
742 "the used parameter is bounded: {src}"
743 );
744 assert!(
745 !src.contains("A: rt::ToValue"),
746 "the phantom parameter must not be bounded: {src}"
747 );
748 assert!(src.contains("pub struct Wrapper<A, B>"), "{src}");
750 assert!(src.contains("PhantomData"), "{src}");
751 }
752
753 #[test]
756 fn a_variants_phantom_parameter_is_not_bounded_either() {
757 let mut variant = Variant::new("Choice");
758 variant.type_params = vec!["a".to_string(), "b".to_string()];
759 variant.constructors = vec![
760 VariantConstructor::with_payload("Some", DamlType::Var("b".to_string())),
761 VariantConstructor::new("None"),
762 ];
763
764 let src = generate_data_type(&DataType::Variant(variant)).unwrap();
765 assert!(src.contains("B: rt::ToValue"), "{src}");
766 assert!(!src.contains("A: rt::ToValue"), "{src}");
767 }
768}