Standard library tour

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:

  1. lang.nl — 51 lines — syntactic bootstrap: tokens, object hierarchy, the pattern meta-grammar, top-level forms
  2. logic.nl — 44 lines — Boolean, connectives, quantifiers, equality, and the fact/<=>/? statement forms
  3. integers.nl — 36 lines — arithmetic and comparison
  4. rationals.nl — 46 lines — exact rational arithmetic
  5. strings.nl — 24 lines — string operations
  6. collections.nl — 21 lines — generic Set<E> and List<E>, plus set-builder notation
  7. datetime.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

Idioms to notice

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

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


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

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

How the literal syntax works

The structural markers <(> <E> <,> , <)*> decode as:

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

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:

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