use std::collections::{HashMap, HashSet};
use heck::ToUpperCamelCase;
use canton_lf::pb::daml_lf_2 as lf;
use canton_lf::{
Dar, DecodeError, decode_all, imported_package_id, interned_dotted_name, interned_str,
interned_type, package_name, package_version,
};
use crate::ir::{
Choice, Crate, DamlType, DataType, Enum, Field, Interface, Module, NamedModule, PackageModule,
Record, Template, TypeRef, Variant, VariantConstructor,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedType {
module: String,
reason: String,
}
impl SkippedType {
fn new(reason: impl Into<String>) -> Self {
Self {
module: String::new(),
reason: reason.into(),
}
}
fn in_module(mut self, module: &str) -> Self {
self.module = module.to_string();
self
}
#[must_use]
pub fn module(&self) -> &str {
&self.module
}
#[must_use]
pub fn reason(&self) -> &str {
&self.reason
}
}
impl std::fmt::Display for SkippedType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.module.is_empty() {
f.write_str(&self.reason)
} else {
write!(f, "{}: {}", self.module, self.reason)
}
}
}
impl std::error::Error for SkippedType {}
pub fn lower_dar(dar: &Dar) -> Result<(Crate, Vec<SkippedType>), DecodeError> {
lower_dar_with(dar, &ExternalPackages::default())
}
pub fn lower_dar_with(
dar: &Dar,
external: &ExternalPackages,
) -> Result<(Crate, Vec<SkippedType>), DecodeError> {
lower_dar_selecting(dar, &Selection::everything(), external)
}
pub fn lower_dar_selecting(
dar: &Dar,
selection: &Selection,
external: &ExternalPackages,
) -> Result<(Crate, Vec<SkippedType>), DecodeError> {
Ok(lower_crate_selecting(
&decode_all(dar)?,
selection,
external,
))
}
#[must_use]
pub fn lower_packages_selecting(
packages: &[(String, lf::Package)],
selection: &Selection,
external: &ExternalPackages,
) -> (Crate, Vec<SkippedType>) {
lower_crate_selecting(packages, selection, external)
}
#[derive(Clone, Debug, Default)]
pub struct Selection {
keys: Option<Vec<String>>,
prefixes: Vec<String>,
}
impl Selection {
#[must_use]
pub fn everything() -> Self {
Self {
keys: None,
prefixes: Vec::new(),
}
}
#[must_use]
pub fn only<K: Into<String>>(keys: impl IntoIterator<Item = K>) -> Self {
Self {
keys: Some(keys.into_iter().map(Into::into).collect()),
prefixes: Vec::new(),
}
}
#[must_use]
pub fn and_prefixed<K: Into<String>>(mut self, prefixes: impl IntoIterator<Item = K>) -> Self {
self.keys.get_or_insert_with(Vec::new);
self.prefixes.extend(prefixes.into_iter().map(Into::into));
self
}
fn resolve(&self, packages: &[(String, lf::Package)]) -> Option<HashSet<String>> {
let keys = self.keys.as_ref()?;
let mut ids = HashSet::new();
for (id, package) in packages {
let name = canton_lf::package_name(package);
let named = keys
.iter()
.any(|k| k == id || name.is_some_and(|n| n == k.as_str()))
|| self.prefixes.iter().any(|p| {
name.is_some_and(|n| n == p.as_str() || n.starts_with(&format!("{p}-")))
});
if named {
ids.insert(id.clone());
}
}
Some(ids)
}
}
#[derive(Clone, Debug, Default)]
pub struct ExternalPackages {
by_key: HashMap<String, String>,
by_prefix: Vec<(String, String)>,
}
impl ExternalPackages {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, key: impl Into<String>, crate_name: impl Into<String>) -> Self {
self.by_key.insert(key.into(), crate_name.into());
self
}
#[must_use]
pub fn with_prefixed(
mut self,
prefix: impl Into<String>,
crate_name: impl Into<String>,
) -> Self {
self.by_prefix.push((prefix.into(), crate_name.into()));
self
}
#[must_use]
pub fn crate_names(&self) -> Vec<String> {
let mut names: Vec<String> = self
.by_key
.values()
.chain(self.by_prefix.iter().map(|(_, c)| c))
.cloned()
.collect();
names.sort_unstable();
names.dedup();
names
}
fn resolve(&self, packages: &[(String, lf::Package)]) -> HashMap<String, String> {
let mut resolved = HashMap::new();
for (id, package) in packages {
let name = canton_lf::package_name(package);
let by_id = self.by_key.get(id.as_str());
let by_name = name.and_then(|n| self.by_key.get(n));
let by_prefix = self.by_prefix.iter().find_map(|(prefix, krate)| {
name.filter(|n| *n == prefix || n.starts_with(&format!("{prefix}-")))
.map(|_| krate)
});
if let Some(crate_name) = by_id.or(by_name).or(by_prefix) {
resolved.insert(id.clone(), crate_name.clone());
}
}
resolved
}
}
#[must_use]
#[cfg(test)]
pub(crate) fn lower_crate(packages: &[(String, lf::Package)]) -> (Crate, Vec<SkippedType>) {
lower_crate_selecting(
packages,
&Selection::everything(),
&ExternalPackages::default(),
)
}
pub(crate) fn lower_crate_selecting(
packages: &[(String, lf::Package)],
selection: &Selection,
external: &ExternalPackages,
) -> (Crate, Vec<SkippedType>) {
let external = external.resolve(packages);
let selected = selection.resolve(packages);
let module_names = package_module_names(packages);
let mut krate = Crate::default();
let mut errors = Vec::new();
let mut lowered: std::collections::HashSet<&str> = std::collections::HashSet::new();
for (id, package) in packages {
if !lowered.insert(id.as_str()) {
continue;
}
if external.contains_key(id.as_str()) {
continue;
}
if selected.as_ref().is_some_and(|ids| !ids.contains(id)) {
continue;
}
let lowering = Lowering {
package,
qualify: Some(Qualify {
module_names: &module_names,
current_id: id,
external: &external,
selected: selected.as_ref(),
}),
depth: std::cell::Cell::new(0),
};
let mut package_module = PackageModule {
name: module_names[id.as_str()].clone(),
modules: Vec::new(),
};
let colliding_modules = colliding_module_names(package);
let mut seen_modules: std::collections::HashSet<String> = std::collections::HashSet::new();
for lf_module in &package.modules {
let Some(dotted) = interned_dotted_name(package, lf_module.name_interned_dname) else {
errors.push(SkippedType::new("unresolved module name"));
continue;
};
if !seen_modules.insert(dotted.clone()) {
errors.push(SkippedType::new(format!(
"module `{dotted}` is declared more than once in this package"
)));
continue;
}
let module_ident = dotted.replace('.', "_");
if !is_rust_ident(&module_ident) {
errors.push(SkippedType::new(format!(
"module `{dotted}` is not representable as a Rust module name"
)));
continue;
}
if let Some(sources) = colliding_modules.get(&module_ident) {
errors.push(SkippedType::new(format!(
"modules `{}` both map to the Rust module `{}`; \
rename one in Daml to generate either",
sources.join("` and `"),
dotted.replace('.', "_"),
)));
continue;
}
let ir_module = lowering.module(lf_module, &dotted, &mut errors);
if !ir_module.data_types.is_empty()
|| !ir_module.templates.is_empty()
|| !ir_module.interfaces.is_empty()
{
package_module.modules.push(NamedModule {
name: dotted.replace('.', "_"),
module: ir_module,
});
}
}
if !package_module.modules.is_empty() {
krate.packages.push(package_module);
}
}
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
drop_dangling_references(&mut krate, &mut errors);
box_recursion(&mut krate);
(krate, errors)
}
fn parameters_used_under_optional(krate: &Crate) -> HashMap<String, Vec<bool>> {
struct Generic<'a> {
params: &'a [String],
direct: std::collections::BTreeSet<&'a str>,
forwards: Vec<(String, usize, &'a str)>,
}
let mut generics: HashMap<String, Generic<'_>> = HashMap::new();
for package in &krate.packages {
for module in &package.modules {
for data_type in &module.module.data_types {
let (name, params, types) = match data_type {
DataType::Record(record) => (
&record.name,
&record.type_params,
record
.fields
.iter()
.map(|field| &field.ty)
.collect::<Vec<_>>(),
),
DataType::Variant(variant) => (
&variant.name,
&variant.type_params,
variant
.constructors
.iter()
.filter_map(|constructor| constructor.payload.as_ref())
.collect(),
),
DataType::Enum(_) | DataType::InterfaceMarker(_) => continue,
};
if params.is_empty() {
continue;
}
let mut generic = Generic {
params,
direct: std::collections::BTreeSet::new(),
forwards: Vec::new(),
};
for ty in types {
collect_nesting_positions(ty, &mut generic.direct, &mut generic.forwards);
}
generics.insert(path_key_for(&package.name, &module.name, name), generic);
}
}
}
let mut nests_parameter: HashMap<String, Vec<bool>> = generics
.iter()
.map(|(key, generic)| {
let flags = generic
.params
.iter()
.map(|param| generic.direct.contains(param.as_str()))
.collect();
(key.clone(), flags)
})
.collect();
loop {
let mut changed = false;
for (key, generic) in &generics {
for (target, index, param) in &generic.forwards {
let target_nests = nests_parameter
.get(target)
.and_then(|flags| flags.get(*index))
.copied()
.unwrap_or(false);
if !target_nests {
continue;
}
let Some(position) = generic.params.iter().position(|p| p == param) else {
continue;
};
let Some(flag) = nests_parameter
.get_mut(key)
.and_then(|flags| flags.get_mut(position))
else {
continue;
};
if !*flag {
*flag = true;
changed = true;
}
}
}
if !changed {
break;
}
}
nests_parameter
}
fn collect_nesting_positions<'a>(
ty: &'a DamlType,
direct: &mut std::collections::BTreeSet<&'a str>,
forwards: &mut Vec<(String, usize, &'a str)>,
) {
match ty {
DamlType::Optional(inner) => {
if let Some(param) = bare_parameter(inner) {
direct.insert(param);
}
collect_nesting_positions(inner, direct, forwards);
}
DamlType::Ref(reference) => {
let target = path_key_of(reference);
for (index, arg) in reference.args.iter().enumerate() {
if let Some(param) = bare_parameter(arg) {
forwards.push((target.clone(), index, param));
}
collect_nesting_positions(arg, direct, forwards);
}
}
DamlType::List(inner) | DamlType::TextMap(inner) | DamlType::Boxed(inner) => {
collect_nesting_positions(inner, direct, forwards);
}
DamlType::GenMap(key, value) => {
collect_nesting_positions(key, direct, forwards);
collect_nesting_positions(value, direct, forwards);
}
_ => {}
}
}
fn bare_parameter(ty: &DamlType) -> Option<&str> {
match ty {
DamlType::Var(name) => Some(name),
DamlType::Boxed(inner) => bare_parameter(inner),
_ => None,
}
}
fn drop_ambiguous_nested_optionals(krate: &mut Crate, errors: &mut Vec<SkippedType>) {
let nests_parameter = parameters_used_under_optional(krate);
let mut ambiguous: Vec<(String, String, String, String)> = Vec::new();
for package in &krate.packages {
for module in &package.modules {
let mut check = |name: &str, types: Vec<&TypeRef>| {
if let Some(target) = types.iter().find_map(|reference| {
let nests = nests_parameter.get(&path_key_of(reference))?;
reference
.args
.iter()
.zip(nests)
.any(|(arg, &nested)| nested && matches!(arg, DamlType::Optional(_)))
.then(|| path_key_of(reference))
}) {
ambiguous.push((
package.name.clone(),
module.name.clone(),
name.to_string(),
target,
));
}
};
for data_type in &module.module.data_types {
let mut types = Vec::new();
collect_type_refs_of_data_type(data_type, &mut types);
check(data_type_name(data_type), types);
}
for template in &module.module.templates {
let mut types = Vec::new();
each_type_of_template(template, &mut |ty| {
collect_type_refs(ty, &mut types);
});
check(&template.name, types);
}
for interface in &module.module.interfaces {
let mut types = Vec::new();
each_type_of_interface(interface, &mut |ty| {
collect_type_refs(ty, &mut types);
});
check(&interface.name, types);
}
}
}
for (package_name, module_name, name, target) in ambiguous {
let key = path_key_for(&package_name, &module_name, &name);
for package in &mut krate.packages {
for module in &mut package.modules {
let at = |name: &str| path_key_for(&package.name, &module.name, name);
module
.module
.data_types
.retain(|data_type| at(data_type_name(data_type)) != key);
module
.module
.templates
.retain(|template| at(&template.name) != key);
module
.module
.interfaces
.retain(|interface| at(&interface.name) != key);
}
}
errors.push(
SkippedType::new(format!(
"`{name}` instantiates `{}` with an Optional that the target nests \
under its own Optional; the LF-JSON form of that is not yet representable",
target.rsplit("::").next().unwrap_or(&target),
))
.in_module(&module_name),
);
}
}
fn collect_type_refs_of_data_type<'a>(data_type: &'a DataType, out: &mut Vec<&'a TypeRef>) {
let mut walk = |ty: &'a DamlType| collect_type_refs(ty, out);
match data_type {
DataType::Record(record) => record.fields.iter().for_each(|field| walk(&field.ty)),
DataType::Variant(variant) => variant
.constructors
.iter()
.filter_map(|constructor| constructor.payload.as_ref())
.for_each(walk),
DataType::Enum(_) | DataType::InterfaceMarker(_) => {}
}
}
fn collect_type_refs<'a>(ty: &'a DamlType, out: &mut Vec<&'a TypeRef>) {
match ty {
DamlType::Ref(reference) => {
out.push(reference);
reference
.args
.iter()
.for_each(|arg| collect_type_refs(arg, out));
}
DamlType::List(inner)
| DamlType::Optional(inner)
| DamlType::TextMap(inner)
| DamlType::Boxed(inner) => collect_type_refs(inner, out),
DamlType::GenMap(key, value) => {
collect_type_refs(key, out);
collect_type_refs(value, out);
}
_ => {}
}
}
fn declared_keys(krate: &Crate) -> std::collections::HashSet<String> {
krate
.packages
.iter()
.flat_map(|package| {
package.modules.iter().flat_map(move |module| {
let at = |name: &str| path_key_for(&package.name, &module.name, name);
module
.module
.data_types
.iter()
.map(move |data_type| at(data_type_name(data_type)))
.chain(
module
.module
.templates
.iter()
.map(move |template| at(&template.name)),
)
})
})
.collect()
}
fn still_declares(module: &Module, key: &str, at: &impl Fn(&str) -> String) -> bool {
module
.data_types
.iter()
.any(|data_type| at(data_type_name(data_type)) == key)
|| module.templates.iter().any(|t| at(&t.name) == key)
}
fn drop_dangling_references(krate: &mut Crate, errors: &mut Vec<SkippedType>) {
let mut declared = declared_keys(krate);
loop {
let mut dropped = false;
for package in &mut krate.packages {
let package_name = package.name.clone();
for module in &mut package.modules {
let module_name = module.name.clone();
let at = |name: &str| path_key_for(&package_name, &module_name, name);
let missing = |referenced: Vec<String>| -> Option<String> {
referenced
.into_iter()
.find(|key| key.starts_with("crate::") && !declared.contains(key))
};
let mut report = Vec::new();
module.module.data_types.retain(|data_type| {
let mut referenced = Vec::new();
collect_all_refs_of_data_type(data_type, &mut referenced);
match missing(referenced) {
Some(gone) => {
report.push((
at(data_type_name(data_type)),
data_type_name(data_type).to_string(),
gone,
));
false
}
None => true,
}
});
module.module.templates.retain(|template| {
let mut referenced = Vec::new();
collect_all_refs_of_template(template, &mut referenced);
match missing(referenced) {
Some(gone) => {
report.push((at(&template.name), template.name.clone(), gone));
false
}
None => true,
}
});
module.module.interfaces.retain(|interface| {
if !declared.contains(&at(&interface.name)) {
return false;
}
let mut referenced = Vec::new();
collect_all_refs_of_interface(interface, &mut referenced);
match missing(referenced) {
Some(gone) => {
report.push((at(&interface.name), interface.name.clone(), gone));
false
}
None => true,
}
});
for (key, name, gone) in report {
if !still_declares(&module.module, &key, &at) {
declared.remove(&key);
}
errors.push(
SkippedType::new(format!(
"`{name}` references `{}`, which was skipped",
gone.rsplit("::").next().unwrap_or(&gone),
))
.in_module(&module_name),
);
dropped = true;
}
}
}
if !dropped {
break;
}
}
krate.packages.iter_mut().for_each(|package| {
package.modules.retain(|module| {
!module.module.data_types.is_empty()
|| !module.module.templates.is_empty()
|| !module.module.interfaces.is_empty()
});
});
krate.packages.retain(|package| !package.modules.is_empty());
}
fn collect_all_refs_of_data_type(data_type: &DataType, out: &mut Vec<String>) {
match data_type {
DataType::Record(record) => record
.fields
.iter()
.for_each(|field| collect_all_refs(&field.ty, out)),
DataType::Variant(variant) => variant
.constructors
.iter()
.filter_map(|constructor| constructor.payload.as_ref())
.for_each(|payload| collect_all_refs(payload, out)),
DataType::Enum(_) | DataType::InterfaceMarker(_) => {}
}
}
fn collect_all_refs_of_template(template: &Template, out: &mut Vec<String>) {
each_type_of_template(template, &mut |ty| collect_all_refs(ty, out));
}
fn collect_all_refs_of_interface(interface: &Interface, out: &mut Vec<String>) {
each_type_of_interface(interface, &mut |ty| collect_all_refs(ty, out));
}
fn each_type_of_template<'a>(template: &'a Template, visit: &mut impl FnMut(&'a DamlType)) {
for field in &template.fields {
visit(&field.ty);
}
for choice in &template.choices {
visit(&choice.argument);
visit(&choice.returns);
}
if let Some(key) = &template.key {
visit(key);
}
}
fn each_type_of_interface<'a>(interface: &'a Interface, visit: &mut impl FnMut(&'a DamlType)) {
if let Some(view) = &interface.view {
visit(view);
}
for choice in &interface.choices {
visit(&choice.argument);
visit(&choice.returns);
}
}
fn collect_all_refs(ty: &DamlType, out: &mut Vec<String>) {
match ty {
DamlType::Ref(reference) => {
out.push(path_key_of(reference));
reference
.args
.iter()
.for_each(|arg| collect_all_refs(arg, out));
}
DamlType::ContractId(inner)
| DamlType::List(inner)
| DamlType::Optional(inner)
| DamlType::TextMap(inner)
| DamlType::Boxed(inner) => collect_all_refs(inner, out),
DamlType::GenMap(key, value) => {
collect_all_refs(key, out);
collect_all_refs(value, out);
}
_ => {}
}
}
fn package_module_names(packages: &[(String, lf::Package)]) -> HashMap<&str, String> {
let mut names: HashMap<&str, String> = packages
.iter()
.map(|(id, package)| {
let base =
package_name(package).map_or_else(|| format!("p_{}", ident_prefix(id)), sanitize);
(id.as_str(), base)
})
.collect();
let versioned = |id: &str| {
packages
.iter()
.find(|(candidate, _)| candidate == id)
.and_then(|(_, package)| package_version(package))
.map(sanitize)
.filter(|version| !version.is_empty())
};
for step in 0..3 {
let mut counts: HashMap<String, usize> = HashMap::new();
for name in names.values() {
*counts.entry(name.clone()).or_default() += 1;
}
let ambiguous: std::collections::HashSet<String> = counts
.into_iter()
.filter(|(_, count)| *count > 1)
.map(|(name, _)| name)
.collect();
if ambiguous.is_empty() {
break;
}
for (id, name) in &mut names {
if !ambiguous.contains(name.as_str()) {
continue;
}
let suffix = match step {
0 => versioned(id).unwrap_or_else(|| ident_prefix(id)),
1 => ident_prefix(id),
_ => sanitize(id),
};
*name = format!("{name}_{suffix}");
}
}
names
}
fn ident_prefix(package_id: &str) -> String {
let prefix: String = package_id
.chars()
.filter(char::is_ascii_alphanumeric)
.take(8)
.collect();
if prefix.is_empty() {
use std::hash::{Hash as _, Hasher as _};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
package_id.hash(&mut hasher);
format!("h{:x}", hasher.finish())
} else {
prefix
}
}
fn colliding_module_names(
package: &lf::Package,
) -> std::collections::BTreeMap<String, Vec<String>> {
let mut claims: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for lf_module in package.modules.iter().filter(|m| emits_anything(m)) {
let Some(dotted) = interned_dotted_name(package, lf_module.name_interned_dname) else {
continue;
};
let sources = claims.entry(dotted.replace('.', "_")).or_default();
if !sources.contains(&dotted) {
sources.push(dotted);
}
}
claims.retain(|_, sources| sources.len() > 1);
claims
}
fn emits_a_type(def: &lf::DefDataType) -> bool {
matches!(
def.data_cons,
Some(lf::def_data_type::DataCons::Interface(_))
) || def.serializable
}
fn emits_anything(lf_module: &lf::Module) -> bool {
!lf_module.templates.is_empty()
|| !lf_module.interfaces.is_empty()
|| lf_module.data_types.iter().any(emits_a_type)
}
fn sanitize(input: &str) -> String {
input
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
struct Lowering<'a> {
package: &'a lf::Package,
qualify: Option<Qualify<'a>>,
depth: std::cell::Cell<u32>,
}
const MAX_TYPE_DEPTH: u32 = 256;
struct DepthGuard<'a>(&'a std::cell::Cell<u32>);
impl Drop for DepthGuard<'_> {
fn drop(&mut self) {
self.0.set(self.0.get() - 1);
}
}
pub(crate) const EXTERNAL_CRATE_ROOT: &str = "::";
struct Qualify<'a> {
module_names: &'a HashMap<&'a str, String>,
current_id: &'a str,
external: &'a HashMap<String, String>,
selected: Option<&'a HashSet<String>>,
}
impl Lowering<'_> {
fn module(
&self,
lf_module: &lf::Module,
module_dotted: &str,
errors: &mut Vec<SkippedType>,
) -> Module {
let colliding = self.colliding_flattened_names(lf_module);
let template_names: std::collections::HashSet<String> = lf_module
.templates
.iter()
.filter_map(|def| rust_name(self.package, def.tycon_interned_dname).ok())
.filter(|name| !colliding.contains_key(name))
.collect();
for (flattened, sources) in &colliding {
errors.push(
SkippedType::new(format!(
"`{}` both map to the Rust name `{flattened}`; \
rename one in Daml to generate either",
sources.join("` and `"),
))
.in_module(module_dotted),
);
}
let skip = |name: &str| colliding.contains_key(name);
let mut module = Module::default();
let mut payloads: HashMap<String, Vec<Field>> = HashMap::new();
let mut seen_types: std::collections::HashSet<String> = std::collections::HashSet::new();
for data_type in &lf_module.data_types {
match self.data_type(data_type) {
Ok(Some(lowered)) if skip(data_type_name(&lowered)) => {}
Ok(Some(lowered)) if !seen_types.insert(data_type_name(&lowered).to_string()) => {
errors.push(
SkippedType::new(format!(
"`{}` is declared more than once in this module",
data_type_name(&lowered)
))
.in_module(module_dotted),
);
}
Ok(Some(DataType::Record(record))) if template_names.contains(&record.name) => {
payloads.insert(record.name, record.fields);
}
Ok(Some(lowered)) => module.data_types.push(lowered),
Ok(None) => {}
Err(error) => errors.push(error.in_module(module_dotted)),
}
}
for def in &lf_module.templates {
match self.template(def, module_dotted, &mut payloads) {
Ok(template) if skip(&template.name) => {}
Ok(template) => module.templates.push(template),
Err(error) => errors.push(error.in_module(module_dotted)),
}
}
for def in &lf_module.interfaces {
match self.interface(def, module_dotted) {
Ok(interface) if skip(&interface.name) => {}
Ok(interface) => module.interfaces.push(interface),
Err(error) => errors.push(error.in_module(module_dotted)),
}
}
module
}
fn colliding_flattened_names(
&self,
lf_module: &lf::Module,
) -> std::collections::BTreeMap<String, Vec<String>> {
let declared = lf_module
.data_types
.iter()
.filter(|def| emits_a_type(def))
.map(|def| def.name_interned_dname)
.chain(
lf_module
.templates
.iter()
.map(|def| def.tycon_interned_dname),
)
.chain(
lf_module
.interfaces
.iter()
.map(|def| def.tycon_interned_dname),
);
let mut claims: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for index in declared {
let Some(dotted) = interned_dotted_name(self.package, index) else {
continue;
};
let flattened = dotted.replace('.', "_");
let sources = claims.entry(flattened).or_default();
if !sources.contains(&dotted) {
sources.push(dotted);
}
}
claims.retain(|_, sources| sources.len() > 1);
claims
}
fn interface(
&self,
def: &lf::DefInterface,
module_dotted: &str,
) -> Result<Interface, SkippedType> {
let name = rust_name(self.package, def.tycon_interned_dname)?;
let choices = def
.choices
.iter()
.map(|choice| self.choice(choice))
.collect::<Result<Vec<_>, _>>()?;
let view = def
.view
.as_ref()
.map(|ty| self.type_(ty))
.transpose()?
.filter(|view| !matches!(view, DamlType::Unit));
Ok(Interface {
name,
module_name: module_dotted.to_string(),
package_id: self
.qualify
.as_ref()
.map_or_else(String::new, |q| q.current_id.to_string()),
package_name: package_name(self.package).unwrap_or_default().to_string(),
view,
choices,
})
}
fn template(
&self,
def: &lf::DefTemplate,
module_dotted: &str,
payloads: &mut HashMap<String, Vec<Field>>,
) -> Result<Template, SkippedType> {
let name = rust_name(self.package, def.tycon_interned_dname)?;
let fields = payloads.remove(&name).ok_or_else(|| {
SkippedType::new(format!("template {name}: payload record not found"))
})?;
let choices = def
.choices
.iter()
.map(|choice| self.choice(choice))
.collect::<Result<Vec<_>, _>>()?;
let key = match def.key.as_ref() {
Some(key) => Some(self.type_(key.r#type.as_ref().ok_or_else(|| {
SkippedType::new(format!("template {name}: contract key has no type"))
})?)?),
None => None,
};
Ok(Template {
name,
module_name: module_dotted.to_string(),
package_id: self
.qualify
.as_ref()
.map_or_else(String::new, |q| q.current_id.to_string()),
package_name: self.source_safe_package_name()?,
fields,
choices,
key,
})
}
fn source_safe_package_name(&self) -> Result<String, SkippedType> {
let name = package_name(self.package).unwrap_or_default();
if is_source_safe(name) {
Ok(name.to_string())
} else {
Err(SkippedType::new(format!(
"package name `{}` contains characters that cannot appear in generated source",
name.escape_debug()
)))
}
}
fn choice(&self, choice: &lf::TemplateChoice) -> Result<Choice, SkippedType> {
let name = interned_str(self.package, choice.name_interned_str)
.ok_or_else(|| SkippedType::new("unresolved choice name"))?
.to_string();
if !is_source_safe(&name) {
return Err(SkippedType::new(format!(
"choice `{}` contains characters that cannot appear in generated source",
name.escape_debug()
)));
}
let argument = choice
.arg_binder
.as_ref()
.and_then(|binder| binder.r#type.as_ref())
.ok_or_else(|| SkippedType::new(format!("choice {name}: no argument type")))?;
let returns = choice
.ret_type
.as_ref()
.ok_or_else(|| SkippedType::new(format!("choice {name}: no return type")))?;
Ok(Choice {
name,
consuming: choice.consuming,
argument: self.type_(argument)?,
returns: self.type_(returns)?,
})
}
fn data_type(&self, data_type: &lf::DefDataType) -> Result<Option<DataType>, SkippedType> {
let name = rust_name(self.package, data_type.name_interned_dname)?;
if let Some(lf::def_data_type::DataCons::Interface(_)) = &data_type.data_cons {
return Ok(Some(DataType::InterfaceMarker(name)));
}
if !data_type.serializable {
return Ok(None);
}
let type_params = data_type
.params
.iter()
.map(|param| {
interned_str(self.package, param.var_interned_str)
.map(str::to_string)
.ok_or_else(|| SkippedType::new("unresolved type parameter"))
})
.collect::<Result<Vec<_>, _>>()?;
check_type_params(&name, &type_params)?;
let Some(data_cons) = &data_type.data_cons else {
return Err(SkippedType::new(format!("{name}: no data constructor")));
};
match data_cons {
lf::def_data_type::DataCons::Record(fields) => {
let fields = self.fields(&fields.fields)?;
Ok(Some(DataType::Record(Record {
name,
type_params,
fields,
})))
}
lf::def_data_type::DataCons::Variant(fields) => {
let constructors = fields
.fields
.iter()
.map(|field| self.variant_constructor(field))
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(DataType::Variant(Variant {
name,
type_params,
constructors,
})))
}
lf::def_data_type::DataCons::Enum(constructors) => {
let constructors = constructors
.constructors_interned_str
.iter()
.map(|&index| {
let name = interned_str(self.package, index)
.ok_or_else(|| SkippedType::new("unresolved enum constructor"))?;
if is_rust_ident(name) {
Ok(name.to_string())
} else {
Err(SkippedType::new(format!(
"enum constructor `{name}` is not representable as a Rust identifier"
)))
}
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(DataType::Enum(Enum { name, constructors })))
}
lf::def_data_type::DataCons::Interface(_) => Ok(None),
}
}
fn fields(&self, fields: &[lf::FieldWithType]) -> Result<Vec<Field>, SkippedType> {
let lowered = fields
.iter()
.map(|field| {
let label = interned_str(self.package, field.field_interned_str)
.ok_or_else(|| SkippedType::new("unresolved field label"))?
.to_string();
if !is_source_safe(&label) {
return Err(SkippedType::new(format!(
"field `{}` contains characters that cannot appear in generated source",
label.escape_debug()
)));
}
let ty = self.type_(field_type(field)?)?;
Ok(Field { label, ty })
})
.collect::<Result<Vec<Field>, SkippedType>>()?;
let mut by_rust_name: HashMap<String, &str> = HashMap::new();
for field in &lowered {
let rust = heck::ToSnakeCase::to_snake_case(field.label.as_str());
if let Some(previous) = by_rust_name.insert(rust.clone(), &field.label) {
return Err(SkippedType::new(format!(
"fields `{previous}` and `{}` both map to the Rust field `{rust}`",
field.label
)));
}
}
Ok(lowered)
}
fn variant_constructor(
&self,
field: &lf::FieldWithType,
) -> Result<VariantConstructor, SkippedType> {
let name = interned_str(self.package, field.field_interned_str)
.ok_or_else(|| SkippedType::new("unresolved variant constructor"))?
.to_string();
if !is_rust_ident(&name) {
return Err(SkippedType::new(format!(
"variant constructor `{name}` is not representable as a Rust identifier"
)));
}
let payload = match self.type_(field_type(field)?)? {
DamlType::Unit => None,
other => Some(other),
};
Ok(VariantConstructor { name, payload })
}
fn type_(&self, ty: &lf::Type) -> Result<DamlType, SkippedType> {
self.apply(ty, &[])
}
fn enter(&self) -> Result<DepthGuard<'_>, SkippedType> {
let depth = self.depth.get();
if depth >= MAX_TYPE_DEPTH {
return Err(SkippedType::new(format!(
"type nests deeper than {MAX_TYPE_DEPTH} levels, or its interned \
types refer to each other in a cycle"
)));
}
self.depth.set(depth + 1);
Ok(DepthGuard(&self.depth))
}
fn apply(&self, ty: &lf::Type, extra: &[&lf::Type]) -> Result<DamlType, SkippedType> {
let _guard = self.enter()?;
let Some(sum) = &ty.sum else {
return Err(SkippedType::new("empty type"));
};
match sum {
lf::r#type::Sum::Tapp(app) => {
let lhs = app
.lhs
.as_deref()
.ok_or_else(|| SkippedType::new("type application without a function"))?;
let rhs = app
.rhs
.as_deref()
.ok_or_else(|| SkippedType::new("type application without an argument"))?;
let mut args = Vec::with_capacity(extra.len() + 1);
args.push(rhs);
args.extend_from_slice(extra);
self.apply(lhs, &args)
}
lf::r#type::Sum::Var(var) => {
if !extra.is_empty() || !var.args.is_empty() {
return Err(SkippedType::new("type-variable application is unsupported"));
}
let name = interned_str(self.package, var.var_interned_str)
.ok_or_else(|| SkippedType::new("unresolved type variable"))?
.to_string();
Ok(DamlType::Var(name))
}
lf::r#type::Sum::Con(con) => {
let tycon = con
.tycon
.as_ref()
.ok_or_else(|| SkippedType::new("type constructor without a name"))?;
let path = self.con_path(tycon)?;
let args = con
.args
.iter()
.chain(extra.iter().copied())
.map(|arg| self.type_(arg))
.collect::<Result<Vec<_>, _>>()?;
Ok(DamlType::Ref(TypeRef { path, args }))
}
lf::r#type::Sum::Builtin(builtin) => {
let args: Vec<&lf::Type> =
builtin.args.iter().chain(extra.iter().copied()).collect();
self.builtin(builtin.builtin, &args)
}
lf::r#type::Sum::InternedType(index) => {
let resolved = interned_type(self.package, *index)
.ok_or_else(|| SkippedType::new("unresolved interned type"))?;
self.apply(resolved, extra)
}
lf::r#type::Sum::Nat(_) => Err(SkippedType::new("unexpected bare type-level Nat")),
lf::r#type::Sum::Forall(_) | lf::r#type::Sum::Struct(_) | lf::r#type::Sum::Syn(_) => {
Err(SkippedType::new(
"unsupported LF type (forall / struct / syn)",
))
}
}
}
fn con_path(&self, tycon: &lf::TypeConId) -> Result<Vec<String>, SkippedType> {
use lf::self_or_imported_package_id::Sum;
let type_name = rust_name(self.package, tycon.name_interned_dname)?;
let Some(qualify) = &self.qualify else {
return Ok(vec![type_name]);
};
let module_id = tycon
.module
.as_ref()
.ok_or_else(|| SkippedType::new("type constructor without a module"))?;
let module_dotted =
interned_dotted_name(self.package, module_id.module_name_interned_dname)
.ok_or_else(|| SkippedType::new("unresolved referenced module name"))?;
let target_id = match module_id.package_id.as_ref().and_then(|p| p.sum.as_ref()) {
None | Some(Sum::SelfPackageId(_)) => qualify.current_id.to_string(),
Some(Sum::ImportedPackageIdInternedStr(index)) => interned_str(self.package, *index)
.ok_or_else(|| SkippedType::new("unresolved imported package id"))?
.to_string(),
Some(Sum::PackageImportId(index)) => imported_package_id(self.package, *index)
.ok_or_else(|| SkippedType::new("unresolved package import id"))?
.to_string(),
};
let package_module = qualify
.module_names
.get(target_id.as_str())
.ok_or_else(|| {
SkippedType::new(format!("reference to package {target_id} not in the DAR"))
})?;
if let Some(crate_name) = qualify.external.get(&target_id) {
return Ok(vec![
EXTERNAL_CRATE_ROOT.to_string(),
crate_name.clone(),
package_module.clone(),
module_dotted.replace('.', "_"),
type_name,
]);
}
if let Some(selected) = qualify.selected
&& !selected.contains(&target_id)
{
return Err(SkippedType::new(format!(
"reference to {package_module}, which this crate neither \
generates nor references as an external package"
)));
}
Ok(vec![
"crate".to_string(),
package_module.clone(),
module_dotted.replace('.', "_"),
type_name,
])
}
fn builtin(&self, builtin: i32, args: &[&lf::Type]) -> Result<DamlType, SkippedType> {
use lf::BuiltinType;
let kind = BuiltinType::try_from(builtin)
.map_err(|_| SkippedType::new(format!("unknown builtin type {builtin}")))?;
let arg = |index: usize| -> Result<DamlType, SkippedType> {
let ty = args.get(index).copied().ok_or_else(|| {
SkippedType::new(format!("{kind:?} missing type argument {index}"))
})?;
self.type_(ty)
};
let ty = match kind {
BuiltinType::Unit => DamlType::Unit,
BuiltinType::Bool => DamlType::Bool,
BuiltinType::Int64 => DamlType::Int64,
BuiltinType::Date => DamlType::Date,
BuiltinType::Timestamp => DamlType::Timestamp,
BuiltinType::Party => DamlType::Party,
BuiltinType::Text => DamlType::Text,
BuiltinType::Numeric => DamlType::Numeric(self.numeric_scale(args)?),
BuiltinType::ContractId => DamlType::ContractId(Box::new(arg(0)?)),
BuiltinType::Optional => DamlType::Optional(Box::new(arg(0)?)),
BuiltinType::List => DamlType::List(Box::new(arg(0)?)),
BuiltinType::Textmap => DamlType::TextMap(Box::new(arg(0)?)),
BuiltinType::Genmap => DamlType::GenMap(Box::new(arg(0)?), Box::new(arg(1)?)),
other => {
return Err(SkippedType::new(format!(
"builtin type {other:?} is not representable in codegen"
)));
}
};
Ok(ty)
}
fn numeric_scale(&self, args: &[&lf::Type]) -> Result<u8, SkippedType> {
let mut ty = *args
.first()
.ok_or_else(|| SkippedType::new("Numeric without a scale argument"))?;
if let Some(lf::r#type::Sum::InternedType(index)) = &ty.sum {
ty = interned_type(self.package, *index)
.ok_or_else(|| SkippedType::new("unresolved Numeric scale"))?;
}
match &ty.sum {
Some(lf::r#type::Sum::Nat(scale)) => u8::try_from(*scale)
.map_err(|_| SkippedType::new(format!("Numeric scale {scale} out of range"))),
_ => Err(SkippedType::new("Numeric scale is not a type-level Nat")),
}
}
}
fn box_recursion(krate: &mut Crate) {
let mut edges: HashMap<String, Vec<String>> = HashMap::new();
for package in &krate.packages {
for module in &package.modules {
let at = |name: &str| path_key_for(&package.name, &module.name, name);
module_containment_edges(&module.module, &at, &mut edges);
}
}
for package in &mut krate.packages {
let package_name = package.name.clone();
for module in &mut package.modules {
let module_name = module.name.clone();
let at = |name: &str| path_key_for(&package_name, &module_name, name);
module_box_cycles(&mut module.module, &at, &edges);
}
}
}
fn module_containment_edges(
module: &Module,
at: &impl Fn(&str) -> String,
edges: &mut HashMap<String, Vec<String>>,
) {
for data_type in &module.data_types {
match data_type {
DataType::Record(record) => {
let node = edges.entry(at(&record.name)).or_default();
for field in &record.fields {
collect_inline_refs(&field.ty, node);
}
}
DataType::Variant(variant) => {
let node = edges.entry(at(&variant.name)).or_default();
for constructor in &variant.constructors {
if let Some(payload) = &constructor.payload {
collect_inline_refs(payload, node);
}
}
}
DataType::Enum(_) | DataType::InterfaceMarker(_) => {}
}
}
for template in &module.templates {
let node = edges.entry(at(&template.name)).or_default();
for field in &template.fields {
collect_inline_refs(&field.ty, node);
}
}
}
fn module_box_cycles(
module: &mut Module,
at: &impl Fn(&str) -> String,
edges: &HashMap<String, Vec<String>>,
) {
for data_type in &mut module.data_types {
match data_type {
DataType::Record(record) => {
let holder = at(&record.name);
for field in &mut record.fields {
box_cycle_closers(&mut field.ty, &holder, edges);
}
}
DataType::Variant(variant) => {
let holder = at(&variant.name);
for constructor in &mut variant.constructors {
if let Some(payload) = &mut constructor.payload {
box_cycle_closers(payload, &holder, edges);
}
}
}
DataType::Enum(_) | DataType::InterfaceMarker(_) => {}
}
}
for template in &mut module.templates {
let holder = at(&template.name);
for field in &mut template.fields {
box_cycle_closers(&mut field.ty, &holder, edges);
}
}
}
fn path_key_for(package_module: &str, module_name: &str, type_name: &str) -> String {
format!("crate::{package_module}::{module_name}::{type_name}")
}
fn path_key_of(reference: &TypeRef) -> String {
reference.path.join("::")
}
fn collect_inline_refs(ty: &DamlType, out: &mut Vec<String>) {
match ty {
DamlType::Optional(inner) => collect_inline_refs(inner, out),
DamlType::Ref(reference) => {
out.push(path_key_of(reference));
for arg in &reference.args {
collect_inline_refs(arg, out);
}
}
_ => {}
}
}
fn inline_reaches(edges: &HashMap<String, Vec<String>>, from: &str, to: &str) -> bool {
let mut stack = vec![from];
let mut seen = std::collections::HashSet::new();
while let Some(node) = stack.pop() {
if node == to {
return true;
}
if seen.insert(node)
&& let Some(next) = edges.get(node)
{
stack.extend(next.iter().map(String::as_str));
}
}
false
}
fn box_cycle_closers(ty: &mut DamlType, holder: &str, edges: &HashMap<String, Vec<String>>) {
match ty {
DamlType::Optional(inner) => box_cycle_closers(inner, holder, edges),
DamlType::Ref(reference) => {
for arg in &mut reference.args {
box_cycle_closers(arg, holder, edges);
}
let key = path_key_of(reference);
if key == holder || inline_reaches(edges, &key, holder) {
let inner = std::mem::replace(ty, DamlType::Unit);
*ty = DamlType::Boxed(Box::new(inner));
}
}
_ => {}
}
}
fn field_type(field: &lf::FieldWithType) -> Result<&lf::Type, SkippedType> {
field
.r#type
.as_ref()
.ok_or_else(|| SkippedType::new("field without a type"))
}
fn data_type_name(data_type: &DataType) -> &str {
match data_type {
DataType::Record(record) => &record.name,
DataType::Variant(variant) => &variant.name,
DataType::Enum(enumeration) => &enumeration.name,
DataType::InterfaceMarker(name) => name,
}
}
fn rust_name(package: &lf::Package, name_interned_dname: i32) -> Result<String, SkippedType> {
let dotted = interned_dotted_name(package, name_interned_dname)
.ok_or_else(|| SkippedType::new("unresolved dotted name"))?;
let name = dotted.replace('.', "_");
if is_rust_ident(&name) {
Ok(name)
} else {
Err(SkippedType::new(format!(
"`{dotted}` is not representable as a Rust identifier"
)))
}
}
fn check_type_params(name: &str, params: &[String]) -> Result<(), SkippedType> {
let mut seen: std::collections::BTreeMap<String, &str> = std::collections::BTreeMap::new();
for param in params {
let ident = param.to_upper_camel_case();
if !is_rust_ident(&ident) {
return Err(SkippedType::new(format!(
"{name}: type parameter `{param}` is not representable as a Rust \
generic parameter"
)));
}
if let Some(previous) = seen.insert(ident.clone(), param) {
return Err(SkippedType::new(format!(
"{name}: type parameters `{previous}` and `{param}` both map to the \
Rust generic parameter `{ident}`; rename one in Daml"
)));
}
}
Ok(())
}
fn is_source_safe(name: &str) -> bool {
!name.chars().any(char::is_control) && !name.contains("*/")
}
fn is_rust_ident(name: &str) -> bool {
!name.is_empty()
&& !name.starts_with(|c: char| c.is_ascii_digit())
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use canton_lf::Dar;
fn package_with(names: &[&str]) -> lf::Package {
let mut strings: Vec<String> = Vec::new();
let mut dotted = Vec::new();
for name in names {
let segments = name
.split('.')
.map(|segment| {
strings.push(segment.to_string());
i32::try_from(strings.len() - 1).unwrap()
})
.collect();
dotted.push(lf::InternedDottedName {
segments_interned_str: segments,
});
}
lf::Package {
interned_strings: strings,
interned_dotted_names: dotted,
..lf::Package::default()
}
}
fn record_def(name: i32, label: i32) -> lf::DefDataType {
lf::DefDataType {
name_interned_dname: name,
serializable: true,
data_cons: Some(lf::def_data_type::DataCons::Record(
lf::def_data_type::Fields {
fields: vec![lf::FieldWithType {
field_interned_str: label,
r#type: Some(lf::Type {
sum: Some(lf::r#type::Sum::Builtin(lf::r#type::Builtin {
builtin: lf::BuiltinType::Party as i32,
args: vec![],
})),
}),
}],
},
)),
..lf::DefDataType::default()
}
}
#[test]
fn two_daml_names_that_flatten_alike_are_skipped_not_silently_merged() {
let mut package = package_with(&["Foo.Bar", "Foo_Bar"]);
package.interned_strings.push("owner".to_string());
package.interned_strings.push("note".to_string());
let (owner, note) = (3, 4);
package.modules.push(lf::Module {
name_interned_dname: 1, data_types: vec![record_def(0, note), record_def(1, owner)],
templates: vec![lf::DefTemplate {
tycon_interned_dname: 1,
..lf::DefTemplate::default()
}],
..lf::Module::default()
});
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
let reasons: Vec<String> = skipped.iter().map(ToString::to_string).collect();
assert!(
reasons.iter().any(|r| r.contains("Foo.Bar")
&& r.contains("Foo_Bar")
&& r.contains("map to the Rust name")),
"the clash must be reported naming both Daml sources: {reasons:?}"
);
let emitted: Vec<&str> = krate
.packages
.iter()
.flat_map(|p| &p.modules)
.flat_map(|m| m.module.templates.iter().map(|t| t.name.as_str()))
.collect();
assert!(!emitted.contains(&"Foo_Bar"), "emitted: {emitted:?}");
}
fn named_package(name: &str, version: Option<&str>) -> lf::Package {
lf::Package {
interned_strings: vec![name.to_string(), version.unwrap_or_default().to_string()],
metadata: Some(lf::PackageMetadata {
name_interned_str: 0,
version_interned_str: 1,
upgraded_package_id: None,
}),
..lf::Package::default()
}
}
fn module_names(packages: &[(&str, lf::Package)]) -> Vec<String> {
let owned: Vec<(String, lf::Package)> = packages
.iter()
.map(|(id, package)| ((*id).to_string(), package.clone()))
.collect();
let names = package_module_names(&owned);
owned
.iter()
.map(|(id, _)| names[id.as_str()].clone())
.collect()
}
#[test]
fn a_package_module_is_named_after_the_package_alone() {
assert_eq!(
module_names(&[
("aaa", named_package("quickstart-licensing", Some("0.0.1"))),
("bbb", named_package("splice-amulet", Some("0.1.14"))),
]),
["quickstart_licensing", "splice_amulet"],
);
}
#[test]
fn two_versions_of_one_package_are_separated_by_version() {
assert_eq!(
module_names(&[
("aaa", named_package("my-app", Some("1.0.0"))),
("bbb", named_package("my-app", Some("2.0.0"))),
("ccc", named_package("other", Some("1.0.0"))),
]),
["my_app_1_0_0", "my_app_2_0_0", "other"],
);
}
#[test]
fn packages_alike_down_to_the_version_fall_back_to_the_id() {
assert_eq!(
module_names(&[
("aaaaaaaaaaaa", named_package("my-app", Some("1.0.0"))),
("bbbbbbbbbbbb", named_package("my-app", Some("1.0.0"))),
("cccccccccccc", named_package("no-version", Some(""))),
("dddddddddddd", named_package("no-version", None)),
("eeeeeeeeeeee", lf::Package::default()),
]),
[
"my_app_1_0_0_aaaaaaaa",
"my_app_1_0_0_bbbbbbbb",
"no_version_cccccccc",
"no_version_dddddddd",
"p_eeeeeeee",
],
);
}
#[test]
fn ids_sharing_a_prefix_still_get_distinct_module_names() {
let names = module_names(&[
("aaaaaaaa1111", named_package("my-app", Some("1.0.0"))),
("aaaaaaaa2222", named_package("my-app", Some("1.0.0"))),
]);
assert_eq!(names.len(), 2);
assert_ne!(names[0], names[1], "{names:?}");
assert!(
names.iter().all(|n| n.starts_with("my_app_1_0_0_")),
"{names:?}"
);
}
#[test]
fn a_non_serializable_claimant_does_not_take_a_real_type_down() {
let mut package = package_with(&["Mod", "Handler.OnEvent", "Handler_OnEvent"]);
package.interned_strings.push("owner".to_string());
let owner = i32::try_from(package.interned_strings.len() - 1).unwrap();
let mut ghost = record_def(1, owner);
ghost.serializable = false;
package.modules.push(lf::Module {
name_interned_dname: 0,
data_types: vec![ghost, record_def(2, owner)],
..lf::Module::default()
});
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
assert!(skipped.is_empty(), "nothing collides: {skipped:?}");
let emitted: Vec<&str> = krate.packages[0].modules[0]
.module
.data_types
.iter()
.map(data_type_name)
.collect();
assert_eq!(emitted, ["Handler_OnEvent"]);
}
#[test]
fn a_module_that_emits_nothing_does_not_block_one_that_does() {
let mut package = package_with(&["Util.Text", "Util_Text"]);
package.interned_strings.push("owner".to_string());
let owner = i32::try_from(package.interned_strings.len() - 1).unwrap();
package.modules.push(lf::Module {
name_interned_dname: 0, ..lf::Module::default()
});
package.modules.push(lf::Module {
name_interned_dname: 1, data_types: vec![record_def(1, owner)],
..lf::Module::default()
});
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
assert!(skipped.is_empty(), "nothing collides: {skipped:?}");
let modules: Vec<&str> = krate.packages[0]
.modules
.iter()
.map(|m| m.name.as_str())
.collect();
assert_eq!(modules, ["Util_Text"]);
}
#[test]
fn two_type_parameters_that_upper_camel_alike_are_skipped() {
let mut package = package_with(&["Mod", "Pair"]);
let base = i32::try_from(package.interned_strings.len()).unwrap();
package.interned_strings.push("a".to_string());
package.interned_strings.push("A".to_string());
let mut pair = record_def(1, base);
pair.params = vec![
lf::TypeVarWithKind {
var_interned_str: base,
kind: None,
},
lf::TypeVarWithKind {
var_interned_str: base + 1,
kind: None,
},
];
package.modules.push(lf::Module {
name_interned_dname: 0,
data_types: vec![pair],
..lf::Module::default()
});
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
assert!(
skipped
.iter()
.any(|s| s.to_string().contains("Rust generic parameter `A`")),
"{skipped:?}"
);
assert!(krate.packages.is_empty(), "nothing emitted");
}
#[test]
fn the_same_package_id_twice_yields_one_module() {
let mut package = package_with(&["Mod", "T"]);
package.interned_strings.push("owner".to_string());
let owner = i32::try_from(package.interned_strings.len() - 1).unwrap();
package.modules.push(lf::Module {
name_interned_dname: 0,
data_types: vec![record_def(1, owner)],
..lf::Module::default()
});
let (krate, _) = lower_crate(&[
("aa".to_string(), package.clone()),
("aa".to_string(), package),
]);
let names: Vec<&str> = krate.packages.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names.len(), 1, "{names:?}");
}
#[test]
fn two_module_names_that_flatten_alike_are_skipped() {
let mut package = package_with(&["A.B", "A_B", "T"]);
package.interned_strings.push("owner".to_string());
let owner = i32::try_from(package.interned_strings.len() - 1).unwrap();
for name in [0, 1] {
package.modules.push(lf::Module {
name_interned_dname: name,
data_types: vec![record_def(2, owner)],
..lf::Module::default()
});
}
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
assert!(
skipped
.iter()
.any(|s| s.to_string().contains("map to the Rust module")),
"{skipped:?}"
);
let modules: Vec<&str> = krate
.packages
.iter()
.flat_map(|p| p.modules.iter().map(|m| m.name.as_str()))
.collect();
assert!(modules.is_empty(), "no module may be emitted: {modules:?}");
}
#[test]
fn an_optional_passed_to_a_generic_that_nests_it_is_refused_not_mis_encoded() {
let reference = |name: &str, args: Vec<DamlType>| {
DamlType::Ref(TypeRef {
path: vec![
"crate".to_string(),
"pkg_1_0_0".to_string(),
"Mod".to_string(),
name.to_string(),
],
args,
})
};
let field = |label: &str, ty: DamlType| Field {
label: label.to_string(),
ty,
};
let module = |data_types: Vec<DataType>| Crate {
packages: vec![PackageModule {
name: "pkg_1_0_0".to_string(),
modules: vec![NamedModule {
name: "Mod".to_string(),
module: Module {
data_types,
..Module::default()
},
}],
}],
};
let wrap_nesting = DataType::Record(Record {
name: "Wrap".to_string(),
type_params: vec!["a".to_string()],
fields: vec![field(
"value",
DamlType::Optional(Box::new(DamlType::Var("a".to_string()))),
)],
});
let user = |arg: DamlType| {
DataType::Record(Record {
name: "User".to_string(),
type_params: vec![],
fields: vec![field("w", reference("Wrap", vec![arg]))],
})
};
let mut krate = module(vec![
wrap_nesting.clone(),
user(DamlType::Optional(Box::new(DamlType::Text))),
]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
assert!(
errors
.iter()
.any(|e| e.to_string().contains("not yet representable")),
"{errors:?}"
);
let names: Vec<&str> = krate.packages[0].modules[0]
.module
.data_types
.iter()
.map(data_type_name)
.collect();
assert_eq!(names, ["Wrap"], "only the ambiguous user is dropped");
let mut fine = module(vec![wrap_nesting, user(DamlType::Text)]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut fine, &mut errors);
assert!(errors.is_empty(), "{errors:?}");
let wrap_plain = DataType::Record(Record {
name: "Wrap".to_string(),
type_params: vec!["a".to_string()],
fields: vec![field("value", DamlType::Var("a".to_string()))],
});
let mut plain = module(vec![
wrap_plain,
user(DamlType::Optional(Box::new(DamlType::Text))),
]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut plain, &mut errors);
assert!(errors.is_empty(), "{errors:?}");
assert_eq!(plain.packages[0].modules[0].module.data_types.len(), 2);
}
#[test]
fn nesting_introduced_through_an_intermediate_generic_is_still_refused() {
let mut krate = generics_module(vec![
generic_record("Wrap", &["a"], vec![("value", optional(var("a")))]),
generic_record(
"Outer",
&["a"],
vec![("inner", reference("Wrap", vec![var("a")]))],
),
generic_record(
"User",
&[],
vec![("o", reference("Outer", vec![optional(DamlType::Text)]))],
),
]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
assert!(
errors
.iter()
.any(|e| e.to_string().contains("not yet representable")),
"the transitive nesting must be refused, not mis-encoded: {errors:?}"
);
let names: Vec<&str> = krate.packages[0].modules[0]
.module
.data_types
.iter()
.map(data_type_name)
.collect();
assert_eq!(
names,
["Wrap", "Outer"],
"only the instantiation site is dropped"
);
}
#[test]
fn mutually_recursive_generics_reach_a_fixpoint() {
let mut krate = generics_module(vec![
generic_record("Ping", &["a"], vec![("p", optional(var("a")))]),
generic_record(
"Pong",
&["a"],
vec![
("q", reference("Ping", vec![var("a")])),
("r", optional(reference("Pong", vec![var("a")]))),
],
),
generic_record(
"User",
&[],
vec![("o", reference("Pong", vec![optional(DamlType::Text)]))],
),
]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
assert!(
errors
.iter()
.any(|e| e.to_string().contains("not yet representable")),
"{errors:?}"
);
}
#[test]
fn a_container_between_the_two_optionals_is_not_a_nested_optional() {
for container in [
DamlType::List(Box::new(var("a"))),
DamlType::TextMap(Box::new(var("a"))),
DamlType::GenMap(Box::new(DamlType::Text), Box::new(var("a"))),
] {
let mut krate = generics_module(vec![
generic_record("Wrap", &["a"], vec![("value", optional(container))]),
generic_record(
"User",
&[],
vec![("w", reference("Wrap", vec![optional(DamlType::Text)]))],
),
]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
assert!(errors.is_empty(), "must not be refused: {errors:?}");
assert_eq!(krate.packages[0].modules[0].module.data_types.len(), 2);
}
}
#[test]
fn dropping_an_interface_keeps_its_marker_reachable() {
let mut krate = Crate {
packages: vec![PackageModule {
name: "pkg".to_string(),
modules: vec![NamedModule {
name: "Mod".to_string(),
module: Module {
data_types: vec![DataType::InterfaceMarker("Holding".to_string())],
templates: vec![Template {
name: "Vault".to_string(),
module_name: "Mod".to_string(),
package_id: "id".to_string(),
package_name: "pkg".to_string(),
fields: vec![Field {
label: "held".to_string(),
ty: DamlType::ContractId(Box::new(reference("Holding", vec![]))),
}],
choices: vec![],
key: None,
}],
interfaces: vec![Interface {
name: "Holding".to_string(),
module_name: "Mod".to_string(),
package_id: "id".to_string(),
package_name: "pkg".to_string(),
view: Some(reference("HoldingView", vec![])),
choices: vec![],
}],
},
}],
}],
};
let mut errors = Vec::new();
drop_dangling_references(&mut krate, &mut errors);
let module = &krate.packages[0].modules[0].module;
assert!(module.interfaces.is_empty(), "the interface itself must go");
assert_eq!(
module.data_types.len(),
1,
"the marker is still emitted: {:?}",
module
.data_types
.iter()
.map(data_type_name)
.collect::<Vec<_>>()
);
let templates: Vec<&str> = module.templates.iter().map(|t| t.name.as_str()).collect();
assert_eq!(
templates,
["Vault"],
"a ContractId of the marker still resolves; reasons: {errors:?}"
);
}
#[test]
fn a_contract_id_type_argument_is_not_a_value_position() {
let mut krate = generics_module(vec![
generic_record("Wrap", &["a"], vec![("value", optional(var("a")))]),
generic_record(
"Outer",
&["a"],
vec![(
"c",
DamlType::ContractId(Box::new(reference("Wrap", vec![var("a")]))),
)],
),
generic_record(
"User",
&[],
vec![("o", reference("Outer", vec![optional(DamlType::Text)]))],
),
]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
assert!(errors.is_empty(), "must not be refused: {errors:?}");
assert_eq!(krate.packages[0].modules[0].module.data_types.len(), 3);
}
#[test]
fn a_parameter_handed_on_inside_a_container_does_not_propagate() {
let mut krate = generics_module(vec![
generic_record("Wrap", &["b"], vec![("value", optional(var("b")))]),
generic_record(
"Outer",
&["a"],
vec![(
"inner",
reference("Wrap", vec![DamlType::List(Box::new(var("a")))]),
)],
),
generic_record(
"User",
&[],
vec![("o", reference("Outer", vec![optional(DamlType::Text)]))],
),
]);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
assert!(errors.is_empty(), "must not be refused: {errors:?}");
}
fn var(name: &str) -> DamlType {
DamlType::Var(name.to_string())
}
fn optional(ty: DamlType) -> DamlType {
DamlType::Optional(Box::new(ty))
}
fn reference(name: &str, args: Vec<DamlType>) -> DamlType {
DamlType::Ref(TypeRef {
path: vec![
"crate".to_string(),
"pkg".to_string(),
"Mod".to_string(),
name.to_string(),
],
args,
})
}
fn generic_record(name: &str, params: &[&str], fields: Vec<(&str, DamlType)>) -> DataType {
DataType::Record(Record {
name: name.to_string(),
type_params: params.iter().map(|p| (*p).to_string()).collect(),
fields: fields
.into_iter()
.map(|(label, ty)| Field {
label: label.to_string(),
ty,
})
.collect(),
})
}
fn generics_module(data_types: Vec<DataType>) -> Crate {
Crate {
packages: vec![PackageModule {
name: "pkg".to_string(),
modules: vec![NamedModule {
name: "Mod".to_string(),
module: Module {
data_types,
..Module::default()
},
}],
}],
}
}
#[test]
fn both_post_lowering_passes_cover_templates_and_interfaces() {
let reference = |name: &str, args: Vec<DamlType>| {
DamlType::Ref(TypeRef {
path: vec![
"crate".to_string(),
"pkg_1_0_0".to_string(),
"Mod".to_string(),
name.to_string(),
],
args,
})
};
let module_of = |data_types: Vec<DataType>,
templates: Vec<Template>,
interfaces: Vec<Interface>| Crate {
packages: vec![PackageModule {
name: "pkg_1_0_0".to_string(),
modules: vec![NamedModule {
name: "Mod".to_string(),
module: Module {
data_types,
templates,
interfaces,
},
}],
}],
};
let choice = |ty: DamlType| Choice {
name: "Do".to_string(),
consuming: true,
argument: DamlType::Unit,
returns: ty,
};
let template_of = |name: &str, field: DamlType| Template {
name: name.to_string(),
module_name: "Mod".to_string(),
package_id: "aa".to_string(),
package_name: "pkg".to_string(),
fields: vec![Field {
label: "x".to_string(),
ty: field,
}],
choices: vec![],
key: None,
};
let mut krate = module_of(
vec![DataType::InterfaceMarker("Iface".to_string())],
vec![],
vec![Interface {
name: "Iface".to_string(),
module_name: "Mod".to_string(),
package_id: "aa".to_string(),
package_name: "pkg".to_string(),
view: None,
choices: vec![choice(reference("Bad", vec![]))], }],
);
let mut errors = Vec::new();
drop_dangling_references(&mut krate, &mut errors);
assert!(
errors
.iter()
.any(|e| e.to_string().contains("`Iface` references `Bad`")),
"an interface dangles like anything else: {errors:?}"
);
assert!(
krate.packages.is_empty() || krate.packages[0].modules[0].module.interfaces.is_empty(),
"the interface must not be emitted"
);
let wrap = DataType::Record(Record {
name: "Wrap".to_string(),
type_params: vec!["a".to_string()],
fields: vec![Field {
label: "value".to_string(),
ty: DamlType::Optional(Box::new(DamlType::Var("a".to_string()))),
}],
});
let mut krate = module_of(
vec![wrap],
vec![template_of(
"Tpl",
reference("Wrap", vec![DamlType::Optional(Box::new(DamlType::Text))]),
)],
vec![],
);
let mut errors = Vec::new();
drop_ambiguous_nested_optionals(&mut krate, &mut errors);
assert!(
errors
.iter()
.any(|e| e.to_string().contains("not yet representable")),
"a template payload is an instantiation site too: {errors:?}"
);
assert!(
krate.packages[0].modules[0].module.templates.is_empty(),
"the template must not be emitted"
);
}
#[test]
fn a_type_referencing_a_skipped_type_is_dropped_transitively() {
let path = |name: &str| {
DamlType::Ref(TypeRef {
path: vec![
"crate".to_string(),
"pkg_1_0_0".to_string(),
"Mod".to_string(),
name.to_string(),
],
args: vec![],
})
};
let record = |name: &str, field: DamlType| {
DataType::Record(Record {
name: name.to_string(),
type_params: vec![],
fields: vec![Field {
label: "x".to_string(),
ty: field,
}],
})
};
let mut krate = Crate {
packages: vec![PackageModule {
name: "pkg_1_0_0".to_string(),
modules: vec![NamedModule {
name: "Mod".to_string(),
module: Module {
data_types: vec![
record("Good", path("Bad")),
record("Worse", path("Good")),
record("Fine", DamlType::Text),
],
..Module::default()
},
}],
}],
};
let mut errors = Vec::new();
drop_dangling_references(&mut krate, &mut errors);
let surviving: Vec<&str> = krate.packages[0].modules[0]
.module
.data_types
.iter()
.map(data_type_name)
.collect();
assert_eq!(surviving, ["Fine"], "only the self-contained type survives");
let reasons: Vec<String> = errors.iter().map(ToString::to_string).collect();
assert!(
reasons
.iter()
.any(|r| r.contains("`Good` references `Bad`")),
"{reasons:?}"
);
assert!(
reasons
.iter()
.any(|r| r.contains("`Worse` references `Good`")),
"the drop must cascade: {reasons:?}"
);
}
#[test]
fn a_daml_name_rust_cannot_spell_is_skipped_not_panicked_on() {
assert!(is_rust_ident("Red"));
assert!(is_rust_ident("_x9"));
for ok in ["Accept", "owner", "splice-amulet", "Amulet_Transfer", "a b"] {
assert!(is_source_safe(ok), "{ok:?}");
}
for bad in [
"Accept\n```",
"a\rb",
"tab\there",
"end*/",
"\u{7f}",
"\u{85}",
] {
assert!(!is_source_safe(bad), "{bad:?} must be refused");
}
for bad in ["Red'", "", "9lives", "naïve", "a-b", "A.B"] {
assert!(!is_rust_ident(bad), "{bad:?} is not a Rust identifier");
}
let mut package = package_with(&["Colour"]);
package.interned_strings.push("Red'".to_string());
let constructor = i32::try_from(package.interned_strings.len() - 1).unwrap();
package.modules.push(lf::Module {
name_interned_dname: 0,
data_types: vec![lf::DefDataType {
name_interned_dname: 0,
serializable: true,
data_cons: Some(lf::def_data_type::DataCons::Enum(
lf::def_data_type::EnumConstructors {
constructors_interned_str: vec![constructor],
},
)),
..lf::DefDataType::default()
}],
..lf::Module::default()
});
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
assert!(
skipped
.iter()
.any(|s| s.to_string().contains("enum constructor `Red'`")),
"{skipped:?}"
);
assert!(krate.packages.is_empty(), "nothing may be emitted");
}
#[test]
fn the_collision_suffix_is_never_empty() {
assert_ne!(ident_prefix("€€€€"), ident_prefix("£££££"));
assert!(!ident_prefix("€€€€").is_empty());
assert_eq!(ident_prefix("aabbccddee"), "aabbccdd");
}
fn qualified(name: &str) -> DamlType {
DamlType::Ref(TypeRef {
path: vec![
"crate".to_string(),
"pkg_1_0_0".to_string(),
"Mod".to_string(),
name.to_string(),
],
args: vec![],
})
}
fn record(name: &str, fields: Vec<(&str, DamlType)>) -> DataType {
DataType::Record(Record {
name: name.to_string(),
type_params: vec![],
fields: fields
.into_iter()
.map(|(label, ty)| Field {
label: label.to_string(),
ty,
})
.collect(),
})
}
fn crate_of(data_types: Vec<DataType>) -> Crate {
Crate {
packages: vec![PackageModule {
name: "pkg_1_0_0".to_string(),
modules: vec![NamedModule {
name: "Mod".to_string(),
module: Module {
data_types,
..Module::default()
},
}],
}],
}
}
fn field_ty(krate: &Crate, type_index: usize, field_index: usize) -> &DamlType {
match &krate.packages[0].modules[0].module.data_types[type_index] {
DataType::Record(record) => &record.fields[field_index].ty,
other => panic!("expected a record, got {other:?}"),
}
}
#[test]
fn optional_self_recursion_is_boxed() {
let mut krate = crate_of(vec![record(
"Tree",
vec![
("left", DamlType::Optional(Box::new(qualified("Tree")))),
("size", DamlType::Int64),
],
)]);
box_recursion(&mut krate);
assert_eq!(
field_ty(&krate, 0, 0),
&DamlType::Optional(Box::new(DamlType::Boxed(Box::new(qualified("Tree"))))),
);
assert_eq!(field_ty(&krate, 0, 1), &DamlType::Int64);
}
#[test]
fn mutual_recursion_is_boxed() {
let mut krate = crate_of(vec![
record("A", vec![("b", qualified("B"))]),
record(
"B",
vec![("a", DamlType::Optional(Box::new(qualified("A"))))],
),
]);
box_recursion(&mut krate);
assert_eq!(
field_ty(&krate, 0, 0),
&DamlType::Boxed(Box::new(qualified("B")))
);
assert_eq!(
field_ty(&krate, 1, 0),
&DamlType::Optional(Box::new(DamlType::Boxed(Box::new(qualified("A"))))),
);
}
#[test]
fn generic_instantiation_recursion_is_boxed() {
let wrap_of_t = DamlType::Ref(TypeRef {
path: vec![
"crate".to_string(),
"pkg_1_0_0".to_string(),
"Mod".to_string(),
"Wrap".to_string(),
],
args: vec![qualified("T")],
});
let mut krate = crate_of(vec![
DataType::Record(Record {
name: "Wrap".to_string(),
type_params: vec!["a".to_string()],
fields: vec![Field {
label: "w".to_string(),
ty: DamlType::Var("a".to_string()),
}],
}),
record("T", vec![("w", wrap_of_t)]),
]);
box_recursion(&mut krate);
let DamlType::Ref(reference) = field_ty(&krate, 1, 0) else {
panic!("outer Wrap reference must stay a Ref");
};
assert_eq!(
reference.args[0],
DamlType::Boxed(Box::new(qualified("T"))),
"the recursive argument occurrence is boxed"
);
}
#[test]
fn heap_containers_already_break_cycles() {
let mut krate = crate_of(vec![record(
"Tree",
vec![
("children", DamlType::List(Box::new(qualified("Tree")))),
("index", DamlType::TextMap(Box::new(qualified("Tree")))),
],
)]);
let before = krate.clone();
box_recursion(&mut krate);
assert_eq!(krate, before, "heap-indirected recursion is left alone");
}
#[test]
fn lowers_a_real_dar_to_a_qualified_crate() {
let Ok(path) = std::env::var("CANTON_TEST_DAR") else {
eprintln!("skipping lower test: set CANTON_TEST_DAR=/path/to/x.dar");
return;
};
let dar = Dar::open(&path).expect("open DAR");
let (krate, errors) = lower_dar(&dar).expect("decode + lower DAR");
assert!(
!krate.packages.is_empty(),
"a DAR should lower at least one package"
);
let unresolved: Vec<&SkippedType> = errors
.iter()
.filter(|error| error.reason().contains("not in the DAR"))
.collect();
assert!(
unresolved.is_empty(),
"every cross-package reference should resolve: {unresolved:?}"
);
let src = crate::generate_crate(&krate).expect("generate crate");
syn::parse_file(&src).expect("generated source must be valid Rust");
let modules: usize = krate.packages.iter().map(|p| p.modules.len()).sum();
let types: usize = krate
.packages
.iter()
.flat_map(|p| &p.modules)
.map(|m| m.module.data_types.len())
.sum();
println!(
"lowered {} packages / {modules} modules / {types} data types → {} bytes of Rust ({} skipped)",
krate.packages.len(),
src.len(),
errors.len(),
);
}
#[test]
fn an_interned_type_cycle_is_refused_instead_of_overflowing_the_stack() {
let mut package = package_with(&["M.T"]);
package.interned_strings.push("f".to_string());
let label = i32::try_from(package.interned_strings.len() - 1).unwrap();
package.interned_types = vec![lf::Type {
sum: Some(lf::r#type::Sum::InternedType(0)),
}];
let mut def = record_def(0, label);
if let Some(lf::def_data_type::DataCons::Record(fields)) = &mut def.data_cons {
fields.fields[0].r#type = Some(lf::Type {
sum: Some(lf::r#type::Sum::InternedType(0)),
});
}
package.modules = vec![lf::Module {
name_interned_dname: 0,
data_types: vec![def],
..lf::Module::default()
}];
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
assert!(
skipped.iter().any(|s| s.to_string().contains("cycle")),
"should be reported as a skipped type: {skipped:?}"
);
let emitted: usize = krate
.packages
.iter()
.flat_map(|p| &p.modules)
.map(|m| m.module.data_types.len())
.sum();
assert_eq!(
emitted, 0,
"a type that could not be resolved is not emitted"
);
}
#[test]
fn an_indirect_interned_cycle_is_refused_too() {
let mut package = package_with(&["M.T"]);
package.interned_strings.push("f".to_string());
let label = i32::try_from(package.interned_strings.len() - 1).unwrap();
package.interned_types = vec![
lf::Type {
sum: Some(lf::r#type::Sum::InternedType(1)),
},
lf::Type {
sum: Some(lf::r#type::Sum::InternedType(0)),
},
];
let mut def = record_def(0, label);
if let Some(lf::def_data_type::DataCons::Record(fields)) = &mut def.data_cons {
fields.fields[0].r#type = Some(lf::Type {
sum: Some(lf::r#type::Sum::InternedType(0)),
});
}
package.modules = vec![lf::Module {
name_interned_dname: 0,
data_types: vec![def],
..lf::Module::default()
}];
let (_krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
assert!(
skipped.iter().any(|s| s.to_string().contains("cycle")),
"{skipped:?}"
);
}
#[test]
fn a_name_declared_twice_is_refused_rather_than_silently_resolved() {
let mut package = package_with(&["M", "T"]);
package.interned_strings.push("a".to_string());
let a = i32::try_from(package.interned_strings.len() - 1).unwrap();
package.interned_strings.push("b".to_string());
let b = i32::try_from(package.interned_strings.len() - 1).unwrap();
package.modules = vec![lf::Module {
name_interned_dname: 0,
data_types: vec![record_def(1, a), record_def(1, b)],
templates: vec![lf::DefTemplate {
tycon_interned_dname: 1,
..lf::DefTemplate::default()
}],
..lf::Module::default()
}];
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
let reasons: Vec<String> = skipped.iter().map(ToString::to_string).collect();
assert!(
reasons
.iter()
.any(|r| r.contains("declared more than once")),
"the duplicate must be reported: {reasons:?}"
);
let templates: Vec<&str> = krate
.packages
.iter()
.flat_map(|p| &p.modules)
.flat_map(|m| &m.module.templates)
.flat_map(|t| t.fields.iter().map(|f| f.label.as_str()))
.collect();
assert!(
templates.is_empty() || templates == ["a"],
"a template must not silently take the second declaration's fields: {templates:?}"
);
}
#[test]
fn a_module_declared_twice_is_refused() {
let mut package = package_with(&["M"]);
package.interned_strings.push("f".to_string());
let label = i32::try_from(package.interned_strings.len() - 1).unwrap();
let module = lf::Module {
name_interned_dname: 0,
data_types: vec![record_def(0, label)],
..lf::Module::default()
};
package.modules = vec![module.clone(), module];
let (krate, skipped) = lower_crate(&[("aa".to_string(), package)]);
let reasons: Vec<String> = skipped.iter().map(ToString::to_string).collect();
assert!(
reasons
.iter()
.any(|r| r.contains("declared more than once")),
"{reasons:?}"
);
let modules: usize = krate.packages.iter().map(|p| p.modules.len()).sum();
assert_eq!(modules, 1, "only one module may be emitted");
}
}