///|
/// Output dialect for generated DBC integration artifacts.
pub enum DbcCodegenTarget {
DbcCodegenMoonBit
DbcCodegenCHeader
DbcCodegenJsonSchema
DbcCodegenMarkdown
}
///|
pub fn dbc_codegen_target_variants() -> Array[DbcCodegenTarget] {
[
DbcCodegenMoonBit,
DbcCodegenCHeader,
DbcCodegenJsonSchema,
DbcCodegenMarkdown,
]
}
///|
/// Options used by a deterministic DBC artifact generator.
pub struct DbcCodegenOptions {
target : DbcCodegenTarget
module_name : String
include_comments : Bool
include_validation : Bool
line_ending : String
}
///|
pub fn dbc_codegen_options(
target? : DbcCodegenTarget = DbcCodegenMoonBit,
module_name? : String = "can_schema",
include_comments? : Bool = true,
include_validation? : Bool = true,
line_ending? : String = "\n",
) -> DbcCodegenOptions {
{ target, module_name, include_comments, include_validation, line_ending }
}
///|
pub fn DbcCodegenOptions::target(self : DbcCodegenOptions) -> DbcCodegenTarget {
self.target
}
///|
pub fn DbcCodegenOptions::module_name(self : DbcCodegenOptions) -> String {
self.module_name
}
///|
pub fn DbcCodegenOptions::include_comments(self : DbcCodegenOptions) -> Bool {
self.include_comments
}
///|
pub fn DbcCodegenOptions::include_validation(self : DbcCodegenOptions) -> Bool {
self.include_validation
}
///|
pub fn DbcCodegenOptions::line_ending(self : DbcCodegenOptions) -> String {
self.line_ending
}
///|
/// A generated artifact with reproducible metadata.
pub struct DbcGeneratedArtifact {
path : String
target : DbcCodegenTarget
content : String
line_count : Int
message_count : Int
signal_count : Int
warnings : Array[String]
}
///|
pub fn DbcGeneratedArtifact::path(self : DbcGeneratedArtifact) -> String {
self.path
}
///|
pub fn DbcGeneratedArtifact::target(
self : DbcGeneratedArtifact,
) -> DbcCodegenTarget {
self.target
}
///|
pub fn DbcGeneratedArtifact::content(self : DbcGeneratedArtifact) -> String {
self.content
}
///|
pub fn DbcGeneratedArtifact::line_count(self : DbcGeneratedArtifact) -> Int {
self.line_count
}
///|
pub fn DbcGeneratedArtifact::message_count(self : DbcGeneratedArtifact) -> Int {
self.message_count
}
///|
pub fn DbcGeneratedArtifact::signal_count(self : DbcGeneratedArtifact) -> Int {
self.signal_count
}
///|
pub fn DbcGeneratedArtifact::warnings(
self : DbcGeneratedArtifact,
) -> Array[String] {
self.warnings.copy()
}
///|
/// Render a safe identifier for generated code.
pub fn dbc_codegen_identifier(name : String) -> String {
let builder = StringBuilder()
for character in name {
if (character >= 'a' && character <= 'z') ||
(character >= 'A' && character <= 'Z') ||
character == '_' ||
(character >= '0' && character <= '9') {
builder.write_char(character)
} else {
builder.write_char('_')
}
}
let result = builder.to_string()
if result.is_empty() {
"unnamed"
} else {
result
}
}
///|
pub fn dbc_codegen_target_name(target : DbcCodegenTarget) -> String {
match target {
DbcCodegenMoonBit => "moonbit"
DbcCodegenCHeader => "c-header"
DbcCodegenJsonSchema => "json-schema"
DbcCodegenMarkdown => "markdown"
}
}
///|
/// Generate an artifact for a collection of DBC messages.
pub fn dbc_codegen(
messages : Array[Message],
options : DbcCodegenOptions,
) -> DbcGeneratedArtifact {
let lines : Array[String] = []
let warnings : Array[String] = []
if options.include_comments() {
lines.push("generated by moonbit-canbus")
lines.push("module=" + dbc_codegen_identifier(options.module_name()))
}
let mut signal_count = 0
for item in messages {
if !item.validate() {
warnings.push("invalid message " + item.name())
}
lines.append(dbc_codegen_message(item, options))
signal_count += item.signal_count()
}
if options.include_validation() {
if messages.is_empty() {
warnings.push("schema contains no messages")
}
for item in messages {
if item.dlc() > 8 &&
item.dlc() != 12 &&
item.dlc() != 16 &&
item.dlc() != 20 &&
item.dlc() != 24 &&
item.dlc() != 32 &&
item.dlc() != 48 &&
item.dlc() != 64 {
warnings.push("non-canonical CAN-FD length in " + item.name())
}
}
}
let content = lines.join(options.line_ending())
{
path: options.module_name() +
"." +
dbc_codegen_target_name(options.target()),
target: options.target(),
content,
line_count: lines.length(),
message_count: messages.length(),
signal_count,
warnings,
}
}
///|
fn dbc_codegen_message(
item : Message,
options : DbcCodegenOptions,
) -> Array[String] {
let lines : Array[String] = []
let identifier = dbc_codegen_identifier(item.name())
match options.target() {
DbcCodegenMoonBit => {
lines.push(
"const " + identifier + "_id : UInt = " + item.id().to_string(),
)
lines.push(
"const " + identifier + "_dlc : Int = " + item.dlc().to_string(),
)
for signal in item.signals() {
lines.push(
"const " +
identifier +
"_" +
dbc_codegen_identifier(signal.name()) +
"_start : Int = " +
signal.start_bit().to_string(),
)
lines.push(
"const " +
identifier +
"_" +
dbc_codegen_identifier(signal.name()) +
"_size : Int = " +
signal.size().to_string(),
)
}
}
DbcCodegenCHeader => {
lines.push("#define " + identifier + "_ID " + item.id().to_string() + "u")
lines.push("#define " + identifier + "_DLC " + item.dlc().to_string())
for signal in item.signals() {
let name = identifier + "_" + dbc_codegen_identifier(signal.name())
lines.push(
"#define " + name + "_START " + signal.start_bit().to_string(),
)
lines.push("#define " + name + "_LENGTH " + signal.size().to_string())
}
}
DbcCodegenJsonSchema => {
lines.push("message " + item.id().to_string() + " " + item.name() + " {")
lines.push(" dlc: " + item.dlc().to_string() + ",")
for signal in item.signals() {
lines.push(
" signal " +
signal.name() +
" { start: " +
signal.start_bit().to_string() +
", size: " +
signal.size().to_string() +
" },",
)
}
lines.push("}")
}
DbcCodegenMarkdown => {
lines.push("### " + item.name())
lines.push(
"Identifier: `0x" +
item.id().to_string() +
"`; DLC: `" +
item.dlc().to_string() +
"`",
)
lines.push("| Signal | Start | Size | Factor | Offset | Unit |")
lines.push("| --- | ---: | ---: | ---: | ---: | --- |")
for signal in item.signals() {
lines.push(
"| " +
signal.name() +
" | " +
signal.start_bit().to_string() +
" | " +
signal.size().to_string() +
" | " +
signal.factor().to_string() +
" | " +
signal.offset().to_string() +
" | " +
signal.unit() +
" |",
)
}
}
}
lines
}
///|
/// Lint a DBC schema for issues that affect generated integration code.
pub fn dbc_codegen_lint(messages : Array[Message]) -> Array[String] {
let issues : Array[String] = []
for index, left in messages {
if !left.validate() {
issues.push("invalid message: " + left.name())
}
if left.dlc() == 0 {
issues.push("zero DLC message: " + left.name())
}
for right in messages[index + 1:] {
if left.id() == right.id() {
issues.push("duplicate identifier: " + left.id().to_string())
}
if left.name() == right.name() {
issues.push("duplicate message name: " + left.name())
}
}
for signal in left.signals() {
if signal.size() <= 0 {
issues.push("empty signal: " + signal.name())
}
if signal.factor() == 0.0 {
issues.push("zero factor: " + signal.name())
}
}
}
issues
}
///|
/// Produce one artifact per supported target.
pub fn dbc_codegen_all_targets(
messages : Array[Message],
module_name : String,
) -> Array[DbcGeneratedArtifact] {
let result : Array[DbcGeneratedArtifact] = []
for target in dbc_codegen_target_variants() {
result.push(
dbc_codegen(messages, dbc_codegen_options(target~, module_name~)),
)
}
result
}
///|
/// Return the total generated source size across artifacts.
pub fn dbc_codegen_total_lines(artifacts : Array[DbcGeneratedArtifact]) -> Int {
let mut total = 0
for artifact in artifacts {
total += artifact.line_count()
}
total
}
///|
/// Return a deterministic schema report suitable for CI.
pub fn dbc_codegen_report(messages : Array[Message]) -> String {
let artifacts = dbc_codegen_all_targets(messages, "can_schema")
let issues = dbc_codegen_lint(messages)
"messages=" +
messages.length().to_string() +
" issues=" +
issues.length().to_string() +
" generated_lines=" +
dbc_codegen_total_lines(artifacts).to_string()
}
///|
/// Generate a DBC workspace artifact without mutating its schema.
pub fn dbc_codegen_workspace(
workspace : DbcWorkspace,
options : DbcCodegenOptions,
) -> DbcGeneratedArtifact {
dbc_codegen(workspace.messages(), options)
}