NDEF language reference
This document describes the currently supported NDEF language and C++ backend-map contract. It records existing behavior; incompatible extensions require an explicit design decision.
Files and lookup
- A node-definition file uses the
.ndefsuffix. - One
.ndeffile contains exactly one module. - C++ generation requires
backend_cpp.mapin the same directory as the.ndeffile. - Backend selection and the backend-map filename are currently fixed.
- Generated files use the
.ndefbasename and are written beside it.
Lexical rules
Names use ASCII letters, digits, and underscores and cannot begin with a digit:
[A-Za-z_][A-Za-z0-9_]*
NDEF comments begin with // and continue through the end of the line. Multiline comments do
not exist. Horizontal whitespace is insignificant. Blank lines are supported around
declarations; blank lines inside declaration bodies are currently not supported. Comment-only
lines are allowed inside bodies.
The language keywords are:
module node trait choice enum end with value transient
*, ?, :, |, and , are syntax operators or separators. Keywords are contextual in
some name positions—value, for example, remains a legal field name—but keyword names should
not be used as referenced types where they would be parsed as modifiers.
Grammar overview
The following EBNF is descriptive. newline is significant because declarations and fields
are line-oriented.
file = { newline }, module, newline, { definition } ;
module = "module", name ;
definition = node | trait | choice | enum ;
node = "node", name, [ trait-list ], newline,
{ field }, "end", newline ;
trait-list = "with", name, { ",", name } ;
trait = "trait", name, newline,
{ field }, "end", newline ;
field = name, ":",
[ "*" | "?" ], [ "value" ], [ "transient" ],
name, newline ;
choice = "choice", name, newline,
option-list, newline, "end", newline ;
enum = "enum", name, newline,
option-list, newline, "end", newline ;
option-list = name, { "|", name } ;
Within an option list, a newline may appear directly before or after |. Inline, leading-pipe,
trailing-pipe, and mixed layouts are accepted. A trailing | without another option is invalid.
Modules
module expressions
The module name is emitted directly as the C++ namespace. It must therefore also be a valid, non-keyword C++ identifier.
Nodes and fields
node BinaryExpression
op: BinaryOp
left: Expr
right: Expr
end
A node generates a snake_case C++ struct. Fields resolve as follows:
- A backend-mapped type uses its configured C++ spelling.
- An enum is stored as its generated enum type.
- A node or choice reference normally uses
std::unique_ptr<T>. - A
valuenode or choice reference embedsTdirectly.
Field modifiers
Modifiers have the fixed order shown below:
field: [* | ?] [value] [transient] Type
*generatesstd::vector<T>around the otherwise generated representation.?generatesstd::optional<T>for backend-mapped, enum, andvaluetypes. A pointer-backed node or choice remainsstd::unique_ptr<T>, which already represents absence.valueembeds node and choice types instead of usingstd::unique_ptr.transientemits the C++ member but excludes it from dump and visitor structure.*and?are mutually exclusive.
Examples:
names: *identifier
metadata: ?value Metadata
function_scope: transient scope
Choices
choice Expr
Number | BinaryExpression
| FunctionCall
end
A choice generates a snake_case std::variant alias. Alternatives must resolve to nodes or
other choices. Choice aliases are dependency-ordered; cyclic choice aliases are invalid.
Enums
enum BinaryOp
Plus | Minus | Multiply
end
An enum generates enum class <snake_case_name>_t. Enumerator spelling is preserved exactly.
Enum option lists use the same flexible layout as choices.
Traits
trait Location
location: source_range
end
node FunctionHead with Location
name: identifier
end
A trait generates a public C++ base struct. Nodes may apply one or more comma-separated traits. Traits cannot inherit, appear in choices, or receive visitor overloads. Trait fields are flattened into derived-node dump output. Duplicate applications and field collisions across traits and local fields are invalid. Structured node or choice values cannot currently be embedded by value inside traits because generated traits precede complete node definitions.
Semantic validation
Before generation, semantic analysis rejects:
- Duplicate declarations, fields, enum entries, choice alternatives, traits, and mappings.
- Unknown field types, traits, and choice alternatives.
- Traits used as ordinary field types or choice alternatives.
- Invalid modifier combinations.
- Cyclic choice aliases and recursive by-value node dependencies.
- Invalid C++ identifiers, namespaces, and reserved keywords.
- Generated C++ name collisions, including snake_case and enum
_tcollisions. - Collisions with reserved generated names such as
visitor.
A field type that is neither a declared NDEF type nor backend-mapped is reported as unknown. The analyzer cannot distinguish a misspelled declaration from a missing backend mapping.
C++ name conversion and ordering
Definition names are converted from CamelCase to snake_case. Acronym boundaries are supported;
for example, HTTPServer becomes http_server. Field and enum-entry spellings are not changed.
Source order does not determine whether references are valid. Generation emits declarations in an order required by C++ dependencies: enums, forward declarations, traits, dependency-ordered choices, and dependency-ordered nodes. Order within otherwise independent groups remains stable.
Visitor contract
Each module generates a mutable visitor class:
- Public
visit(T&)overloads provide node and choice entry points. - Choice visits dispatch with
std::visit. - Node visits call virtual
enter(T&), traverse children, then call virtualleave(T&). - Only non-
value, non-transientnode and choice fields are traversed. - Null pointers are skipped; repeated pointer fields visit every non-null element.
- Definitions used exclusively through
valueortransientcontexts are omitted, while unreferenced definitions remain available as roots.
Dump contract
Generated dump functions produce a YAML-like structural representation:
- Four spaces are used per nesting level.
- Every node begins with
_typecontaining its original NDEF name. - Field names use their original NDEF spelling.
- Repeated fields are lists; empty lists emit
[]. - Missing optionals and null pointers emit
null. - Strings are quoted and escaped.
- Enums emit original entry names.
- Trait fields are flattened into the node object.
- Transient fields are omitted.
Backend scalar values use the inline dump_value customization point. Streamable values use
operator<<; applications can provide a more-specific overload in the value type's namespace
for argument-dependent lookup and context-sensitive formatting.
Strict YAML compatibility is not yet guaranteed and remains a documented design question.
C++ backend map
backend_cpp.map contains include directives and type mappings:
@include <cstddef>
@include "project/scope.hpp"
index: std::size_t
scope: std::unique_ptr<scope>
Rules:
@includeaccepts exactly<...>or"..."spelling.- Includes are unconditional, retain declaration order, and are deduplicated against generated standard includes.
- A mapping uses
name: C++ type spelling. - C++ type spelling is the trimmed remainder of the line up to
#or the newline. - Backend-map comments begin with
#, unlike NDEF//comments. - Duplicate mapping names are invalid; unused mappings are allowed.
- The generated model header includes configured headers before emitting declarations.
One behavior requires a future explicit decision: if a backend mapping has the same name as a declared NDEF type, current model generation gives the backend mapping precedence for fields. Definitions should avoid this collision until the rule is resolved.
Language versioning
NDEF files do not declare a language version. Version syntax is deliberately deferred because the format currently has limited distribution and no demonstrated compatibility need.