Grammar
Level:
nelumbo.lang. Available afterimport nelumbo.lang, or transitively throughnelumbo.logicor any package.
This page describes the grammar of Nelumbo itself — the syntax you write in a .nl file. It is the foundation the rest of the reference refers back to.
Nelumbo is a meta-language, and the meta-ness goes all the way down. Even the grammar described on this page is itself defined in .nl files:
- The pattern meta-grammar (
<T>,<(>...<)+>,<(>...<)?>,<,>,<|>, …), the top-level declaration forms (import,T :: S,T ::= P,T v,::>, scope blocks), and the underlying object hierarchy (Object,Type,Variable,Root, …) are all declared inlang.nl. The Java core contains just enough hand-coded parsing to loadlang.nl; from that point on, the user's own::=patterns are what parse every subsequent file. - The three execution-driving statement forms (
fact,<=>,?), along with theBooleantype and every logical operator, are declared inlogic.nl.
So "the grammar of Nelumbo" is really the set of patterns that lang.nl and logic.nl install when they are loaded. This page documents the nelumbo.lang part: the declarations that shape syntax. The statement forms that nelumbo.logic adds are on Statements.
A .nl file
A Nelumbo file is a sequence of declarations and statements, read top to bottom. Order matters: a name must be declared before it is used.
The top-level constructs are:
| Form | Meaning | Defined in |
|---|---|---|
import M |
Import module M |
|
T :: S |
Declare type T as a subtype of S |
Types below |
T ::= P |
Declare pattern P producing a value of type T |
Patterns below |
pattern N ::= P |
Declare a reusable named pattern N |
Named patterns below |
T v |
Declare logical variable v of type T |
Variables below |
{ ... } |
Scope block — declarations inside are local | visibility.md |
L ::> { ... } |
Pattern transformation | language-transformations.md |
Importing nelumbo.logic adds the statement forms that drive execution: rules (L <=> R, see writing-rules.md), facts (fact E), and queries and tests (E ?, E ? [F][N]). They are documented on Statements.
Comments are // to end of line. Whitespace is not significant.
Type declarations
T :: S
T :: S1, S2
Declares T as a type. Everything to the right of :: is the list of supertypes. Object is the root of the type hierarchy and never needs to be declared.
Examples from the codebase:
Person :: Object
Male :: Person
Female :: Person
LLM :: Smart
Person :: Smart, Living
Multiple inheritance is supported: Person :: Smart, Living says a Person is both a Smart and a Living.
A bare type can also be introduced for use as a generic parameter:
Type E
See generic parenthesisation in nelumbo.lang and the nelumbo.collections types for details.
Pattern declarations
T ::= pattern
T ::= pattern1, pattern2, pattern3
The ::= operator adds a new way to produce a value of type T to the language's syntax. The left-hand side is the type being produced; the right-hand side is one or more patterns, separated by commas.
A pattern is a mix of:
- Literal tokens — identifiers, symbols, keywords, quoted operators — that must appear exactly
- Holes of the form
<T>— placeholders filled by an expression of typeT - Built-in token holes like
<NUMBER>,<STRING>,<NAME>— seebuilt-in-tokens.md - Variable holes
<Variable>— used by binding forms like quantifiers - Unique holes
<one T>— at most one value for each combination of the other holes; the keyword comes aftervisible/hiddenand before the type (<hidden one T#5>). Meaningful inBooleanpatterns:=is declared<one Object> = <one Object>, which gives functions a single, closed result (see Open facts andone) - Repetition and grouping markers
<(>,<)>,<)*>,<)+>,<)?>,<,>,<|>— see below - An optional precedence annotation
#N— seeprecedence-and-associativity.md - An optional native binding
@fully.qualified.JavaClass— seenative-api.md
Examples from the standard library:
Integer ::= NUMBER @org.modelingvalue.nelumbo.integers.NInteger
Integer ::= Integer + Integer #40
Integer ::= Integer - Integer #40
Integer ::= - Integer #80
Integer ::= | Integer | #35
Boolean ::= Integer "<" Integer #30
Note:
"<"is an ordinary literal. Operator-like characters that would otherwise confuse the parser are written quoted.- A pattern may begin with a hole (
<Integer> - <Integer>), a literal (- <Integer>), or a mix. - The same type may be declared with many
::=patterns; each adds another alternative. They do not conflict with each other as long as they parse unambiguously.
Literal enumerations
A pattern may list several literals as alternatives:
Male ::= Hendrik, Bernhard, Claus, Willem
Female ::= Wilhelmina, Juliana, Beatrix, Maxima, Amalia
Lidwoord ::= de, het
Each listed name becomes a constant of the declared type. They are literals, not variables. The list does not close the type: other declarations may add constants, and Nelumbo never enumerates a type's members to bind a variable (see The open world).
Repetition and grouping
Inside a pattern, angle-bracketed operators build repeating or optional sub-structures:
| Marker | Meaning |
|---|---|
<(> ... <)> |
Group — pure grouping, no repetition |
<(> ... <)?> |
Optional group — zero or one occurrence |
<(> ... <)*> |
Zero-or-more with separator |
<(> ... <)+> |
One-or-more with separator |
<,> |
Separator inside a repetition |
<|> |
Alternation marker inside a group |
<[> ... <]> |
Connected-token group — adjacent tokens, no whitespace allowed between them |
Examples:
Repetition ::= { <(> <Integer> <,> , <)*> } // {3,5,7} or {}
Option ::= <(> super <)?> fast // "fast" or "super fast"
Alternation ::= <(> A <|> B <|> C <)> // "A", "B", or "C"
collections.nl uses this for generic sets and lists:
Set<E> ::= { <(> <(> <E> <|> .. <)> <,> , <)*> } // {1,2,3}, {}, open {1,2,..}
List<E> ::= [ <(> <(> <E> <|> .. <)> <,> , <)*> ]
Connected-token groups: <[> ... <]>
By default whitespace between tokens is insignificant, so a pattern like <NUMBER> - <NUMBER> - <NUMBER> accepts 2025-01-01 and 2025 - 01 - 01. The <[> … <]> form switches that off for the tokens it encloses: every token between <[> and <]> must be directly adjacent in the source — no whitespace, no comment, nothing — to the next.
The brackets themselves are pure meta-syntax: like the other <(>-style markers, they do not appear in the parsed source.
Date ::= [ NUMBER - NUMBER - NUMBER ] @nelumbo.datetime.NDate
Rational ::= ( - )? [ NUMBER . NUMBER ] @nelumbo.rationals.Rational
Integer ::= ( - )? <[> NUMBER <(> "#" <RADIX_NUMBER> <)?> <]> @nelumbo.integers.NInteger
These accept 2025-01-01, 3.14, and 16#FF, but reject 2025 - 01 - 01, 3 . 14, and 16 # FF. This is what distinguishes a literal form (a date, a decimal, a radix-prefixed integer) from a structural expression that happens to use the same operators (<Date> - <Period>, <Integer> - <Integer>).
A practical rule of thumb: use <[> … <]> whenever the absence of whitespace is the only thing telling the parser "this is one literal, not an expression."
(<RADIX_NUMBER> in the Integer form above is a named pattern — see below.)
Named patterns
A recurring fragment of pattern syntax can be given a name and reused, instead of being spelled out at every site. Declare one with the pattern keyword:
pattern N ::= P
The name N is then written <N> inside any later pattern, exactly where its body P would otherwise appear. A named pattern is pure abbreviation: it declares no type and produces no value — <N> expands to P before parsing. This is unlike a T ::= P declaration, which adds a new way to build a value of type T.
The standard library uses named patterns to keep dense declarations legible:
pattern RADIX_NUMBER ::= ( ( NUMBER | NAME ) )+ // integers.nl — the base-N digit run
Integer ::= ( - )? [ NUMBER ( "#" RADIX_NUMBER )? ] @nelumbo.integers.NInteger
pattern YMWD_PERIOD ::= ( NUMBER ( Y | M | W | D ) )+ // datetime.nl — the Y/M/W/D part
pattern TIME_PERIOD ::= T ( NUMBER ( H | M | S ) )+ // — the T-introduced time part
Period ::= [ P ( YMWD_PERIOD ( TIME_PERIOD )? | TIME_PERIOD ) ] @nelumbo.datetime.NPeriod
Even lang.nl and logic.nl define their own (PATTERNS, QNAME, BINDING) — see lang.md. A named pattern must be declared before it is referenced, like any other name.
Variable declarations
T v
T v1, v2, v3
Declares logical variables of type T. Variables declared at the top level are global to the file (or scope); variables declared inside E[...] or A[...] are local to the quantifier.
Integer n, f // fibonacci.nl
Person a, b, c // family.nl
Male y
Female x
A logical variable is a placeholder for whatever value satisfies the surrounding rule or query. It is not a storage cell and does not have a single value.
Expressions
An expression is anything built from:
- Literals —
5,3.14,"hello",true,Hendrik - Variables —
n,a,p - Pattern applications — anything built from a
::=pattern, such asfib(n),a+b,|n|,{1,2,3},het inkomen van Piet is 50000 euro - Logical operators from
nelumbo.logic—&,|,!,->,<->,E[],A[],=,!=
Expressions have types (propagated through pattern declarations) and can appear anywhere the grammar expects an expression of the matching type.
Scope blocks
Curly braces { ... } introduce a lexical scope:
{
Aa :: Object
Aa ::= XXX
Aa x
x=XXX ? [(x=XXX)][..]
}
Names declared inside a scope are not visible outside it. Imports, types, patterns, variables, and rules can all be scoped. See visibility.md for the full rules, including private and hidden.
Pattern transformations
The ::> operator defines a language pattern transformation — a macro-like expansion from one pattern into a block of declarations and rules.
attr OT AN AT ::> {
AT ::= <OT>.AN
Root ::= <OT>.AN := <AT>
private Boolean ::= AN(<OT>,<AT>)
...
}
This is Nelumbo's mechanism for building higher-level DSLs on top of the core. It is covered in language-transformations.md rather than here, because transformations are an advanced feature that deserves a full guide.
Punctuation
| Symbol | Meaning |
|---|---|
, in a supertype list |
Separates supertypes |
, in a variable declaration |
Separates variable names |
{ } |
Scope block — see visibility.md |
( ) |
Grouping inside an expression |
// |
Line comment |
Special identifiers
| Name | Meaning |
|---|---|
Object, Type, Variable, Root, Functor, Pattern, Namespace, RootNamespace |
The core object hierarchy. Root is the entry-point production for top-level statements; Type is used as a generic parameter introducer (Type T). |
The logic-layer identifiers (Boolean, Function, true, ...) are listed in operators.md.
See also
operators.md— the operators fromnelumbo.logicstatements.md— facts, queries and testsbuilt-in-tokens.md—<NUMBER>,<STRING>,<NAME>,<Variable>precedence-and-associativity.md— how#Nworksvisibility.md—private,hidden,visible, and{ }scopes