Identifier naming¶
A DSDL name is not an identifier in any target language. break is a keyword in every one of them,
fooBar and foo_bar are one name in the ones that fold case, and _Leading is in the region C
and C++ reserve for their implementations. This describes what happens to a name on its way into generated
code, and what a corpus is rejected for.
The projection is deterministic and depends only on the definition: the same DSDL name yields the same identifier in every invocation that emits that language.
Roles¶
One DSDL name can become several different things, and each one is spelled by its own rule. The rule is chosen by the name's role:
| Role | What it names |
|---|---|
TypeName |
The generated type |
FieldName |
A member of it |
ConstantName |
A generated constant |
FunctionName |
A generated function |
LocalName |
A variable inside a generated body |
NamespaceName |
A namespace, package or module component |
FileStem |
The output file, without its extension |
MacroName |
A preprocessor macro |
A field called value and a constant called value in the same definition do not receive the same
identifier, because ConstantName upper-cases and FieldName does not.
The pipeline¶
Every name passes through the same stages, in order. A stage that does not apply to the role or the language is skipped.
- Case projection. Fold the name to the language's convention for that role:
snake_case,PascalCase, or preserve it as written. - Escape. Replace anything outside
[A-Za-z0-9_], and prefix a leading digit. - Keyword strop. If the result is a keyword, append
_.breakbecomesbreak_. - Upper-case. Constants and macros are upper-cased.
- Claimed names. Generated code declares members of its own — a serialise method, a metadata constant, a union tag. A DSDL name landing on one of those is escaped the same way a keyword is.
- Reserved namespaces. A name that lands in a region the language reserves for its implementation is rejected, or encoded on request.
Stages 1 and 4 are the ones that differ per language; the tables below give them per role.
Uniqueness within a scope¶
Case projection is many-to-one: fooBar, foo_bar and FooBar are three DSDL names and one
snake_case identifier. Keyword stropping is many-to-one too, since break and break_ both give
break_.
Where such names meet in one region of generated code — a struct body, a constant block — the region is a scope, and the scope keeps the map injective by appending an ordinal to the later name. The first declaration in DSDL order keeps the plain spelling:
uint8 break
uint8 fooBar
uint8 foo_bar
uint8 _leading
becomes, in Go:
Break uint8
FooBar uint8
FooBar_2 uint8
Leading uint8
The ordinal is assigned in DSDL declaration order, so it is stable across runs and moves only when the definition does. Each rename is reported:
note: field 'foo_bar' is emitted as 'FooBar_2' for target language 'go';
another name in the same scope already projects to 'FooBar'
A scope covers one region, not a whole file. C++ declares a definition's fields and constants into one struct body, so those share a scope; the languages that put constants where a field cannot reach them give each its own.
Rejected corpora¶
A scope repairs a collision by renaming, which works while the name is internal to a region. Two kinds of name cannot be repaired that way, because the choice of which definition to rename would depend on the order they were discovered in:
- Output file names.
ns.FooBar.1.0andns.Foo_bar.1.0both wantfoo_bar_1_0wherever file stems are snake_cased. One file would overwrite the other. - Type names. Two definitions projecting onto one type name in a language that shares a scope across a namespace.
Both are rejected at discovery, naming the pair and the languages affected:
error: type name collision in generated output: ns.Foo_bar and ns.FooBar map to the
same output file name for target language 'go'
Only the languages the invocation emits are checked, so a build never fails over a hazard in output it was not going to produce. An invocation that emits nothing — analysis, or the language server — checks every language.
A service is checked the same way against its own sections. A service Foo emits Foo_Request, and
a sibling definition may be called Foo_Request; the pair is rejected where the two would meet.
Reserved namespaces¶
C and C++ reserve part of the identifier space for their implementations. A DSDL name landing in it is rejected:
| Name | C | C++ | Rust, Go, TypeScript, Python |
|---|---|---|---|
_leading |
accepted | accepted | accepted |
_Leading — leading _ before a capital |
rejected | rejected | accepted |
__dunder — leading __ |
rejected | rejected | accepted |
foo__bar — interior __ |
accepted | rejected | accepted |
C++ reserves any __ run; C reserves one only at the start of a name.
Renaming the definition is the expected answer, and the diagnostic says so. Where a corpus cannot be renamed, the offending characters can be encoded instead — the diagnostic names the flag and shows the identifier it would produce.
Per-language rules¶
Case and stropping by role¶
preserve keeps the DSDL spelling. escape replaces characters outside [A-Za-z0-9_]. strop
appends _ to a keyword. upper upper-cases the finished identifier.
| Language | Role | Case | Escape | Strop | Upper |
|---|---|---|---|---|---|
| C | TypeName, FieldName, FunctionName, LocalName, NamespaceName | preserve | ✅ | ✅ | |
| C | ConstantName, MacroName | preserve | ✅ | ✅ | |
| C | FileStem | preserve | |||
| C++ | TypeName, FieldName, FunctionName, LocalName, NamespaceName | preserve | ✅ | ✅ | |
| C++ | ConstantName, MacroName | preserve | ✅ | ✅ | |
| C++ | FileStem | preserve | |||
| Rust | TypeName, FieldName, FunctionName, LocalName, NamespaceName | preserve | ✅ | ✅ | |
| Rust | ConstantName, MacroName | snake | ✅ | ✅ | ✅ |
| Rust | FileStem | snake | ✅ | ✅ | |
| Go | TypeName, FieldName, FunctionName, LocalName | pascal | ✅ | ✅ | |
| Go | ConstantName, MacroName | snake | ✅ | ✅ | ✅ |
| Go | NamespaceName, FileStem | snake | ✅ | ✅ | |
| TypeScript | TypeName | pascal | ✅ | ✅ | |
| TypeScript | FieldName, FunctionName, LocalName, NamespaceName, FileStem | snake | ✅ | ✅ | |
| TypeScript | ConstantName, MacroName | snake | ✅ | ✅ | ✅ |
| Python | TypeName | pascal | ✅ | ✅ | |
| Python | FieldName, FunctionName, LocalName, NamespaceName, FileStem | snake | ✅ | ✅ | |
| Python | ConstantName, MacroName | snake | ✅ | ✅ | ✅ |
A C or C++ file stem takes no keyword escape because a file name is not an identifier, and a C or C++ macro takes none because a macro token is not in the language's namespace and always carries its type name as a prefix.
The machine-checked form of this table is
test/unit/golden/naming-roles.txt,
regenerated from the call sites that produce each name. A diff there is an ABI change.
Scope boundaries¶
This decides whether two definitions can collide, and how much a scope can repair.
| Language | A definition's type lives in | Two definitions in one namespace |
|---|---|---|
| C | one global scope, namespace flattened into the identifier | share it |
| C++ | a namespace per DSDL namespace | share it |
| Go | a package per DSDL namespace | share it |
| Rust | a module per definition and version | separate |
| TypeScript | a module per definition and version | separate |
| Python | a module per definition and version | separate |
Where a namespace is one scope, two versions of one type reach one identifier unless the type name carries its version. Rust, TypeScript and Python are unaffected, because each version has a module of its own.
Case folding¶
Go, TypeScript and Python fold case, so fooBar and foo_bar are one identifier there and distinct
identifiers in C, C++ and Rust. The scope repairs it either way: in C, C++ and Rust the keyword and
claimed-name escapes are many-to-one where case folding is not.
Implementation¶
include/llvmdsdl/Support/NamingPolicy.h
holds the role table and the pipeline; NamingScope in the same header holds the per-region
uniqueness.
include/llvmdsdl/Support/DefinitionNaming.h
composes the names built from a definition's identity — its type name, file stem, include guard and
linkage symbol. Discovery rejects the corpora above.
The design record, including why each policy is what it is, is Identifier naming under Development.