This document describes the currently supported NDEF language and C++ backend-map contract. It records existing behavior; incompatible extensions require an explicit design decision.
.ndef suffix..ndef file contains exactly one module.backend_cpp.map in the same directory as the .ndef file..ndef basename and are written beside it.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 allwith value transient fixed
*, ?, :, |, 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.
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, ":",
[ "fixed" ], [ "*" | "?" ], [ "value" ], [ "transient" ],
name, newline ;
choice = "choice", name, [ all-trait-list ], newline,
option-list, newline, "end", newline ;
all-trait-list = "allwith", name, { ",", name } ;
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.
module expressions
The module name is emitted directly as the C++ namespace. It must therefore also be a valid, non-keyword C++ identifier.
node BinaryExpression
op: BinaryOp
left: Expr
right: Expr
end
A node generates a snake_case C++ struct. Fields resolve as follows:
std::unique_ptr<T>.value node or choice reference embeds T directly.Modifiers have the fixed order shown below:
field: [fixed] [* | ?] [value] [transient] Type
* generates std::vector<T> around the otherwise generated representation.fixed marks a repeated field whose element count must be preserved by generated
transformers. It is valid only together with * and currently uses the same C++ storage as
the corresponding non-fixed repeated field.? generates std::optional<T> for backend-mapped, enum, and value types. A pointer-backed
node or choice remains std::unique_ptr<T>, which already represents absence.value embeds node and choice types instead of using std::unique_ptr.transient emits the C++ member but excludes it from dump and visitor structure.* and ? are mutually exclusive.Examples:
names: *identifier
arguments: fixed *Expr
metadata: ?value Metadata
function_scope: transient scope
choice Expr allwith Location, Attributes
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.
An allwith list applies its traits to every node reachable through the choice alternatives.
Propagation is transitive through nested choices and changes the generated node types globally,
including uses of those nodes outside the annotated choice. Explicit node traits are ordered
before inferred choice traits; inferred traits follow choice declaration order and their order
within each allwith list. Repeated applications, including overlap with an explicit with,
are deduplicated at the first occurrence.
When a module contains an allwith choice, the generated model header also provides
as_trait<Trait>(choice). It recursively unwraps nested choices and returns a mutable or const
reference to the requested trait, matching the choice argument:
expr& value = /* ... */;
as_trait<location>(value).location = source_range{};
const expr& const_value = value;
const location& where = as_trait<location>(const_value);
The accessor assumes that the requested trait is present on every reachable node. Requesting a trait that is not common to the choice produces a C++ template compilation error.
When a choice is used by at least one repeated, pointer-backed field such as code: *Expr, the
C++ backend also generates one reusable list alias:
using expr_list = std::vector<std::unique_ptr<expr>>;
Repeated pointer-backed choice fields use this alias in generated structs.
Repeated value choice fields do not generate this alias because their representation is
std::vector<expr> instead.
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.
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,
and choices may propagate them with allwith. Traits cannot inherit, appear as choice
alternatives, or receive visitor overloads. Trait fields are flattened into derived-node dump
output. Repeated applications are deduplicated; 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.
Before generation, semantic analysis rejects invalid input. Validation and generation diagnostics are errors-only: a failure stops the workflow, and accepted input does not produce warnings.
Semantic analysis rejects:
_t collisions.visitor and transformer.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.
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.
Each module generates a mutable visitor class:
visit(T&) overloads provide node and choice entry points.std::visit.enter(T&), traverse children, then call virtual leave(T&).value, non-transient node and choice fields are traversed.replace_<choice>(...) helpers accepting
either one std::unique_ptr<choice> or a choice-list replacement. The base visitor records
explicit replacement state, consumes it immediately after the child visit, and then clears it.value or transient contexts are omitted, while
unreferenced definitions remain available as roots.Each module generates a mutable bottom-up transformer class:
rewrite(std::unique_ptr<T>) overloads provide node and choice entry points.std::visit and wrap concrete node replacements back into
the choice variant.visit_multiple(T&) and return
std::vector<std::unique_ptr<T>>; repeated choices use the generated <choice>_list alias.visit_single(T&) and return std::unique_ptr<T>.Number alternative of
Expr returns std::unique_ptr<expr> or expr_list, allowing replacements with any
expression alternative.value, non-transient node and choice fields are rewritten. Trait fields are not
traversed, matching the visitor implementation.nullptr; null multiple rewrites return an empty list.nullptr, one
replacement is moved back into the field, and more than one replacement trips an assertion.Generated dump functions produce a YAML-like structural representation:
_type containing its original NDEF name.[].null.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.
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:
@include accepts exactly <...> or "..." spelling.name: C++ type spelling.# or the newline.#, unlike NDEF // comments.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.
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.