pub mod ir;
mod generate;
pub(crate) mod emit;
pub(crate) mod lower;
pub(crate) mod map;
pub use canton_lf;
pub use generate::{GenerateError, Options, Runtime, Stats, default_crate_name, generate};
pub use lower::{ExternalPackages, SkippedType, lower_dar, lower_dar_with};
use proc_macro2::TokenStream;
#[derive(Debug)]
pub struct CodegenError(syn::Error);
impl std::fmt::Display for CodegenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "generated source is not valid Rust: {}", self.0)
}
}
impl std::error::Error for CodegenError {}
use crate::ir::{Crate, DataType, Module, Record, Template};
const MODULE_PREAMBLE: &str = concat!(
"#![allow(non_camel_case_types, non_snake_case, unused_imports, dead_code, unused_variables, clippy::all)]\n",
"//! Generated Daml bindings — do not edit by hand.\n\n",
"use canton_daml as rt;\n\n",
);
const CRATE_LINTS: &str =
"#![allow(non_camel_case_types, non_snake_case, unused_imports, clippy::all)]\n";
fn crate_docs(krate: &Crate) -> String {
use std::fmt::Write as _;
let mut docs = String::new();
docs.push_str("//! Typed Rust bindings generated from a Daml archive (DAR).\n//!\n");
docs.push_str(
"//! **Generated file — do not edit by hand.** Regenerate with\n\
//! `dpm-codegen-rust --dar <the DAR> --out <this crate>`; edits are lost.\n//!\n\
//! Each Daml package in the DAR's dependency closure is one top-level\n\
//! module, and each Daml module a submodule under it, so cross-package\n\
//! references resolve and names never collide. Templates carry typed\n\
//! choices; the runtime traits they implement (`Template`, `Choice`,\n\
//! `Contract`) and the command builders live in `canton-daml`, which the\n\
//! generated code imports as `rt`.\n//!\n\
//! # Packages in this crate\n//!\n",
);
for package in &krate.packages {
let modules = package.modules.len();
let plural = if modules == 1 { "module" } else { "modules" };
let _ = writeln!(docs, "//! - [`{}`] — {modules} Daml {plural}", package.name);
}
docs.push('\n');
docs
}
fn format_items(tokens: TokenStream) -> Result<String, CodegenError> {
let file: syn::File = syn::parse2(tokens).map_err(CodegenError)?;
Ok(prettyplease::unparse(&file))
}
pub fn generate_record(record: &Record) -> Result<String, CodegenError> {
format_items(emit::record_items(record))
}
pub fn generate_data_type(data_type: &DataType) -> Result<String, CodegenError> {
format_items(emit::data_type(data_type))
}
pub fn generate_template(template: &Template) -> Result<String, CodegenError> {
format_items(emit::template(template))
}
pub fn generate_interface(interface: &crate::ir::Interface) -> Result<String, CodegenError> {
format_items(emit::interface(interface))
}
pub fn generate_module(module: &Module) -> Result<String, CodegenError> {
let body = format_items(emit::module_items(module))?;
Ok(format!("{MODULE_PREAMBLE}{body}"))
}
pub fn generate_crate(krate: &Crate) -> Result<String, CodegenError> {
let body = format_items(emit::crate_items(krate))?;
Ok(format!("{CRATE_LINTS}{}{body}", crate_docs(krate)))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::ir::{DamlType, Field, Record, TypeRef, Variant, VariantConstructor};
fn field(label: &str, ty: DamlType) -> Field {
Field {
label: label.to_string(),
ty,
}
}
#[test]
fn generates_valid_rust_for_a_record() {
let record = Record {
name: "AppInstallRequest".to_string(),
type_params: Vec::new(),
fields: vec![
field("provider", DamlType::Party),
field("installId", DamlType::Text),
field("amount", DamlType::Numeric(10)),
field(
"cid",
DamlType::ContractId(Box::new(DamlType::Ref(TypeRef::local(
"Foo",
Vec::new(),
)))),
),
field("tags", DamlType::List(Box::new(DamlType::Text))),
field("note", DamlType::Optional(Box::new(DamlType::Text))),
field("type", DamlType::Text),
],
};
let src = generate_record(&record).expect("generator emits valid Rust");
syn::parse_file(&src).expect("output must be valid Rust");
assert!(src.contains("pub struct AppInstallRequest"), "{src}");
assert!(src.contains("pub provider: rt::Party"), "{src}");
assert!(
src.contains("pub install_id: ::std::string::String"),
"{src}"
);
assert!(src.contains("pub amount: rt::Numeric"), "{src}");
assert!(src.contains("rt::ContractId<Foo>"), "{src}");
assert!(src.contains("Vec<::std::string::String>"), "{src}");
assert!(src.contains("Option<::std::string::String>"), "{src}");
assert!(src.contains("r#type: ::std::string::String"), "{src}");
}
#[test]
fn only_a_renamed_field_carries_a_doc() {
let record = Record {
name: "Payload".to_string(),
type_params: Vec::new(),
fields: vec![
field("owner", DamlType::Party),
field("acceptArg", DamlType::Text),
field("type", DamlType::Text),
],
};
let src = generate_record(&record).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("Daml field `acceptArg`"), "{src}");
assert!(!src.contains("Daml field `owner`"), "{src}");
assert!(src.contains("r#type"), "{src}");
assert!(!src.contains("Daml field `type`"), "{src}");
}
#[test]
fn generated_decoders_attach_the_field_name_to_a_failure() {
let record = Record {
name: "Payload".to_string(),
type_params: Vec::new(),
fields: vec![
field("owner", DamlType::Party),
field("note", DamlType::Optional(Box::new(DamlType::Text))),
],
};
let src = generate_record(&record).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains(r#".map_err(|e| e.at("owner"))"#), "{src}");
assert!(src.contains(r#".map_err(|e| e.at("note"))"#), "{src}");
assert!(
src.contains(r#"rt::required_field(value, 0usize, "owner")?)"#),
"{src}"
);
}
#[test]
fn record_emits_value_codecs() {
let record = Record {
name: "Payload".to_string(),
type_params: Vec::new(),
fields: vec![
field("owner", DamlType::Party),
field("count", DamlType::Int64),
],
};
let src = generate_record(&record).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("impl rt::ToValue for Payload"), "{src}");
assert!(src.contains("impl rt::FromValue for Payload"), "{src}");
assert!(src.contains("rt::record("), "{src}");
assert!(
src.contains("rt::required_field(value, 0usize, \"owner\")"),
"{src}"
);
}
#[test]
fn optional_fields_decode_as_absent_tolerant() {
let record = Record {
name: "Payload".to_string(),
type_params: Vec::new(),
fields: vec![
field("owner", DamlType::Party),
field("note", DamlType::Optional(Box::new(DamlType::Text))),
],
};
let src = generate_record(&record).unwrap();
syn::parse_file(&src).unwrap();
assert!(
src.contains("rt::optional_field(value, 1usize, \"note\")"),
"{src}"
);
assert!(
src.contains("rt::required_field(value, 0usize, \"owner\")"),
"{src}"
);
}
#[test]
fn nested_optional_maps_to_nested_opt() {
let record = Record {
name: "R".to_string(),
type_params: Vec::new(),
fields: vec![
field("single", DamlType::Optional(Box::new(DamlType::Text))),
field(
"nested",
DamlType::Optional(Box::new(DamlType::Optional(Box::new(DamlType::Text)))),
),
],
};
let src = generate_record(&record).unwrap();
syn::parse_file(&src).unwrap();
assert!(
src.contains("pub single: ::core::option::Option<::std::string::String>"),
"{src}"
);
assert!(
src.contains(
"pub nested: ::core::option::Option<rt::NestedOpt<::std::string::String>>"
),
"{src}"
);
}
#[test]
fn record_derives_the_json_codec() {
let record = Record {
name: "Payload".to_string(),
type_params: Vec::new(),
fields: vec![field("installId", DamlType::Text)],
};
let src = generate_record(&record).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("rt::serde::Serialize"), "{src}");
assert!(src.contains("rt::serde::Deserialize"), "{src}");
assert!(src.contains("serde(crate = \"rt::serde\")"), "{src}");
assert!(src.contains("serde(rename = \"installId\")"), "{src}");
assert!(
src.contains("pub install_id: ::std::string::String"),
"{src}"
);
}
#[test]
fn variant_generates_a_rust_enum() {
use crate::ir::{Variant, VariantConstructor};
let variant = Variant {
name: "Shape".to_string(),
type_params: Vec::new(),
constructors: vec![
VariantConstructor {
name: "Circle".to_string(),
payload: Some(DamlType::Numeric(10)),
},
VariantConstructor {
name: "Point".to_string(),
payload: None,
},
],
};
let src = generate_data_type(&DataType::Variant(variant)).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("pub enum Shape"), "{src}");
assert!(src.contains("Circle(rt::Numeric)"), "{src}");
assert!(src.contains("Point(rt::Unit)"), "{src}");
}
#[test]
fn enum_generates_a_c_like_enum() {
use crate::ir::Enum;
let enumeration = Enum {
name: "DayOfWeek".to_string(),
constructors: vec!["Monday".to_string(), "Tuesday".to_string()],
};
let src = generate_data_type(&DataType::Enum(enumeration)).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("pub enum DayOfWeek"), "{src}");
assert!(src.contains("Monday,"), "{src}");
assert!(src.contains("Copy"), "{src}");
}
#[test]
fn template_generates_payload_and_typed_choices() {
use crate::ir::{Choice, Template, TypeRef};
let template = Template {
name: "AppInstall".to_string(),
module_name: "Licensing.AppInstall".to_string(),
package_id: "abc123".to_string(),
package_name: "app-install".to_string(),
fields: vec![field("provider", DamlType::Party)],
choices: vec![Choice {
name: "Accept".to_string(),
consuming: true,
argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
returns: DamlType::ContractId(Box::new(DamlType::Ref(TypeRef::local(
"AppInstalled",
Vec::new(),
)))),
}],
key: None,
};
let src = generate_template(&template).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("pub struct AppInstall"), "{src}");
assert!(src.contains("pub provider: rt::Party"), "{src}");
assert!(
src.contains("impl rt::Choice<AppInstall> for AppInstall_Accept"),
"{src}"
);
assert!(
src.contains("type Return = rt::ContractId<AppInstalled>"),
"{src}"
);
assert!(
src.contains("const NAME: &'static str = \"Accept\""),
"{src}"
);
assert!(src.contains("const CONSUMING: bool = true"), "{src}");
assert!(src.contains("impl rt::Template for AppInstall"), "{src}");
assert!(
src.contains("const MODULE_NAME: &'static str = \"Licensing.AppInstall\""),
"{src}"
);
assert!(
src.contains("const ENTITY_NAME: &'static str = \"AppInstall\""),
"{src}"
);
assert!(
src.contains("const PACKAGE_NAME: &'static str = \"app-install\""),
"{src}"
);
}
#[test]
fn a_template_documents_its_identity_and_its_choices() {
use crate::ir::{Choice, Template, TypeRef};
let template = Template {
name: "AppInstall".to_string(),
module_name: "Licensing.AppInstall".to_string(),
package_id: "abc123".to_string(),
package_name: "app-install".to_string(),
fields: vec![field("provider", DamlType::Party)],
choices: vec![
Choice {
name: "Accept".to_string(),
consuming: true,
argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
returns: DamlType::Unit,
},
Choice {
name: "Peek".to_string(),
consuming: false,
argument: DamlType::Ref(TypeRef::local("AppInstall_Peek", Vec::new())),
returns: DamlType::Unit,
},
],
key: None,
};
let src = generate_template(&template).unwrap();
syn::parse_file(&src).unwrap();
assert!(
src.contains("The Daml template `Licensing.AppInstall:AppInstall`"),
"{src}"
);
assert!(
src.contains("`#app-install:Licensing.AppInstall:AppInstall`"),
"{src}"
);
assert!(src.contains("- `Accept` — consuming"), "{src}");
assert!(src.contains("- `Peek` — non-consuming"), "{src}");
assert!(!src.contains("/**"), "{src}");
}
#[test]
fn keyed_template_emits_with_key_impl() {
use crate::ir::{Template, TypeRef};
let template = Template {
name: "Account".to_string(),
module_name: "Bank.Account".to_string(),
package_id: "abc123".to_string(),
package_name: "bank".to_string(),
fields: vec![field("owner", DamlType::Party)],
choices: Vec::new(),
key: Some(DamlType::Ref(TypeRef::local("AccountKey", Vec::new()))),
};
let src = generate_template(&template).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("impl rt::WithKey for Account"), "{src}");
assert!(src.contains("type Key = AccountKey"), "{src}");
}
#[test]
fn interface_emits_identity_view_and_choice_impls() {
use crate::ir::{Choice, Interface, TypeRef};
let interface = Interface {
name: "Holding".to_string(),
module_name: "Splice.Api.Token.HoldingV1".to_string(),
package_id: "abc123".to_string(),
package_name: "splice-api-token-holding".to_string(),
view: Some(DamlType::Ref(TypeRef::local("HoldingView", Vec::new()))),
choices: vec![Choice {
name: "Transfer".to_string(),
consuming: true,
argument: DamlType::Ref(TypeRef::local("Holding_Transfer", Vec::new())),
returns: DamlType::Unit,
}],
};
let src = generate_interface(&interface).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("impl rt::Contract for Holding"), "{src}");
assert!(src.contains("impl rt::Interface for Holding"), "{src}");
assert!(src.contains("type View = HoldingView"), "{src}");
assert!(
src.contains("impl rt::Choice<Holding> for Holding_Transfer"),
"{src}"
);
}
#[test]
fn keyless_template_emits_no_with_key_impl() {
use crate::ir::Template;
let template = Template {
name: "Note".to_string(),
module_name: "M".to_string(),
package_id: "p".to_string(),
package_name: "m".to_string(),
fields: vec![field("text", DamlType::Text)],
choices: Vec::new(),
key: None,
};
let src = generate_template(&template).unwrap();
syn::parse_file(&src).unwrap();
assert!(
!src.contains("WithKey"),
"keyless template has no key impl: {src}"
);
}
#[test]
fn module_wraps_items_with_the_runtime_preamble() {
use crate::ir::{Choice, Enum, Module, Template};
let module = Module {
data_types: vec![DataType::Enum(Enum {
name: "Color".to_string(),
constructors: vec!["Red".to_string(), "Green".to_string()],
})],
templates: vec![Template {
name: "AppInstall".to_string(),
module_name: "Licensing.AppInstall".to_string(),
package_id: "abc123".to_string(),
package_name: "app-install".to_string(),
fields: vec![field("provider", DamlType::Party)],
choices: vec![Choice {
name: "Accept".to_string(),
consuming: true,
argument: DamlType::Ref(TypeRef::local("AppInstall_Accept", Vec::new())),
returns: DamlType::Unit,
}],
key: None,
}],
interfaces: Vec::new(),
};
let src = generate_module(&module).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("use canton_daml as rt;"), "{src}");
assert!(src.contains("#![allow(non_camel_case_types"), "{src}");
assert!(src.contains("pub enum Color"), "{src}");
assert!(src.contains("pub struct AppInstall"), "{src}");
assert!(
src.contains("impl rt::Choice<AppInstall> for AppInstall_Accept"),
"{src}"
);
}
#[test]
fn generic_record_carries_type_params() {
let record = Record {
name: "Pair".to_string(),
type_params: vec!["a".to_string(), "b".to_string()],
fields: vec![
field("fst", DamlType::Var("a".to_string())),
field("snd", DamlType::Var("b".to_string())),
],
};
let src = generate_record(&record).unwrap();
syn::parse_file(&src).unwrap();
assert!(src.contains("pub struct Pair<A, B>"), "{src}");
assert!(src.contains("pub fst: A"), "{src}");
}
#[test]
fn every_keyword_in_the_list_is_escaped_in_a_field_name() {
for (daml, expected) in [
("type", "r#type:"),
("match", "r#match:"),
("gen", "r#gen:"),
("try", "r#try:"),
("async", "r#async:"),
("box", "r#box:"),
("crate", "crate_:"),
("self", "self_:"),
] {
let record = Record {
name: "Keyworded".to_string(),
type_params: Vec::new(),
fields: vec![field(daml, DamlType::Text)],
};
let src = generate_record(&record).expect("generator emits valid Rust");
assert!(
src.contains(expected),
"a Daml field named `{daml}` should emit `{expected}`:\n{src}"
);
assert!(
src.contains(&format!("#[serde(rename = \"{daml}\")]")),
"the wire label `{daml}` must survive:\n{src}"
);
}
}
#[test]
fn a_phantom_type_parameter_is_not_forced_to_carry_a_codec() {
let mut record = Record::new("Wrapper");
record.type_params = vec!["a".to_string(), "b".to_string()];
record.fields = vec![Field::new("value", DamlType::Var("b".to_string()))];
let src = generate_data_type(&DataType::Record(record)).unwrap();
assert!(
src.contains("B: rt::ToValue"),
"the used parameter is bounded: {src}"
);
assert!(
!src.contains("A: rt::ToValue"),
"the phantom parameter must not be bounded: {src}"
);
assert!(src.contains("pub struct Wrapper<A, B>"), "{src}");
assert!(src.contains("PhantomData"), "{src}");
}
#[test]
fn a_variants_phantom_parameter_is_not_bounded_either() {
let mut variant = Variant::new("Choice");
variant.type_params = vec!["a".to_string(), "b".to_string()];
variant.constructors = vec![
VariantConstructor::with_payload("Some", DamlType::Var("b".to_string())),
VariantConstructor::new("None"),
];
let src = generate_data_type(&DataType::Variant(variant)).unwrap();
assert!(src.contains("B: rt::ToValue"), "{src}");
assert!(!src.contains("A: rt::ToValue"), "{src}");
}
}