Standard library tour
June 24, 2026 · View on GitHub
The Nelumbo standard library is around 300 lines of Nelumbo across seven files. That is remarkably small — and because it is written in Nelumbo, reading it is one of the best ways to learn how the language is actually used.
This guide walks through all seven modules in dependency order, showing how each builds on the previous ones, what is native and what is derived, and what idiomatic Nelumbo looks like in production use.
The files:
lang.nl— 51 lines — syntactic bootstrap: tokens, object hierarchy, the pattern meta-grammar, top-level formslogic.nl— 44 lines — Boolean, connectives, quantifiers, equality, and thefact/<=>/?statement formsintegers.nl— 36 lines — arithmetic and comparisonrationals.nl— 46 lines — exact rational arithmeticstrings.nl— 24 lines — string operationscollections.nl— 21 lines — genericSet<E>andList<E>, plus set-builder notationdatetime.nl— 96 lines — ISO 8601 dates, times, date-times, and durations
Each module is small enough to read in full, and the commentary around them illuminates the idioms they establish.
1. nelumbo.lang (51 lines)
The syntactic bootstrap. Every other .nl file — including logic.nl — is parsed using the ::= declarations in this file. The Java core knows just enough to load lang.nl; from there on, the same machinery the user writes parses everything.
import nelumbo.lang // not literally — this is itself nelumbo.lang
// Token types — produced by the tokenizer
SINGLEQUOTE :: NATIVE SEMICOLON :: NATIVE COMMA :: NATIVE
LEFT :: NATIVE RIGHT :: NATIVE STRING :: NATIVE
NUMBER :: NATIVE NAME :: NATIVE OPERATOR :: NATIVE
NEWLINE :: NATIVE
BEGINOFFILE :: NATIVE ENDOFFILE :: NATIVE
// Object hierarchy
Object :: NATIVE
Type :: Object
Variable :: Object
Root :: Object // a top-level statement
Functor :: Root // a `::=`-declared pattern
Pattern :: Object #PATTERN // a pattern fragment
Namespace :: Object // a `{ ... }` scope
RootNamespace :: Root, Namespace
// File / scope grammar
Namespace ::= <BEGINOFFILE> ... <ENDOFFILE> @...Namespace
RootNamespace ::= { ... } @...Namespace
// Pattern meta-grammar — declares the syntax of `::=` patterns themselves
Pattern ::= <NAME> @nelumbo.patterns.TokenTextPattern,
<STRING> @nelumbo.patterns.TokenTextPattern,
...
<LEFT> ... <RIGHT> @nelumbo.patterns.SequencePattern,
"<" (visible|hidden)? <Type#100> (#NUMBER)? ">" @nelumbo.patterns.NodeTypePattern
// Top-level statement forms
Root ::= "import" ... @nelumbo.lang.Import,
<Root#0> ::> <RootNamespace> @nelumbo.lang.Transform,
(hidden)? <Type#100> <NAME>, ... @nelumbo.lang.Variable,
<NAME> (< <Type#100> >)? :: <Type#100>, ... (# <NAME>)? @nelumbo.lang.Type,
(private)? <Type#100> ::= <Pattern#100>+ (#NUMBER)? (@...)? @nelumbo.lang.Functor
// Generic parenthesisation — one rule, applies to every type
Type P
P ::= (<P>) @nelumbo.lang.Parenthesized
What is unique about this file
- No
<=>rules, nofact, no?. All those forms are declared inlogic.nland are not yet available whenlang.nlis being loaded.lang.nlcontains only::,::=, and::>declarations. - Self-bootstrap. The declarations describe the very syntax used to write them. The Java parser used to first read
lang.nlis a minimal hand-coded equivalent of these rules; once the file is loaded, the patterns it installed take over. - Every other stdlib module starts with
import nelumbo.langtransitively —logic.nlimports it directly; everything else gets it vialogic.
Idioms to notice
- The
Patternblock is the densest part of the file: it uses quoted operator characters ("<","(","|",",",")", …) to talk about the very<...>syntax those characters have meaning in. This is the meta-syntax describing itself. - The
Type P/P ::= (<P>)pair at the end is the canonical demonstration of generics.collections.nluses the same mechanism forSet<E>andList<E>.
See ../reference/stdlib/lang.md for the full annotated walk-through.
2. nelumbo.logic (44 lines)
The three-valued logic layer. This is where Boolean, the connectives, and — crucially — the fact, <=>, and ? statement forms are declared.
import nelumbo.lang
Boolean :: Object
FactType :: Boolean
Function :: Object
Literal :: Object
private Boolean ::= eq(<Literal>,<Literal>) @nelumbo.logic.Equal
Boolean ::= true @nelumbo.logic.NBoolean,
false @nelumbo.logic.NBoolean,
unknown @nelumbo.logic.NBoolean,
! <Boolean> #25 @nelumbo.logic.Not,
<Boolean> & <Boolean> #22 @nelumbo.logic.And,
<Boolean> | <Boolean> #20 @nelumbo.logic.Or,
E[<(> <Variable#100> <,> , <)+>](<Boolean#0>) @nelumbo.logic.ExistentialQuantifier,
A[<(> <Variable#100> <,> , <)+>](<Boolean#0>) @nelumbo.logic.UniversalQuantifier,
<Object> = <Object> #30 @nelumbo.logic.NIs,
<Object> != <Object> #30,
<Boolean> -> <Boolean> #18,
<Boolean> "<->" <Boolean> #16
pattern BINDING ::= [ ... ]
// Top-level statement forms — declared here, not in the Java core
Root ::= "fact" <Boolean#0>, ... @nelumbo.logic.Fact,
<Boolean#0> "<=>" (<Boolean#0> ("if" <Boolean#0>)?), ... @nelumbo.logic.Rule,
<Boolean#0> ? (<BINDING> <BINDING>)? @nelumbo.logic.Query
Boolean p1, p2
p1 -> p2 <=> !p1 | p2
p1 <-> p2 <=> (p1 -> p2) & (p2 -> p1)
Literal l1, l2
Function f1, f2
Object n1, n2
l1 = l2 <=> eq(l1, l2)
l1 = f1 <=> f1 = l1
n1 != n2 <=> !(n1 = n2)
What is native here
Nine native bindings: Equal (for the private literal-equality eq), NBoolean, Not, And, Or, ExistentialQuantifier, UniversalQuantifier, NIs (the public <Object> = <Object> operator), and three statement-form natives Fact, Rule, Query. Everything else is derived in Nelumbo.
What is derived
->(implication) is defined as!p | q. No Java code involved.<->(bi-implication) is defined as(p -> q) & (q -> p).!=(inequality) is defined as!(n1 = n2).=for mixed literal/function is defined as a rewrite:l1 = f1 <=> f1 = l1, swapping sides so the function is on the left. This is what makes5 = fib(n)work the same way asfib(n) = 5.
This is the module's first big lesson: even at the deepest level of the language, most derivations happen in Nelumbo, not Java. The Java surface is kept small.
The key statement forms
fact, <=>, and ? are themselves Root ::= patterns declared in this file. A .nl file that imports only nelumbo.lang (and not nelumbo.logic) can declare types and patterns but has no way to assert facts, write rules, or run queries.
Idioms to notice
- The
::=declaration forBooleanlists many alternatives separated by commas — all productions forBoolean. This is the conventional way to declare a type with many forms. - Precedence annotations follow a ladder:
<->at 16,->at 18,|at 20,&at 22,!at 25,=/!=at 30. Tighter-binding operators get higher numbers. E[...]andA[...]use<Variable#100>to require that the binding-site position contains a bare variable (precedence 100 is near the top — almost primary-expression tight). The body uses<Boolean#0>to accept any Boolean expression, even low-precedence ones.- Four internal types (
Boolean,Literal,Function,Object) appear in the equality rules. TheLiteral/Function/Objectsplit is what makes the three equality rules sufficient.
3. nelumbo.integers (36 lines)
Builds arithmetic on top of logic.
import nelumbo.logic
Integer :: Object
private Boolean ::= add(<Integer>, <Integer>, <Integer>) @...Integers,
mult(<Integer>, <Integer>, <Integer>) @...Integers,
gt(<Integer>, <Integer>) @...Integers
Boolean ::= <Integer> ">" <Integer> #30,
<Integer> "<" <Integer> #30,
<Integer> "<=" <Integer> #30,
<Integer> ">=" <Integer> #30
Integer ::= <NUMBER> @...NInteger,
<Integer> - <Integer> #40,
<Integer> + <Integer> #40,
- <Integer> #80,
<Integer> * <Integer> #50,
<Integer> / <Integer> #50,
| <Integer> | #35
Integer a, b, c
a>b <=> gt(a,b)
a<b <=> gt(b,a)
a<=b <=> a<b | a=b
a>=b <=> a>b | a=b
a+b=c <=> add(a,b,c)
a-b=c <=> add(c,b,a)
a*b=c <=> mult(a,b,c)
a/b=c <=> mult(c,b,a)
-a=b <=> 0-a=b
|a|=b <=> b=a if a>=0,
b=-a if a<0
What is native
Two classes: Integers (the add, mult, gt @NelumboMethods) and the NInteger literal. That's it — all of integer arithmetic in Java.
What is derived from those four
Everything else, using rule rewriting. The most characteristic pattern:
a + b = c <=> add(a, b, c)
a - b = c <=> add(c, b, a)
a * b = c <=> mult(a, b, c)
a / b = c <=> mult(c, b, a)
Subtraction is not a separate operation — it's addition with the arguments permuted. Division is multiplication permuted. Because add and mult are three-argument relations (not two-argument functions), any of the three arguments can be the "output" — the native computes whichever one is missing.
Similarly, the comparison operators all derive from the single native helper gt:
a > b <=> gt(a, b)
a < b <=> gt(b, a)
a <= b <=> a < b | a = b
a >= b <=> a > b | a = b
gt (rather than the > operator) is the native because an operator functor's name cannot bind a @NelumboMethod — the named helper keeps the comparison logic in a method like add and mult.
Unary minus is a one-liner:
-a = b <=> 0 - a = b
And absolute value uses the guard pattern with mutually exclusive conditions:
|a| = b <=> b = a if a >= 0,
b = -a if a < 0
Idioms to notice
- The rewrite-with-permutation idiom makes the native count small. Rather than writing separate subtraction and division natives, the module reuses the
addandmultmethods in inverted roles. This is only possible because the natives are relational. privateis used onadd,mult, andgtbecause they are implementation details. Callers should use+,-,*,/, and the comparison operators, not the underlying private relations.- Two-clause absolute-value rule with mutually exclusive guards — the canonical way to define a piecewise function without risking contradictions.
integersTest.nl as specification
The test file exercises every operator in multiple directions:
a+11=21 ? [(a=10)][..] // solve for left operand
10+a=21 ? [(a=11)][..] // solve for right operand
10+11=a ? [(a=21)][..] // compute the sum
|a|=10 ? [(a=-10),(a=10)][(a=0),..] // |a|=10 has two solutions
Read it when you want to confirm how an operator behaves in a case you are unsure about.
4. nelumbo.rationals (46 lines)
Builds exact rationals on top of integers. Structurally identical to integers.
import nelumbo.integers
Rational :: Object
private Boolean ::= add(<Rational>, <Rational>, <Rational>) @...Rationals,
mult(<Rational>, <Rational>, <Rational>) @...Rationals,
gt(<Rational>, <Rational>) @...Rationals,
iir(<Integer>,<Integer>,<Rational>) @...Rationals
Boolean ::= <Rational> ">" <Rational> #30,
<Rational> "<" <Rational> #30,
<Rational> "<=" <Rational> #30,
<Rational> >= <Rational> #30
Rational ::= <(> - <)?> <[> <NUMBER> . <NUMBER> <]> @...Rational,
<Rational> - <Rational> #40,
<Rational> + <Rational> #40,
- <Rational> #80,
<Rational> * <Rational> #50,
<Rational> / <Rational> #50,
| <Rational> | #35,
r(<Integer>),
r(<Integer>/<Integer>)
Rational a, b, c
a>b <=> gt(a,b)
a<b <=> gt(b,a)
a<=b <=> a<b | a=b
a>=b <=> a>b | a=b
a+b=c <=> add(a,b,c)
a-b=c <=> add(c,b,a)
a*b=c <=> mult(a,b,c)
a/b=c <=> mult(c,b,a)
-a=b <=> 0.0-a=b
|a|=b <=> b=a if a>=0.0,
b=-a if a<0.0
Integer x, y
r(x)=a <=> iir(x,1,a)
r(x/y)=a <=> iir(x,y,a)
What's new relative to integers
iir — a three-argument relation (a @NelumboMethod on Rationals) that converts between (Integer, Integer) and Rational. Two Nelumbo rules wrap it:
r(x) = a <=> iir(x, 1, a)
r(x/y) = a <=> iir(x, y, a)
So r(5) is promoted to 5.0, and r(1/3) is an exact one-third. No silent conversion between Integer and Rational — you must go through r(...).
The key lesson
Notice that the arithmetic, comparison, and absolute-value rules are textually identical to the integer versions, differing only in the types and the literal 0.0 vs. 0. The same structural shape works for both number types; the types and the natives do the work of distinguishing them.
This is a good template: when adding a new numeric-like type, mirror the integer module structure. You will end up with the same four private natives (add, mult, > by another name, and a literal constructor) and the same set of derived rules.
5. nelumbo.strings (24 lines)
The smallest non-trivial module.
import nelumbo.integers
String :: Object
String ::= <STRING> @...NString
private Boolean ::= string_concat(<String>, <String>, <String>) @...Strings,
string_length(<String>, <Integer>) @...Strings,
integer_string(<Integer>, <String>) @...Strings
String ::= <String> + <String> #40,
str(<Integer>)
Integer ::= len(<String>),
int(<String>)
String a, b, c
Integer x
a + b = c <=> string_concat(a, b, c)
len(a) = x <=> string_length(a, x)
int(a) = x <=> integer_string(x, a)
str(x) = a <=> integer_string(x, a)
What's native
Two natives: NString (the literal) and Strings, which hosts all three primitives as @NelumboMethods — string_concat (three-way concatenation), string_length (two-way length), and integer_string (bidirectional integer/string conversion).
What's derived
The surface operations — + on strings, str(i), len(s), int(s) — are all Nelumbo wrappers around those three primitives.
The nicest trick here is that int(a) = x and str(x) = a share the same native integer_string, just wrapped in two directions. Two user-facing operations, one native primitive.
Bidirectional behaviour
The surface operator + is also relational:
"foo" + "bar" = a ? [(a="foobar")][..] // forward
a + "bar" = "foobar" ? [(a="foo")][..] // solve for prefix
"foo" + a = "foobar" ? [(a="bar")][..] // solve for suffix
All three work from the same rule and the same native. The Strings#string_concat method handles the three combinations internally — see native-classes.md for how.
6. nelumbo.collections (60 lines)
The only module that uses generic-type parameters.
import nelumbo.integers
Type E
Collection<E> :: Object
Set<E> :: Collection<E>
List<E> :: Collection<E>
private Boolean ::= build(<E>, <Boolean#0>, <Set<E>>) @...BuildSet,
size(...), indexOf(...), elementOf(...), subset(...),
intersection(...), union(...), diff(...), concat(...) @...Collections
Set<E> ::= { <(> <E> <,> , <)*> } @...NSet,
{ [ <E> ] ( <Boolean#0> ) } @...SetBuilder,
<Set<E>> && <Set<E>>, ▸ intersection
<Set<E>> || <Set<E>>, ▸ union
<Set<E>> - <Set<E>> ▸ difference
List<E> ::= [ <(> <E> <,> , <)*> ] @...NList,
<List<E>> + <List<E>> ▸ concatenation
Integer ::= | <Collection<E>> |, ▸ cardinality
<E> "pos" <List<E>> ▸ 0-based index
Boolean ::= <Set<E>> "<" <Set<E>>, ..., ▸ subset / superset
<E> "in" <Collection<E>> ▸ membership
E e Boolean c Set<E> s
{[e](c)} = s <=> build(e, c, s)
... operator rules wiring each operator to its predicate ...
What it introduces
Type E— the declaration that introduces a generic type parameter.lang.nluses the same mechanism for parenthesisation (Type P; P ::= (<P>)); this is its first use to define container types.Collection<E>,Set<E>, andList<E>— parameterised container types with literal syntax.Collection<E>is the common supertype.- Set-builder notation —
{[e](c)}, the comprehension form ofSet<E>. - Algebraic operations — cardinality
|c|, membershipe in c, subset/superset< > <= >=, set intersection/union/difference&& || -, list concatenation+, and list indexinge pos l. Each is a relation backed by theCollectionsnative class and runs in both directions. Seereference/stdlib/collections.mdfor the full table.
How the literal syntax works
The structural markers <(> <E> <,> , <)*> decode as:
<(>…<)*>— a group with zero-or-more repetition<E>— the element (typed by the generic parameter)<,>— the separator marker,after<,>— the actual separator character the user writes
So Set<E> accepts {}, {x}, {x, y}, {x, y, z}, and so on. Same for List<E> with [ and ].
Set-builder notation
Set<E> has a second form — the comprehension {[e](c)}, "the set of all e such that c". The [e] slot must be a bare variable, and (c) is any Boolean condition over it. It reduces to one native rule:
{[e](c)} = s <=> build(e, c, s)
build is backed by BuildSet, which — like E[...] and A[...] — is a quantifier: it evaluates the condition under each binding of the local element variable, strips that variable, and gathers the witnessing values into a set. So set construction reuses the same three-valued quantifier machinery as the logic layer:
Integer i
{[i](|i|=10)} = s ? [(s={-10,10})][(s={0}),..]
The two solutions of |i| = 10 become the fact s = {-10, 10}; i = 0 is a proven non-member, so {0} lands on the falsehoods side with .. for the open remainder.
What is still absent
Algebraic operations — membership, union, intersection, length, map, fold — are not in the module as of this writing. The module provides value types, literal syntax, and the comprehension constructor; richer behaviour is either in natives not yet shipped, or left to the user's own modules. Check the latest source and tests when you go to use it.
7. nelumbo.datetime (96 lines)
The largest stdlib module, and a good demonstration that the integer idioms scale to a much richer value domain. It imports nelumbo.integers (for the Period * Integer scaling operator) and adds four independent value types.
import nelumbo.integers
DateTime :: Object Date :: Object Time :: Object Period :: Object
DateTime ::= <[> <Date> T <Time#50> <]> @...NDateTime, <DateTime> + <Period> #40, ...
Date ::= <[> <NUMBER> - <NUMBER> - <NUMBER> <]> @...NDate, <Date> + <Period> #40, ...
Time ::= <[> <NUMBER> : <NUMBER> ... <]> @...NTime, <Time> + <Period> #40, ...
Period ::= <[> P ... <]> @...NPeriod, <Period> + <Period> #40, <Period> * <Integer> #50, ...
private Boolean ::= datetime_add(<DateTime>,<Period>,<DateTime>) @...Add,
date_add(<Date>,<Period>,<Date>) @...Add,
time_add(<Time>,<Period>,<Time>) @...Add,
period_add(<Period>,<Period>,<Period>) @...Add,
period_multiply(<Period>,<Integer>,<Period>) @...Multiply
DateTime a, b Period x, y, z Integer n
a+x=b <=> datetime_add(a,x,b)
a-x=b <=> datetime_add(b,x,a)
a-b=x <=> datetime_add(b,x,a)
x*n=y <=> period_multiply(x,n,y)
What is native
Eight classes: four literal constructors (NDate, NTime, NDateTime, NPeriod), two relation classes (Add — shared by all three instant types plus period_add, and Multiply for period_multiply), one comparison (GreaterThan), and the value record IsoDuration that backs Period. Every value is a java.time type under the hood.
What is derived
The same rewrites as the numeric modules. Subtraction is datetime_add permuted, so "instant + duration", "instant − duration", and "instant − instant" all flow through one native; <, <=, >= derive from > and = for each of the four types.
Idioms to notice
- The literals are connected-token groups. Each is wrapped in
<[> … <]>, which forbids whitespace between the inner tokens — that is what makes2024-01-15tokenize tightly rather than as three numbers and two minus signs. - Periods carry two semantics at once. They use field-based equality (
P1M != P30D) but a nominal magnitude for ordering (months = 30 days, years = 365). The module is a compact case study in modelling domain semantics through the native'sequals/compare, not the grammar. - Validation happens at parse time. Invalid dates and malformed periods reject with
file:line:colduring parsing, so they never reach the query engine as falsehoods.
See ../reference/stdlib/datetime.md for the full per-operator reference and datetimeTest.nl for the executable specification.
The takeaway
Reading all seven stdlib modules in order, a few observations crystallise:
- The stdlib is small. Around 300 lines of Nelumbo total. Not because the language is underpowered — because the language is expressive enough that a little code covers a lot.
- The syntax itself is in
.nlfiles.lang.nldeclares the pattern meta-grammar and the::,::=,::>,import, variable, type, and functor statement forms.logic.nldeclaresfact,<=>, and?. The Java core only knows enough to loadlang.nl. - Most of it is not native. Perhaps a quarter of the pattern declarations have
@annotations. The rest are defined in Nelumbo using rules. - Layering is strict. Each module imports the one below it; no module imports sideways. This is a good model for your own libraries.
- Three-way relations are the canonical primitive shape. If you are thinking of adding a new operation, check whether it fits the
op(a, b, c)shape; if it does, you can probably reuse the integer/rational/string pattern. - Rule rewriting does most of the work.
a - b = c <=> add(c, b, a)is one line. Defining subtraction without rewriting would mean another native, more code, and more bugs.
When you are writing your own module, you are writing in the same style the stdlib uses. When you are writing a native, you are writing the same kind of Java the stdlib uses.
See also
../reference/stdlib/— per-module reference with exports summary../reference/native-classes.md— catalogue of every shipped nativewriting-your-own-module.md— build a library in the same stylenative-cookbook.md— write natives in the same style../explanation/architecture.md— why the stdlib is a library, not part of the language