silwright

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

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 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.

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, ":",
                [ "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.

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:

Field modifiers

Modifiers have the fixed order shown below:

field: [fixed] [* | ?] [value] [transient] Type

Examples:

names: *identifier
arguments: fixed *Expr
metadata: ?value Metadata
function_scope: transient scope

Choices

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.

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, 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.

Semantic validation

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:

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:

Transformer contract

Each module generates a mutable bottom-up transformer class:

Dump contract

Generated dump functions produce a YAML-like structural representation:

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:

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.