Skip to content

bug: Rust verification - generated output fails cargo check #264

Description

@apoorv7g

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions