Grammar

Level: nelumbo.lang. Available after import nelumbo.lang, or transitively through nelumbo.logic or 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 in lang.nl. The Java core contains just enough hand-coded parsing to load lang.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 the Boolean type and every logical operator, are declared in logic.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 type T
  • Built-in token holes like <NUMBER>, <STRING>, <NAME> — see built-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 after visible/hidden and before the type (<hidden one T#5>). Meaningful in Boolean patterns: = is declared <one Object> = <one Object>, which gives functions a single, closed result (see Open facts and one)
  • Repetition and grouping markers <(>, <)>, <)*>, <)+>, <)?>, <,>, <|> — see below
  • An optional precedence annotation #N — see precedence-and-associativity.md
  • An optional native binding @fully.qualified.JavaClass — see native-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 as fib(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