Propositional logic

Nelumbo is three-valued: a query is true, false, or unknown. The result notation [facts][falsehoods] shows the two sides; [()][] means "true", [][()] means "false", and [..][..] means "unknown". Edit and press Run (or Cmd/Ctrl+Enter).

import nelumbo.logic true & true ? true | false ? !false ? unknown & true ?

Make the query below come out true by changing only the operator (the expected-result annotation [()][] means the editor will underline a mismatch until it is true).

import nelumbo.logic false & true ? [()][]
import nelumbo.logic

false | true ? [()][]

Facts & queries

Ground truth is asserted with fact. Objects and their functors are declared first (Person :: Object introduces a type, FactType ::= ... a fact shape), then a query binds a variable and enumerates every matching answer.

import nelumbo.logic Person :: Object FactType ::= friends(<Person>,<Person>) Person ::= Piet, Jan, Klaas fact friends(Piet, Jan), friends(Jan, Klaas) Person X friends(Piet, X) ?

Add a fact so that friends(Klaas, Kees) holds.

import nelumbo.logic Person :: Object FactType ::= friends(<Person>,<Person>) Person ::= Piet, Jan, Klaas, Kees // add a fact here friends(Klaas, Kees) ? [()][]
import nelumbo.logic
Person :: Object
FactType ::= friends(<Person>,<Person>)
Person ::= Piet, Jan, Klaas, Kees
fact friends(Klaas, Kees)
friends(Klaas, Kees) ? [()][]

Rules & recursion

A rule <=> defines how a functor is derived; if attaches a guard to each alternative, and a rule may reference itself to recurse. The classic Fibonacci rule has a base case for n<=1 and a recursive step for n>1.

import nelumbo.integers Integer ::= fib(<Integer>) Integer n, f fib(n)=f <=> f=n if n<=1, f=fib(n-1)+fib(n-2) if n>1 Integer r fib(7)=r ?

Define factorial so that factorial(5) = 120.

import nelumbo.integers Integer ::= factorial(<Integer>) Integer n, r // fix this rule so factorial(5)=120 (hint: 1 if n<=0, else n*factorial(n-1)) factorial(n)=r <=> r=n factorial(5)=r ? [(r=120)][..]
import nelumbo.integers
Integer ::= factorial(<Integer>)
Integer n, r
factorial(n)=r <=> r=1 if n<=0, r=n*factorial(n-1) if n>0
factorial(5)=r ? [(r=120)][..]

Data types

Nelumbo ships built-in types - integers, rationals, strings, collections, datetime - each importable, with its own operators and functions. Declare a variable of a type, then let a query compute and bind it. Operators like + and functions like len come from the imported module.

import nelumbo.integers import nelumbo.strings Integer i String s 2+3=i ? "foo"+"bar"=s ? len("hello")=i ?
import nelumbo.collections Set<Integer> s Integer i s={1,2,3} ? |{1,2,3}|=i ?

Fix the concatenation so a = "lotus" and its length check (len(a) = 5) passes.

import nelumbo.strings String a Integer d // fix the concatenation so a = "lotus" and its length is 5 "lo"+"xus"=a & len(a)=d ? [(a="lotus",d=5)][..]
import nelumbo.strings
String  a
Integer d
"lo"+"tus"=a & len(a)=d ? [(a="lotus",d=5)][..]

Relational KB

A knowledge base is facts plus rules over relations. Base facts (here pc, parent-child) are asserted directly; rules with quantifiers such as E[x] (there exists) and A[y] (for all) derive new relations from them. Below, the mother relation m is derived from pc via an existential over female children.

import nelumbo.logic Person :: Object Male :: Person Female :: Person FactType ::= pc(<Person>,<Person>) Person ::= p(<Person>), c(<Person>), m(<Person>) Person a, b Female x c(a)=b <=> pc(a,b) p(a)=b <=> pc(b,a) m(a)=b <=> E[x](c(x)=a & b=x) Female ::= Juliana, Beatrix fact pc(Juliana, Beatrix) Person who m(Beatrix)=who ?

Add a father relation f that mirrors the mother rule, using E[y] over male children, so f(Beatrix) = Bernhard.

import nelumbo.logic Person :: Object Male :: Person Female :: Person FactType ::= pc(<Person>,<Person>) Person ::= p(<Person>), c(<Person>), m(<Person>), f(<Person>) Person a, b Female x Male y c(a)=b <=> pc(a,b) p(a)=b <=> pc(b,a) m(a)=b <=> E[x](c(x)=a & b=x) // fix this rule so f(Beatrix)=Bernhard (mirror the mother rule using Male y) f(a)=b <=> E[y](p(y)=a & b=y) Female ::= Juliana, Beatrix Male ::= Bernhard fact pc(Juliana, Beatrix), pc(Bernhard, Beatrix) Person who f(Beatrix)=who ? [(who=Bernhard)][..]
import nelumbo.logic
Person   :: Object
Male     :: Person
Female   :: Person
FactType ::= pc(<Person>,<Person>)
Person   ::= p(<Person>), c(<Person>), m(<Person>), f(<Person>)
Person a, b
Female x
Male   y
c(a)=b <=> pc(a,b)
p(a)=b <=> pc(b,a)
m(a)=b <=> E[x](c(x)=a & b=x)
f(a)=b <=> E[y](c(y)=a & b=y)
Female ::= Juliana, Beatrix
Male   ::= Bernhard
fact pc(Juliana, Beatrix),
     pc(Bernhard, Beatrix)
Person who
f(Beatrix)=who ? [(who=Bernhard)][..]

Custom DSL

Nelumbo is a meta-language: declare your own functors and patterns with an explicit precedence (#N) and natural-language statements and queries become first-class syntax. Here a friend functor is derived from base friends facts, then the phrase X is a friend of Y is declared as a Boolean pattern that reads as ordinary English.

import nelumbo.logic Person :: Object FactType ::= friends(<Person>,<Person>) Person ::= friend(<Person>) Person ::= Piet, Jan, Klaas Person A, B, Who friend(A)=B <=> friends(A,B) | friends(B,A) | friend(friend(A))=B Boolean ::= <Person> is a friend of <Person> #30 Person X, Y X is a friend of Y <=> friend(Y)=X fact friends(Piet, Jan), friends(Jan, Klaas) Who is a friend of Piet ?

A synonym phrase X knows Y has been declared but its rule is wrong (X knows Y <=> X=Y). Fix the rule so Who knows Piet lists the same friends as the phrase above.

import nelumbo.logic Person :: Object FactType ::= friends(<Person>,<Person>) Person ::= friend(<Person>) Person ::= Piet, Jan, Klaas Person A, B, Who friend(A)=B <=> friends(A,B) | friends(B,A) | friend(friend(A))=B Boolean ::= <Person> knows <Person> #30 Person X, Y X knows Y <=> X=Y fact friends(Piet, Jan), friends(Jan, Klaas) Who knows Piet ? [(Who=Jan),(Who=Klaas),(Who=Piet)][..]
import nelumbo.logic
Person   :: Object
FactType ::= friends(<Person>,<Person>)
Person   ::= friend(<Person>)
Person   ::= Piet, Jan, Klaas
Person A, B, Who
friend(A)=B <=> friends(A,B) | friends(B,A) | friend(friend(A))=B
Boolean ::= <Person> knows <Person> #30
Person X, Y
X knows Y <=> friend(Y)=X
fact friends(Piet, Jan), friends(Jan, Klaas)
Who knows Piet ? [(Who=Jan),(Who=Klaas),(Who=Piet)][..]

Transformations

A transformation rule ::> rewrites one construct into several. The attr declaration below expands - at declaration time - into a type, a := setter, a private fact relation, and a getter rule. Declaring attr Person name String then behaves as if all four were written by hand.

import nelumbo.strings Root ::= attr <Type> <NAME> <Type> #100 Type OT, AT NAME AN attr OT AN AT ::> { AT ::= <OT>.AN Root ::= <OT>.AN := <AT> private FactType ::= AN(<OT>,<AT>) OT o AT a o.AN=a <=> AN(o,a) o.AN := a ::> { fact AN(o,a) } } Person :: Object attr Person name String Person ::= Piet Piet.name := "Piet" String n Piet.name=n ?

An address attribute has been added to Person, but Piet's address is set to the wrong value. Set it to "Kalverstraat" so the query matches.

import nelumbo.strings Root ::= attr <Type> <NAME> <Type> #100 Type OT, AT NAME AN attr OT AN AT ::> { AT ::= <OT>.AN Root ::= <OT>.AN := <AT> private FactType ::= AN(<OT>,<AT>) OT o AT a o.AN=a <=> AN(o,a) o.AN := a ::> { fact AN(o,a) } } Person :: Object attr Person name String attr Person address String Person ::= Piet Piet.name := "Piet" Piet.address := "Dam" String a Piet.address=a ? [(a="Kalverstraat")][..]
import nelumbo.strings
Root ::= attr <Type> <NAME> <Type> #100
Type OT, AT
NAME AN
attr OT AN AT ::> {
    AT   ::= <OT>.AN
    Root ::= <OT>.AN := <AT>
    private FactType ::= AN(<OT>,<AT>)
    OT o
    AT a
    o.AN=a <=> AN(o,a)
    o.AN := a ::> {
        fact AN(o,a)
    }
}
Person :: Object
attr Person name String
attr Person address String
Person ::= Piet
Piet.name := "Piet"
Piet.address := "Kalverstraat"
String a
Piet.address=a ? [(a="Kalverstraat")][..]

Scoping

A { } block introduces a namespace; a private declaration is hidden outside its block; and the same name can be reused independently in separate scopes. Both scopes below declare their own XXX member without any clash.

import nelumbo.logic { Aa :: Object private Aa ::= XXX Aa x x=XXX ? } { Bb :: Object private Bb ::= XXX Bb y y=XXX ? }

The third scope declares Cc with members XXX and YYY, but its query binds z to the wrong one. Fix the query so z=XXX holds.

import nelumbo.logic { Aa :: Object private Aa ::= XXX Aa x x=XXX ? [(x=XXX)][..] } { Bb :: Object private Bb ::= XXX Bb y y=XXX ? [(y=XXX)][..] } { Cc :: Object private Cc ::= XXX, YYY Cc z z=YYY ? [(z=XXX)][..] }
import nelumbo.logic
{
    Aa         :: Object
    private Aa ::= XXX
    Aa x
    x=XXX ? [(x=XXX)][..]
}
{
    Bb         :: Object
    private Bb ::= XXX
    Bb y
    y=XXX ? [(y=XXX)][..]
}
{
    Cc         :: Object
    private Cc ::= XXX, YYY
    Cc z
    z=XXX ? [(z=XXX)][..]
}