Generated Rust from Concerto CTO models is written by RustVisitor (lib/codegen/fromcto/rust/rustvisitor.js). Snapshot/unit tests do not compile the output; test/verification/rust.compile.test.js can run cargo check, but hr_base / hr_integration are skipped because they fail.
Related: bug: GraphQL verification bugs (generated output fails graphql-js) #248
Unlike Proto3, Option<HashMap<...>> is valid Rust - so the Proto “optional map” bug does not apply. The HR fixtures fail for different reasons.
Bug 1: Custom scalars referenced in map aliases, but never emitted
Cause
hr_base.cto / hr.cto define scalars (SSN, Time, KinName, KinTelephone) and use them as map keys/values:
map EmployeeTShirtSizes {
o SSN
o TShirtSizeType
}
map EmployeeDirectory {
o SSN
o Employee
}
RustVisitor.visit() no-ops scalar declarations:
} else if (thing.isScalarDeclaration?.()) {
return;
}
visitMapDeclaration still emits the scalar name as a Rust type:
visitMapDeclaration(mapDeclaration, parameters) {
const mapKeyType = mapDeclaration.getKey().getType();
const mapValueType = mapDeclaration.getValue().getType();
const keyType = this.toRustType(mapKeyType);
const valueType = this.toRustType(mapValueType);
parameters.fileWriter.writeLine(
0,
`pub type ${mapDeclaration.getName()} = HashMap<${keyType}, ${valueType}>;`
);
So the crate names types that were never defined.
Reproduce
From the repo root (after npm ci, with Rust toolchain installed). Written for PowerShell:
@'
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
const { dir } = require("tmp-promise");
const { ModelManager } = require("@accordproject/concerto-core");
const { FileWriter } = require("@accordproject/concerto-util");
const RustVisitor = require("./lib/codegen/fromcto/rust/rustvisitor.js");
process.env.ENABLE_MAP_TYPE = "true";
process.env.IMPORT_ALIASING = "true";
const MODEL_DIR = "./test/codegen/fromcto/data/model";
(async () => {
const mm = new ModelManager();
mm.addCTOModel(fs.readFileSync(path.join(MODEL_DIR, "hr_base.cto"), "utf8"), "hr_base.cto");
mm.addCTOModel(fs.readFileSync(path.join(MODEL_DIR, "hr.cto"), "utf8"), "hr.cto");
const { path: out, cleanup } = await dir({ unsafeCleanup: true });
const src = path.join(out, "src");
fs.mkdirSync(src, { recursive: true });
mm.accept(new RustVisitor(), {
fileWriter: new FileWriter(src),
showCompositionRelationships: true,
});
fs.renameSync(path.join(src, "mod.rs"), path.join(src, "lib.rs"));
fs.writeFileSync(path.join(out, "Cargo.toml"), [
"[package]", 'name = "verify"', 'version = "0.1.0"', 'edition = "2021"',
"", "[lib]", 'path = "src/lib.rs"', "",
"[dependencies]",
'serde = { version = "1", features = ["derive"] }',
'chrono = { version = "0.4", features = ["serde"] }', "",
].join("\n"));
try {
execFileSync("cargo", ["check", "--quiet"], {
cwd: out, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"],
});
console.log("OK");
} catch (err) {
console.error([err.stdout, err.stderr].filter(Boolean).join("\n").trim());
process.exit(1);
} finally {
await cleanup();
}
})();
'@ | node
Expected errors (among others)
error[E0425]: cannot find type `SSN` in this scope
error[E0425]: cannot find type `Time` in this scope
error[E0425]: cannot find type `KinName` in this scope
error[E0425]: cannot find type `KinTelephone` in this scope
Generated fragments
pub type EmployeeTShirtSizes = HashMap<SSN, TShirtSizeType>;
pub type EmployeeLoginTimes = HashMap<String, Time>;
pub type EmployeeSocialSecurityNumbers = HashMap<String, SSN>;
pub type NextOfKin = HashMap<KinName, KinTelephone>;
pub type EmployeeDirectory = HashMap<SSN, Employee>;
Minimal repro:
@'
use std::collections::HashMap;
pub type EmployeeDirectory = HashMap<SSN, Employee>;
'@ | Set-Content tmp.rs; rustc --crate-type lib tmp.rs
→ cannot find type 'SSN' in this scope
Bug 2: Inconsistent scalar handling (aliases vs field sites)
Cause
When a field is a map type, visitField unwraps scalars to the underlying primitive:
} else if (ModelUtil.isScalar(mapDeclaration.getKey())) {
const scalarDeclaration = mapDeclaration
.getModelFile()
.getType(mapDeclaration.getKey().getType());
const scalarType = scalarDeclaration.getType();
rustKeyType = this.toRustType(scalarType);
So the same model produces:
| Site |
Generated type |
| Map alias |
HashMap<SSN, Employee> |
Field on Company |
Option<HashMap<String, Employee>> |
Field sites compile (after unwrapping). The type aliases do not. C# emits scalars (visitScalarDeclaration); Rust does not.
Generated contrast
// type alias - broken
pub type EmployeeDirectory = HashMap<SSN, Employee>;
// field - scalar unwrapped to String
pub employee_directory: Option<HashMap<String, Employee>>,
Bug 3: Recursive relationship types without indirection
Cause
Employee / Manager have relationships to Manager. visitRelationshipDeclaration emits the named type directly (Option<Manager>), not String/Box<Manager>.
Manager extends Employee therefore contains manager: Option<Manager> - infinite size in Rust.
Expected error
error[E0072]: recursive type `Manager` has infinite size
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
Generated fragment
pub struct Manager {
// ... inherited from Employee ...
pub manager: Option<Manager>, // needs Option<Box<Manager>>
}
Related code
| File |
Role |
lib/codegen/fromcto/rust/rustvisitor.js |
Emits Rust (visitMapDeclaration, scalar no-op, relationships) |
test/codegen/fromcto/rust/rustvisitor.js |
Unit tests with mocks, not full HR fixtures |
test/verification/rust.compile.test.js |
cargo check verification (skips hr_*) |
test/verification/cases.js |
Documents skip: scalar map keys/values not emitted |
Generated Rust from Concerto CTO models is written by
RustVisitor(lib/codegen/fromcto/rust/rustvisitor.js). Snapshot/unit tests do not compile the output;test/verification/rust.compile.test.jscan runcargo check, buthr_base/hr_integrationare skipped because they fail.Related: bug: GraphQL verification bugs (generated output fails graphql-js) #248
Unlike Proto3,
Option<HashMap<...>>is valid Rust - so the Proto “optional map” bug does not apply. The HR fixtures fail for different reasons.Bug 1: Custom scalars referenced in map aliases, but never emitted
Cause
hr_base.cto/hr.ctodefine scalars (SSN,Time,KinName,KinTelephone) and use them as map keys/values:RustVisitor.visit()no-ops scalar declarations:visitMapDeclarationstill emits the scalar name as a Rust type:So the crate names types that were never defined.
Reproduce
From the repo root (after
npm ci, with Rust toolchain installed). Written for PowerShell:Expected errors (among others)
Generated fragments
Minimal repro:
→
cannot find type 'SSN' in this scopeBug 2: Inconsistent scalar handling (aliases vs field sites)
Cause
When a field is a map type,
visitFieldunwraps scalars to the underlying primitive:So the same model produces:
HashMap<SSN, Employee>CompanyOption<HashMap<String, Employee>>Field sites compile (after unwrapping). The type aliases do not. C# emits scalars (
visitScalarDeclaration); Rust does not.Generated contrast
Bug 3: Recursive relationship types without indirection
Cause
Employee/Managerhave relationships toManager.visitRelationshipDeclarationemits the named type directly (Option<Manager>), notString/Box<Manager>.Manager extends Employeetherefore containsmanager: Option<Manager>- infinite size in Rust.Expected error
Generated fragment
Related code
lib/codegen/fromcto/rust/rustvisitor.jsvisitMapDeclaration, scalar no-op, relationships)test/codegen/fromcto/rust/rustvisitor.jstest/verification/rust.compile.test.jscargo checkverification (skipshr_*)test/verification/cases.js