Types & functors
Before any logic, Nelumbo lets you define syntax: which phrases exist and how they are built.
Person :: Object declares a type and Male :: Person a subtype.
Male ::= Piet, Jan declares constants of a type. A functor declares a new
phrase with ::=: Person ::= father(<Person>) says that
father(a person) is itself a person, where each <T>
is a hole for an expression of type T. A phrase whose type extends
Root can stand on its own line as a sentence; give such a functor the precedence
#0. These sections have no queries and no meaning yet: a sentence is correct when it
parses, and the editor underlines it in red when it does not.
Marie knows Klaas does not parse: Klaas is declared as an Animal, and knows takes a Person. Klaas is a man: fix the declarations so the sentence parses.
import nelumbo.lang Person :: Object Male :: Person Female :: Person Animal :: Object Male ::= Piet, Jan, Klaas Female ::= Marie Sentence :: Root Sentence ::= <Person> knows <Person> #0 Marie knows Klaas
Declare a type Thing with the constants flowers and a book, and a functor, so that both sentences parse.
import nelumbo.lang Person :: Object Person ::= Piet, Marie Thing :: Object Thing ::= flowers, a book Sentence :: Root Sentence ::= <Person> gives <Thing> to <Person> #0 Marie gives flowers to Piet Piet gives a book to Marie
Patterns
A functor's pattern is more than words and holes. <(> really <)?> is
optional, <(> likes <|> loves <)> is a choice, and
<(> <Person> <,> , <)+> repeats a hole one or more times,
separated by commas (<)*> also allows none). The token types
<NUMBER>, <STRING> and <NAME> accept a
number, a quoted string or a name. Several alternatives of one type are separated by commas, each
with its own precedence.
Piet can invite any number of guests, with and before the last one. Change the pattern so all three sentences parse.
import nelumbo.lang Person :: Object Person ::= Piet, Jan, Marie, Klaas Sentence :: Root Sentence ::= <Person> invites <(> <Person> <,> , <)+> <(> and <Person> <)?> #0 Piet invites Jan Piet invites Jan and Marie Piet invites Jan, Marie and Klaas
Change the pattern so that Jan does not know Piet parses as well as Jan knows Piet.
import nelumbo.lang Person :: Object Person ::= Piet, Jan Sentence :: Root Sentence ::= <Person> <(> knows <|> does not know <)> <Person> #0 Jan knows Piet Jan does not know Piet
Precedence
An operator is a functor whose pattern starts with a hole, like <Expr> plus <Expr>.
When a sentence contains several operators, their precedence #N decides how they group: the
higher one binds tighter. With plus at #40 and times at #50,
one plus two times three is one plus (two times three); operators of equal
precedence group from the left. An operator without a precedence does not parse at all. A hole can
carry a precedence too: <Expr#45> takes only operators above 45, so times
but not plus. A hole without one takes the precedence of its functor; that is why the
#0 of a sentence lets every operator in. The standard operators range from #16
(<->) to #80 (unary -).
minus has no precedence, so the last sentence does not parse. Give it the precedence of plus.
import nelumbo.lang
Expr :: Object
Expr ::= one, two, three
Expr ::= <Expr> plus <Expr> #40,
<Expr> minus <Expr> #40,
<Expr> times <Expr> #50
Sentence :: Root
Sentence ::= compute <Expr> #0
compute three minus one times two
twice one plus two does not parse: the hole <Expr#45> rejects plus. Change only the precedence of the hole so both sentences parse.
import nelumbo.lang
Expr :: Object
Expr ::= one, two, three
Expr ::= <Expr> plus <Expr> #40,
<Expr> times <Expr> #50
Sentence :: Root
Sentence ::= twice <Expr#35> #0
twice two times three
twice one plus two
Parentheses
Parentheses override precedence: (one plus two) times three. You never declare them:
nelumbo.lang ends with Type P and P ::= (<P>) @nelumbo.lang.Parenthesized,
one generic functor (see the next section) that gives every type its parentheses. The @ binds
a functor to a Java class; Parenthesized returns the expression inside, so (one)
is just one, not a new phrase. A parenthesized expression is complete in itself, so it fits a
hole of any precedence: twice (one plus two) parses where twice one plus two did not.
Add square brackets as a second way to group expressions so that the last sentence parses. Bind them to @nelumbo.lang.Parenthesized too, so that [one] is just one.
import nelumbo.lang
Expr :: Object
Expr ::= one, two, three
Expr ::= <Expr> plus <Expr> #40,
<Expr> times <Expr> #50
Expr ::= [<Expr>] @nelumbo.lang.Parenthesized
Sentence :: Root
Sentence ::= compute <Expr> #0
compute (one plus two) times three
compute [one plus two] times three
Generics
Type E declares a type parameter. With it, Team<E> :: Object declares a
generic type and Team<E> ::= team of <(> <E> <,> , <)+> a functor for it:
team of Piet, Jan is a Team<Person> and team of Rex, Tom a
Team<Animal>. A hole names the kind of team it takes: <Team<Person>>
rejects a team of animals, <Team<E>> takes a team of anything. Write a generic type
without spaces: Team<E>, not Team < E >. The parentheses of the
previous section are declared this way, and so are the collection types List<E> and
Set<E>.
A team can only hold people, so the last sentence does not parse. Make Team generic so that animals can form teams too.
import nelumbo.lang Person :: Object Person ::= Piet, Jan, Marie Animal :: Object Animal ::= Rex, Tom, Bello Type E Team<E> :: Object Team<E> ::= team of <(> <E> <,> , <)+> Sentence :: Root Sentence ::= <Team<E>> plays against <Team<E>> #0 team of Piet, Jan plays against team of Marie team of Rex plays against team of Tom, Bello
Connected tokens
Whitespace between tokens does not matter: the pattern <NUMBER> : <NUMBER> accepts
9:30 and 9 : 30 alike. Between <[> and <]> the
tokens must touch: <[> <NUMBER> : <NUMBER> <]> accepts only 9:30.
Use it for a literal written as several tokens, such as a time, a date or a decimal number, so that it cannot
be mistaken for an expression with an operator. <[> and <]> themselves do not
appear in the sentence. The standard library writes 3.14, 2025-01-01 and
16#FF this way, and nelumbo.lang the head of a generic type like
Team<E>.
Declare a type Date for dates like 1990-05-17, written without spaces, so that both sentences parse.
import nelumbo.lang
Person :: Object
Person ::= Piet, Marie
Time :: Object
Time ::= <[> <NUMBER> : <NUMBER> <]>
Date :: Object
Date ::= <[> <NUMBER> - <NUMBER> - <NUMBER> <]>
Sentence :: Root
Sentence ::= <Person> arrives at <Time> #0,
<Person> was born on <Date> #0
Piet arrives at 9:30
Marie was born on 1990-05-17
Named patterns
A recurring piece of a pattern can be given a name: pattern GUESTS ::= ... declares it, and
<GUESTS> stands for its body wherever it is used after the declaration. A named
pattern is just an abbreviation: unlike a functor, it declares no type and builds no value. Here
GUESTS is the guest list of the Patterns exercise, now shared by two sentences.
nelumbo.lang uses named patterns itself, for instance
pattern QNAME ::= <[> <(> <NAME> <,> . <)+> <]> for the dotted
name after import.
Both sentences use <TIME>, which is not declared yet. Declare it as a named pattern for a time like 9:30, above the functor that uses it.
import nelumbo.lang
Person :: Object
Person ::= Piet, Marie, Klaas
pattern TIME ::= <[> <NUMBER> : <NUMBER> <]>
Sentence :: Root
Sentence ::= <Person> arrives at <TIME> #0,
<Person> works from <TIME> to <TIME> #0
Marie arrives at 9:30
Klaas works from 8:00 to 17:30
Scoping
A { } block is a scope. A functor declared private in a block exists only
inside that block, including nested blocks. Without private, a declaration in a block
stays visible after the block: the block alone hides nothing. Below, Max is private to the
block and Piet is not. private is also how two functors with the same pattern
are kept apart: if they are declared side by side, the parser cannot tell them apart and reports a
non deterministic pattern merge.
Both functors are written friend(<Person>), one for a person and one for a dog, so they clash. Restrict each one to the sentence that uses it so that both sentences parse.
import nelumbo.lang
Person :: Object
Dog :: Object
Person ::= Piet, Marie
Sentence :: Root
Sentence ::= <Person> reads #0,
<Dog> barks #0
{
private Person ::= friend(<Person>)
friend(Marie) reads
}
{
private Dog ::= friend(<Person>)
friend(Piet) barks
}
Rex is private to its block, so the last sentence does not parse. Make it parse without moving it.
import nelumbo.lang
Person :: Object
Dog :: Object
Sentence :: Root
Sentence ::= <Person> reads #0,
<Dog> barks #0
{
private Person ::= Max
Max reads
}
{
Dog ::= Rex
Rex barks
}
Rex barks
Propositional logic
This is the first section with queries. A query is an expression followed by
?: it asks Nelumbo whether the expression holds. import nelumbo.logic
provides the values true, false and unknown, and the
operators & (and), | (or) and ! (not).
There is no Run button: Nelumbo evaluates every query as you type and shows its result at the end of the line, in a purple label. A long result is cut short; hover over it to see all of it.
A result has two sides, [facts][falsehoods]: on the left the answers for which the
query is true, on the right those for which it is false. An answer is written between parentheses.
These queries have no variables, so an answer is just (). .. means that a
side may contain more than it shows. In the results below:
[()][]means "true",[][()]means "false",[..][..]means "unknown": nothing is known on either side.
Nelumbo is three-valued: a query is true, false or unknown. unknown & true stays
unknown, but unknown | true is true, whatever the unknown part turns out to be. Try
changing the queries below.
After the ? you may write the result you expect, in the same notation as the label:
true & false ? [][()] expects false. The editor then checks it instead of showing the
result: a green check mark at the end of the line when the result matches; otherwise the expected
result is underlined in red and the actual result follows a red cross. Without an expected result
you just get the purple label. The exercises use this to show when you are done.
Make the query below come out true by changing only the operator.
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, Boolean ::= ... a relation),
then a query binds a variable and enumerates every matching answer. Facts are open: what is
not asserted is not false, so the trailing .. in [(X=Jan),..] says
more answers may exist.
Add a fact so that friends(Klaas, Kees) holds.
import nelumbo.logic Person :: Object Boolean ::= friends(<Person>,<Person>) Person ::= Piet, Jan, Klaas, Kees fact friends(Klaas, Kees) friends(Klaas, Kees) ? [()][]
A function value can be asserted too. A function has exactly one value, so its fact is
closed: after fact age(Piet)=40 the answer is [(a=40)]
without .., and every other age is false. Statements run top to bottom, and each fact
or rule only adds knowledge: unknown answers become true or false, open answers grow or close, and
nothing already proven is withdrawn. Nelumbo is monotonic, and it checks this. A fact can
never be taken back further down: after fact age(Piet)=40, a later
fact age(Piet)=22 is an inconsistency, and nothing after it is evaluated. In the same
way, when a query is asked again further down, its answer must keep everything the earlier answer
showed and may only add to it. If not, the query reports an inconsistency.
Only relations and function values can be asserted: fact !friends(Piet, Klaas)
is an error.
Give Jan an age so that both queries below hold.
import nelumbo.integers Person :: Object Person ::= Piet, Jan Integer ::= age(<Person>) Integer a fact age(Jan)=35 age(Jan)=a ? [(a=35)][..] age(Jan)=40 ? [][()]
Unique values
Facts are open, but a hole can be declared unique with one:
<Person> lives in <one City> says that a person lives in exactly one city. A fact then
closes the hole for that person: Piet lives in c ? answers Amsterdam without
.., and every other city is false. Nothing is asserted for Jan, so his city stays
unknown, and the other direction stays open: more people may live in Amsterdam. likes
has no one, so Piet may like other cities too and Utrecht is unknown. A second city for Piet
would be rejected as an inconsistency. This is also why a function value is closed: both sides of
= are unique holes.
Everybody was born in exactly one city. Change the pattern so that the expected results hold.
import nelumbo.logic Person :: Object Person ::= Piet, Jan City :: Object City ::= Amsterdam, Utrecht Boolean ::= <Person> was born in <one City> #30 City c fact Jan was born in Utrecht Jan was born in c ? [(c=Utrecht)][..] Jan was born in Amsterdam ? [][()]
Rules & recursion
A rule <=> defines how a functor is derived. For a function, == gives
its value directly: fib(n) == e is short for fib(n)=f <=> f=e.
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. Ask for a function's value with a function query: fib(7) ?
introduces its own result variable, so the answer lists the value without a name,
[(13)][..]. It is the same as fib(7)=r ? with a declared
Integer r.
Define factorial so that factorial(5) = 120.
import nelumbo.integers Integer ::= factorial(<Integer>) Integer n factorial(n) == 1 if n<=0, n*factorial(n-1) if n>0 factorial(5) ? [(120)][..]
Adding a rule adds knowledge just like adding a fact. It can even add only falsehoods: below,
adult(Jan) is unknown until a rule says that nobody younger than 18 is an adult.
After that rule adult(Jan) is false, while the fact adult(Piet) still
holds. The rule is silent where its guard fails, so it takes nothing away. Had Piet been 16, the
query adult(Piet) ? after the rule would report an inconsistency with the fact.
Combining rules
Several rules can define one relation; their answers are combined side by side. The first rule says
that Piet eats what he likes. All its answers are on the facts side:
[(f=Apple),(f=Pear),..][..]. Its falsehood side is empty but open (..):
nothing is known to be false yet. The second rule only adds falsehoods: Piet does not eat what he is
allergic to. It is silent where its guard fails, so it leaves the facts side alone and fills the open
falsehood side: [(f=Apple),(f=Pear),..][(f=Fish),(f=Nut),..]. Bread is on neither side,
so it stays unknown. The rules must agree: had Piet also liked nuts, the query would report an
inconsistency.
Piet invites his friends, but nothing says whom he does not invite. Add a second rule so that nobody invites someone they quarrel with, and the falsehood side fills up.
import nelumbo.logic Person :: Object Person ::= Piet, Jan, Klaas, Kees, Bob Boolean ::= friends(<Person>,<Person>), quarrels(<Person>,<Person>), invites(<Person>,<Person>) Person p, q fact friends(Piet, Jan), friends(Piet, Klaas) fact quarrels(Piet, Kees), quarrels(Piet, Bob) invites(p,q) <=> friends(p,q) invites(p,q) <=> false if quarrels(p,q) invites(Piet,q) ? [(q=Jan),(q=Klaas),..][(q=Bob),(q=Kees),..]
Cyclic rules
A rule may depend on itself in a cycle. The recursion of fib ends because
n gets smaller; here nothing gets smaller: a friend of a friend of mine is a friend of mine,
so to find my friends, Nelumbo needs my friends. It solves such a cycle by iteration. It starts with
the facts (Jan is a friend of Piet), applies the rule and finds Klaas, a friend of Jan, applies it
again and finds Kees, and stops when a round adds nothing new. That end point is the
fixpoint, and its friends are the answer. The rule need not mention the facts: the facts and
rules of one relation are combined. What the fixpoint does not reach stays unknown, not false: no fact
mentions Bob, but the facts are open, so Bob is a friend of Piet ? is
[..][..].
Friendship goes both ways: if Jan is a friend of Piet, Piet is a friend of Jan. Extend the rule so the expected results hold. Through Jan, Piet then turns out to be a friend of himself.
import nelumbo.logic Person :: Object Person ::= Piet, Jan, Klaas, Kees Boolean ::= <Person> is a friend of <Person> #30 Person me, f, ff, Who ff is a friend of me <=> me is a friend of ff | E[f](ff is a friend of f & f is a friend of me) fact Jan is a friend of Piet fact Klaas is a friend of Jan fact Kees is a friend of Klaas Piet is a friend of Kees ? [()][] Piet is a friend of Piet ? [()][] Who is a friend of Piet ? [(Who=Jan),(Who=Kees),(Who=Klaas),(Who=Piet),..][..]
Data types
Nelumbo ships with 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.
Fix the concatenation so that a = "lotus" and the length check
(len(a) = 5) passes.
import nelumbo.strings String a Integer d "lo"+"tus"=a & len(a)=d ? [(a="lotus",d=5)][..]
Equality
What = means depends on what a phrase is. A functor with arguments, declared for an
Object type, is a function: double(2) stands for the value a rule or
fact gives it, so double(2)=4 is true and double(2)=5 is false. Without
such a rule or fact the value is unknown, so even point(1,2)=point(1,2) is unknown. A
functor declared struct builds values instead: pos(1,2) is the value
itself, equal to another one exactly when it is built the same way, and it can be the answer to a
query (q=pos(1,2)). Constants such as Piet are literals too.
A card is its number, nothing more. Change the declaration of Card so that cards are compared by how they are built.
import nelumbo.integers Card :: Object struct Card ::= card(<Integer>) card(7)=card(7) ? [()][] card(7)=card(8) ? [][()]
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 parents.
Because pc facts are open, Beatrix may have parents not asserted here: the answer
ends in ...
Add a father relation f that mirrors the mother rule, using
E[y] over male parents, so f(Beatrix,who) finds Bernhard.
import nelumbo.logic
Person :: Object
Male :: Person
Female :: Person
Boolean ::= pc(<Person>,<Person>), m(<Person>,<Person>), f(<Person>,<Person>)
Person a, b
Female x
Male y
m(a,b) <=> E[x](pc(x,a) & b=x)
f(a,b) <=> E[y](pc(y,a) & b=y)
Female ::= Juliana, Beatrix
Male ::= Bernhard
fact pc(Juliana, Beatrix),
pc(Bernhard, Beatrix)
Person who
f(Beatrix,who) ? [(who=Bernhard),..][..]
Quantifiers
E[c](...) (there exists) and A[c](...) (for all) never try every member of
a type: c only gets values by navigation, from something already known, here
from Juliana to her children. Without navigation a quantifier gives no answer, even though four
people are declared. That is a design choice: Person ::= Juliana, ... names some
people, not all of them. If A[c](age(c)>10) were decided by trying the declared
people, declaring one more could turn true into false, and Nelumbo never withdraws an answer. So
navigate: through a fact or rule (pc(Juliana,c)), through =, or through a
known collection (c in children(Juliana)). Over a closed set such as Juliana's children,
A can be true and E false; over open facts more children may exist, so only
finding an example decides. Put the membership in parentheses before ->.
Both questions are about Juliana's children, but the quantifiers do not navigate there, so they give no answer. Make them navigate so the expected results hold.
import nelumbo.collections
Person :: Object
Person ::= Juliana, Beatrix, Irene, Margriet
Integer ::= age(<Person>)
Set<Person> ::= children(<Person>)
Person c
fact age(Beatrix)=20, age(Irene)=15, age(Margriet)=30
fact children(Juliana)={Beatrix, Irene, Margriet}
// is every child of Juliana older than 12?
A[c]((c in children(Juliana)) -> age(c)>12) ? [()][]
// does Juliana have a child younger than 18?
E[c](c in children(Juliana) & age(c)<18) ? [()][]
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
rule makes the friends facts symmetric and transitive, then the phrase
X is a friend of Y is declared as a Boolean pattern that reads as
ordinary English.
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 Boolean ::= friends(<Person>,<Person>) Person ::= Piet, Jan, Klaas Person a, b, c, Who friends(a,c) <=> friends(a,c) | friends(c,a) | E[b](friends(a,b) & friends(b,c)) Boolean ::= <Person> knows <Person> #30 Person X, Y X knows Y <=> friends(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 getter function
o.name and a := setter that itself transforms into a
fact about that function. Declaring attr Person name String then
behaves as if both were written by hand.
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 #30
Root ::= <{OT,Literal}>.AN := <{AT,Literal}> #30
{OT,Literal} ol
{AT,Literal} al
ol.AN := al ::> {
fact ol.AN = al
}
}
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")][..]
Controlled English
A transformation can turn a dictionary into a language. A controlled natural language is a
fixed set of sentence shapes, filled with words from a dictionary, where each word's entry decides
how the word may be used. Here noun a dog and noun an owl record every noun
with its own article, and the transformation gives each entry its own sentences:
Rex is a dog and is Hoot an owl ?. So Hoot is a owl does not
parse: after a only dog and cat fit. verb chases
adds Rex chases Tom and who chases Hoot ?. A sentence becomes a fact and a
question becomes a query. Nothing says that Tom is not a dog, so is Tom a dog ? is
unknown. The Dutch example deHet does the same with the articles
de and het.
Dumbo is an elephant and scares Rex, but elephant and scares are not in the dictionary yet. Add them so that all sentences and questions parse.
import nelumbo.logic
Article :: Object
Article ::= a, an
Root ::= noun <Article> <NAME> #100,
verb <NAME> #100
Thing :: Object
{
Article ar
NAME n, v
noun ar n ::> {
Root ::= <{Thing,Literal}> is <ar> n #0
Root ::= is <Thing> <ar> n ? #0
private Boolean ::= n(<Thing>)
Thing x
is x ar n ? ::> {
n(x) ?
}
{Thing,Literal} t
t is ar n ::> {
fact n(t)
}
}
verb v ::> {
Root ::= <{Thing,Literal}> v <{Thing,Literal}> #0
Root ::= who v <Thing> ? #0
private Boolean ::= v(<Thing>,<Thing>)
Thing x, y
who v y ? ::> {
v(x,y) ?
}
{Thing,Literal} s, o
s v o ::> {
fact v(s,o)
}
}
}
noun a dog
noun an owl
noun an elephant
verb scares
Thing ::= Rex, Hoot, Dumbo
Rex is a dog
Hoot is an owl
Dumbo is an elephant
Dumbo scares Rex
is Dumbo an elephant ?
who scares Rex ?
Family tree
A project brings the previous sections together. Only the end of the file is given: facts and questions, each with the answer it should get. You design and write everything above it. Here you build a small English language about the Dutch royal family. Some answers are closed: Beatrix has exactly two parents. Others are open: Amalia's mother is not given, so she may have more grandparents than the two that are known.
Hints
- Declare
Personwith the subtypesMaleandFemale, and the nine people as constants of those. - Declare the sentences as
Booleanphrases with precedence#30. A person has one mother:<one Person> is the mother of <Person>makesIrene is the mother of Beatrixfalse. The same goes for the father. - Declare the variables, including
Who. - A parent is the mother or the father. A grandparent is a parent of a parent
(
E[p](...)), a grandchild the other way around. - An ancestor is a parent, or a parent of an ancestor: a cyclic rule.
- Sisters and brothers share a parent and are not the same person (
!=). To tell them apart, add<Person> is femalewith two rules: true for aFemalevariable, false for aMaleone. - An aunt is a sister of a parent.
Write the language above the given part so that the facts parse and every question gets the expected answer.
import nelumbo.logic
Person :: Object
Male :: Person
Female :: Person
Male ::= Bernhard, Claus, Alexander, Friso
Female ::= Juliana, Beatrix, Irene, Margriet, Amalia
Boolean ::= <one Person> is the mother of <Person> #30,
<one Person> is the father of <Person> #30,
<Person> is female #30,
<Person> is a parent of <Person> #30,
<Person> is a grandparent of <Person> #30,
<Person> is a grandchild of <Person> #30,
<Person> is an ancestor of <Person> #30,
<Person> is a sister of <Person> #30,
<Person> is a brother of <Person> #30,
<Person> is an aunt of <Person> #30
Person p, c, g, a, s, x, Who
Female f
Male m
f is female <=> true
m is female <=> false
p is a parent of c <=> p is the mother of c | p is the father of c
g is a grandparent of c <=> E[p](g is a parent of p & p is a parent of c)
c is a grandchild of g <=> g is a grandparent of c
a is an ancestor of c <=> a is a parent of c | E[p](a is a parent of p & p is an ancestor of c)
s is a sister of x <=> s is female & s != x & E[p](p is a parent of x & p is a parent of s)
s is a brother of x <=> !(s is female) & s != x & E[p](p is a parent of x & p is a parent of s)
s is an aunt of c <=> E[p](p is a parent of c & s is a sister of p)
// ---- given: make these facts parse and these questions give the expected answers ----
fact Juliana is the mother of Beatrix, Juliana is the mother of Irene, Juliana is the mother of Margriet
fact Bernhard is the father of Beatrix, Bernhard is the father of Irene, Bernhard is the father of Margriet
fact Beatrix is the mother of Alexander, Beatrix is the mother of Friso
fact Claus is the father of Alexander, Claus is the father of Friso
fact Alexander is the father of Amalia
Who is the mother of Beatrix ? [(Who=Juliana)][..]
Irene is the mother of Beatrix ? [][()]
Who is a parent of Beatrix ? [(Who=Bernhard),(Who=Juliana)][..]
Who is a grandparent of Amalia ? [(Who=Beatrix),(Who=Claus),..][..]
Who is a grandchild of Juliana ? [(Who=Alexander),(Who=Friso),..][..]
Who is an ancestor of Amalia ? [(Who=Alexander),(Who=Beatrix),(Who=Bernhard),(Who=Claus),(Who=Juliana),..][..]
Who is a sister of Beatrix ? [(Who=Irene),(Who=Margriet),..][(Who=Beatrix),..]
Who is a brother of Alexander ? [(Who=Friso),..][(Who=Alexander),..]
Friso is a sister of Alexander ? [][()]
Who is an aunt of Alexander ? [(Who=Irene),(Who=Margriet),..][..]
Club fees
The rules of a sports club are given in English, as comments, with its members and the fee each one should pay. Turn them into a small language that computes every fee. Some answers come from what is not possible: Henk is not a junior because he has exactly one age, and it is 70. And a rule only needs what it uses: Piet's age is unknown, so his fee is too, but Bram is a volunteer, and a volunteer pays nothing at any age.
Hints
- Declare
Personwith the nine members, and a typeMembershipwith the constantsregular,weekdayandvolunteer. - Declare the sentences as
Booleanphrases with precedence#30. A person has one age and one membership:<Person> is <one Integer> years old. Withoutone,Henk is a juniorcannot be false. - Define junior, adult and senior with
E[n](p is n years old & ...). Mind the boundaries: at 18 you are an adult, at 65 a senior. - Declare the fees as functions,
Integer ::= <Person>'s fee #60. The pattern starts with a hole, so it is an operator and needs a precedence. - Write two function rules with
==andif: a base fee per age group, and the fee per membership, which uses the base fee.
Write the language above the given part so that the facts parse and every question gets the expected answer.
import nelumbo.integers
Person :: Object
Person ::= Anna, Lisa, Henk, Kees, Marie, Sven, Eva, Piet, Bram
Membership :: Object
Membership ::= regular, weekday, volunteer
Boolean ::= <Person> is <one Integer> years old #30,
<Person> has a <one Membership> membership #30,
<Person> is a junior #30,
<Person> is an adult #30,
<Person> is a senior #30
Integer ::= <Person>'s base fee #60,
<Person>'s fee #60
Person p
Integer n
p is a junior <=> E[n](p is n years old & n<18)
p is an adult <=> E[n](p is n years old & n>=18 & n<65)
p is a senior <=> E[n](p is n years old & n>=65)
p's base fee == 50 if p is a junior, 120 if p is an adult, 84 if p is a senior
p's fee == p's base fee if p has a regular membership,
p's base fee - 20 if p has a weekday membership,
0 if p has a volunteer membership
// ---- given: the club's rules, its members, and the answers they should get ----
// Juniors (younger than 18) pay 50 euros, adults 120 euros, seniors (65 or older) 84 euros.
// Weekday members pay 20 euros less. Volunteers pay nothing.
fact Anna is 30 years old, Lisa is 16 years old, Henk is 70 years old, Kees is 12 years old,
Marie is 45 years old, Sven is 18 years old, Eva is 65 years old
fact Anna has a regular membership, Lisa has a weekday membership, Henk has a weekday membership,
Kees has a regular membership, Marie has a volunteer membership, Sven has a regular membership,
Eva has a regular membership, Piet has a weekday membership, Bram has a volunteer membership
Lisa is a junior ? [()][]
Henk is a junior ? [][()]
Sven is an adult ? [()][]
Eva is a senior ? [()][]
Anna is 31 years old ? [][()]
Anna has a weekday membership ? [][()]
Anna's fee ? [(120)][..]
Lisa's fee ? [(30)][..]
Henk's fee ? [(64)][..]
Kees's fee ? [(50)][..]
Marie's fee ? [(0)][..]
Sven's fee ? [(120)][..]
Eva's fee ? [(84)][..]
Piet's fee ? [..][..]
Bram's fee ? [(0)][..]
Train network
The timetable of a small rail network says which cities have a train between them. Turn it into a language that answers travel questions. A train runs both ways, a journey can go round the ring of cities, and "in at most 3 steps" counts down, so it ends. Groningen cannot be reached from Utrecht with these trains, but that is unknown, not false: the timetable may not list every train. Only "in at most 0 steps" is false: no journey takes zero steps.
Hints
- Declare
Citywith the nine cities. - Declare the sentences as
Booleanphrases with precedence#30: the fact sentence,<City> is connected to <City>,I can travel from <City> to <City>, and the same within at most <Integer> steps. - Declare the variables, including
Which. - A train runs both ways: two cities are connected when there is a train between them, in either order.
- You can travel from
atobwhenais connected tob, or when a citycconnected toacan reachb: a cyclic rule. - In at most
nsteps:nis more than 0, and a city connected toaisbor reachesbin at mostn-1steps. Writen>0 & ..., not... if n>0: a guard that fails makes a rule silent, so 0 steps would be unknown instead of false.
Write the language above the given part so that the facts parse and every question gets the expected answer.
import nelumbo.integers
City :: Object
City ::= Utrecht, Amsterdam, Haarlem, Leiden, Delft, Rotterdam, Gouda, Zwolle, Groningen
Boolean ::= there is a train between <City> and <City> #30,
<City> is connected to <City> #30,
I can travel from <City> to <City> #30,
I can travel from <City> to <City> in at most <Integer> steps #30
City a, b, c, Which
Integer n
a is connected to b <=> there is a train between a and b | there is a train between b and a
I can travel from a to b <=> a is connected to b | E[c](a is connected to c & I can travel from c to b)
I can travel from a to b in at most n steps <=> n>0 & E[c](a is connected to c & (c=b | I can travel from c to b in at most n-1 steps))
// ---- given: the timetable and the answers it should give ----
fact there is a train between Utrecht and Amsterdam, there is a train between Amsterdam and Haarlem,
there is a train between Haarlem and Leiden, there is a train between Leiden and Delft,
there is a train between Delft and Rotterdam, there is a train between Rotterdam and Gouda,
there is a train between Gouda and Utrecht, there is a train between Zwolle and Groningen
Utrecht is connected to Amsterdam ? [()][]
Amsterdam is connected to Utrecht ? [()][]
Which is connected to Utrecht ? [(Which=Amsterdam),(Which=Gouda),..][..]
Utrecht is connected to Leiden ? [..][..]
I can travel from Utrecht to Leiden ? [()][]
I can travel from Utrecht to Groningen ? [..][..]
I can travel from Utrecht to Which ? [(Which=Amsterdam),(Which=Delft),(Which=Gouda),(Which=Haarlem),(Which=Leiden),(Which=Rotterdam),(Which=Utrecht),..][..]
I can travel from Utrecht to Leiden in at most 3 steps ? [()][]
I can travel from Utrecht to Leiden in at most 2 steps ? [..][..]
I can travel from Utrecht to Delft in at most 3 steps ? [()][]
I can travel from Utrecht to Delft in at most 0 steps ? [][()]
Number toolkit
The projects so far read like English, but a Nelumbo language can just as well use mathematical
notation. Here you extend the integers with operators: factorial, power, remainder, greatest common
divisor, divisibility and primality. Precedence decides how a question groups: 2 * 3! is 12
and 2 + 10 mod 4 is 4, because ! binds tighter than *, and
mod as tightly as *. Power groups to the right, as in mathematics:
2^3^2 is 2^(3^2), so 512, not 64.
Hints
- Declare the operators on
Integer: the postfix<Integer> !with#70,<Integer> mod <Integer>with#50like*, andgcd(<Integer>, <Integer>), which needs no precedence because it does not start with a hole. - An operator groups to the left by default, so
2^3^2would be(2^3)^2. Hole precedences make it group to the right:<Integer#61> ^ <Integer#59> #60. The left operand cannot be a power itself, the right one can. - Write each function as a recursive function rule with
==andif:n!is 1 for 0 andn*(n-1)!above it, and power works the same way.a mod bsubtractsbuntil the rest is smaller thanb, andgcdis Euclid's algorithm:gcd(a,b)isgcd(b, a mod b)untilbis 0. d divides nis aBooleanoperator with#30: the remainder is 0.- A number above 1 is prime when no number from
n-1down to 2 divides it. Write a relationno divisor of <Integer> up to <Integer>that counts down to 1. It needs an explicitfalsebranch for a divisor: a guard that fails makes its branch silent, not false. Count down to the constant 1: a recursive relation whose guards compare two of its variables (d<n) runs into a known engine bug.
Write the language above the given part so that every question gets the expected answer.
import nelumbo.integers
Integer ::= <Integer> ! #70,
<Integer#61> ^ <Integer#59> #60,
<Integer> mod <Integer> #50,
gcd(<Integer>, <Integer>)
Boolean ::= <Integer> divides <Integer> #30,
prime <Integer> #30,
no divisor of <Integer> up to <Integer> #30
Integer n, k, a, b, d
n! == 1 if n=0, n*(n-1)! if n>0
a^k == 1 if k=0, a*a^(k-1) if k>0
a mod b == a if a<b, (a-b) mod b if a>=b
gcd(a,b) == a if b=0, gcd(b, a mod b) if b>0
d divides n <=> n mod d = 0
no divisor of n up to d <=> true if d<=1,
false if d>1 & d divides n,
no divisor of n up to d-1 if d>1 & !(d divides n)
prime n <=> n>1 & no divisor of n up to n-1
// ---- given: the questions and the answers they should get ----
5! ? [(120)][..]
0! ? [(1)][..]
2 * 3! ? [(12)][..]
2^10 ? [(1024)][..]
2^3^2 ? [(512)][..]
2^3 * 2 ? [(16)][..]
17 mod 5 ? [(2)][..]
2 + 10 mod 4 ? [(4)][..]
(2 + 10) mod 4 ? [(0)][..]
gcd(12, 18) ? [(6)][..]
gcd(17, 5) ? [(1)][..]
3 divides 12 ? [()][]
5 divides 12 ? [][()]
prime 2 ? [()][]
prime 97 ? [()][]
prime 91 ? [][()]
prime 1 ? [][()]
Set algebra
The sets of nelumbo.collections come with intersection &&, union
|| and difference -. Build new set operators on top of them: the symmetric
difference xor (the elements in exactly one of the two sets), disjoint,
multiplying every element by a number, and the sum set s + t: every sum of an element of
s and an element of t. One symbol can mean different things on different types:
+ adds integers but builds the sum set of two sets, and the types of the operands decide
which. An open set stays open: {1,2,..} xor {2} contains 1, and perhaps more.
Hints
- Declare the operators on
Set<Integer>, each with a precedence:xorwith#60like&&and||,*with#50and+with#40, as on integers. So{1,2} + {10} * 2is{1,2} + {20}. - Declare the variables: two sets and a few integers.
s xor tis a function rule over the built-in operators: what is insbut not int, together with what is intbut not ins.- The set-builder
{[n](...)}collects everynfor which the condition holds.s * kis the set of everynthat isi*kfor an elementiofs:{[n](E[i](i in s & n = i*k))}. The sum set works the same way with two elements. - Two sets are disjoint when their intersection is the empty set
{}.
Write the language above the given part so that every question gets the expected answer.
import nelumbo.collections
Set<Integer> ::= <Set<Integer>> xor <Set<Integer>> #60,
<Set<Integer>> + <Set<Integer>> #40,
<Set<Integer>> * <Integer> #50
Boolean ::= <Set<Integer>> disjoint <Set<Integer>> #30
Set<Integer> s, t
Integer i, j, k, n
s xor t == (s - t) || (t - s)
s * k == {[n](E[i](i in s & n = i*k))}
s + t == {[n](E[i,j](i in s & j in t & n = i+j))}
s disjoint t <=> s && t = {}
// ---- given: the questions and the answers they should get ----
{1,2,3} xor {2,3,4} ? [({1,4})][..]
{1,2} xor {2,1} ? [({})][..]
{1,2} disjoint {3,4} ? [()][]
{1,2} disjoint {2,3} ? [][()]
{1,2,3} * 10 ? [({10,20,30})][..]
{1,2} + {10,20} ? [({11,12,21,22})][..]
|{1,2,3} + {1,2,3}| ? [(5)][..]
{1,2} + {10} * 2 ? [({21,22})][..]
{1,2} + {0} = {1,2} ? [()][]
{1,2,..} xor {2} ? [({1,..})][..]
List statistics
Lists of numbers, the way a programmer handles them: the sum, maximum, mean and median of a list, and
the dot product of two lists. You build them from list indexing and recursion, and the questions
combine them with the library's map, where and sort, which take
a lambda such as [i](i*i). Division is exact, so mean [1,2] has no value: 3/2
is not an integer, and the facts side stays empty.
Hints
- Declare the functions on
Integerwith#45:sum <List<Integer>>, and the same formax,meanandmedian. The dot product is an operator,<List<Integer>> dot <List<Integer>>with#50. - Declare
Integer i, j: the given questions useiin their lambdas. - Recurse over an index with a helper such as
sum <List<Integer>> from <Integer>: 0 past the end (i >= |l|), otherwise the elementat i in lplus the sum from the next index. Writefrom (i+1): the helper binds tighter than+, sosum l from i+1is(sum l from i)+1. - At the last index (
|l|-1) the maximum is the element itself, before that the larger of the element and the maximum of the rest:(x > y then x else y). - The mean is the sum divided by the length. The median of a list of odd length is the middle element
of the sorted list:
l sort [i,j](i<j), at index(|l|-1)/2. - The dot product multiplies the elements at the same index and adds up the products: one more helper that recurses over an index.
Write the language above the given part so that every question gets the expected answer.
import nelumbo.collections
Integer ::= sum <List<Integer>> #45,
sum <List<Integer>> from <Integer> #45,
max <List<Integer>> #45,
max <List<Integer>> from <Integer> #45,
mean <List<Integer>> #45,
median <List<Integer>> #45,
<List<Integer>> dot <List<Integer>> #50,
products of <List<Integer>> and <List<Integer>> from <Integer> #45
List<Integer> l, m
Integer i, j
sum l from i == 0 if i >= |l|, (at i in l) + sum l from (i+1) if i < |l|
sum l == sum l from 0
max l from i == at i in l if i = |l|-1,
(at i in l > max l from (i+1) then at i in l else max l from (i+1)) if i < |l|-1
max l == max l from 0
mean l == (sum l) / (|l|)
median l == at ((|l|-1)/2) in (l sort [i,j](i<j))
products of l and m from i == 0 if i >= |l|, (at i in l) * (at i in m) + products of l and m from (i+1) if i < |l|
l dot m == products of l and m from 0
// ---- given: the questions and the answers they should get ----
sum [3,4,5] ? [(12)][..]
sum [] ? [(0)][..]
max [3,9,4] ? [(9)][..]
mean [2,4,9] ? [(5)][..]
mean [1,2] ? [][..]
median [7,1,5] ? [(5)][..]
[1,2,3] dot [4,5,6] ? [(32)][..]
sum ([1,2,3] map [i](i*i)) ? [(14)][..]
max ([3,9,4] where [i](i<5)) ? [(4)][..]
Vectors
Not every value is a number or a name. Here a vector such as vec(1,2) is a value of its own:
two vectors are equal exactly when their parts are, and a vector can be an answer. Its operators are
functions that compute a new vector: vec(1,2) + vec(3,4) is vec(4,6). That takes
two kinds of declarations: the constructor vec is a struct, the operators are functions. A vector
with a computed part is computed first, so vec(1+1, 2) equals vec(2, 2).
Hints
- Declare
Vec :: Object, and only the constructor as a struct:struct Vec ::= vec(<Integer>, <Integer>), the way the library declares its set and list literals. The operators are declared withoutstruct, so they are functions that compute a vector. - Declare the operators with the precedences of their integer counterparts:
Vec ::= <Vec> + <Vec> #40, the same for-, and<Integer> * <Vec> #50. The dot product isInteger ::= <Vec> . <Vec> #50, and<Vec> is perpendicular to <Vec>is aBooleanphrase with#30. The types of the operands tell2 * vec(1,2)apart from2 * 3. - Write a function rule per operator, with a head that takes the vectors apart:
vec(a,b) + vec(c,d) == vec(a+c, b+d). - Two vectors are perpendicular when their dot product is 0.
Write the language above the given part so that every question gets the expected answer.
import nelumbo.integers
Vec :: Object
struct Vec ::= vec(<Integer>, <Integer>)
Vec ::= <Vec> + <Vec> #40,
<Vec> - <Vec> #40,
<Integer> * <Vec> #50
Integer ::= <Vec> . <Vec> #50
Boolean ::= <Vec> is perpendicular to <Vec> #30
Vec u, v
Integer a, b, c, d, k
vec(a,b) + vec(c,d) == vec(a+c, b+d)
vec(a,b) - vec(c,d) == vec(a-c, b-d)
k * vec(a,b) == vec(k*a, k*b)
vec(a,b) . vec(c,d) == a*c + b*d
u is perpendicular to v <=> u . v = 0
// ---- given: the questions and the answers they should get ----
vec(1,2) + vec(3,4) ? [(vec(4,6))][..]
vec(1,2) - vec(3,4) ? [(vec(-2,-2))][..]
2 * vec(1,2) ? [(vec(2,4))][..]
vec(1,2) + 2 * vec(1,1) ? [(vec(3,4))][..]
vec(1,2) . vec(3,4) ? [(11)][..]
vec(1+1, 2) = vec(2, 2) ? [()][]
vec(1,2) + vec(3,4) = vec(4,6) ? [()][]
vec(1,2) = vec(2,1) ? [][()]
vec(2,1) is perpendicular to vec(-1,2) ? [()][]
vec(2,1) is perpendicular to vec(1,2) ? [][()]
Expression interpreter
A language inside your language: arithmetic expressions with variables, and a function eval
that computes their value. An expression is a value, a tree of parts: x + #2 equals
x + #2 but not #2 + x, although both evaluate to 5. eval is a
function over those trees, with one rule per kind of expression. The variables get their values from
facts, and a variable without a value gives an unknown result, not an error.
Hints
- Declare
Expr :: Object, andVar :: Exprwith the constantsx,yandz. - Declare the other expressions as structs so that an expression is a value:
struct Expr ::= # <Integer> #90, <Expr> + <Expr> #40, <Expr> * <Expr> #50. The types of the operands tellx + #2apart from3 + 2. - Declare
eval <Expr>as anIntegerfunction with#80, and<Var> := <one Integer>as aBooleanphrase with#20: a variable has one value. - Write one rule per kind of expression, with a head that takes it apart:
eval (e + f) == eval e + eval f. A variable's value comes from its fact:eval v = i <=> v := i.
Write the language above the given part so that the facts parse and every question gets the expected answer.
import nelumbo.integers
Expr :: Object
Var :: Expr
Var ::= x, y, z
struct Expr ::= # <Integer> #90,
<Expr> + <Expr> #40,
<Expr> * <Expr> #50
Integer ::= eval <Expr> #80
Boolean ::= <Var> := <one Integer> #20
Expr e, f
Var v
Integer i
eval #i == i
eval v = i <=> v := i
eval (e + f) == eval e + eval f
eval (e * f) == eval e * eval f
// ---- given: the variables, and the questions with the answers they should get ----
fact x := 3, y := 4
eval #5 ? [(5)][..]
eval x ? [(3)][..]
eval (x + #2) ? [(5)][..]
eval (x + y * #2) ? [(11)][..]
eval ((x + y) * #2) ? [(14)][..]
eval (z + #1) ? [..][..]
x + #2 = x + #2 ? [()][]
x + #2 = #2 + x ? [][()]
Room booking
Today's bookings for a few meeting rooms are given with times like 9:30. Answer whether a
room is free, at a time or between two times, and whether a room's bookings clash. "Free" is the hard
part. In an open world you can prove that a room is taken: you find the booking. Proving that it is free requires
every booking for the room, so each room's bookings are given as one complete set. Yellow has
no set, so whether it is free stays unknown.
Hints
- A time is a named pattern of connected tokens, so
9:30parses and9 : 30does not:pattern TIME ::= <[> <Integer#100> : <Integer#100> <]>. In a phrase,<TIME>gives two Integer arguments, hours and minutes; compare times in minutes,h*60+m. - A booking is a value, not a fact: declare
Slot :: Objectand astruct Slotwith the pattern<TIME> to <TIME>(precedence#35), andthe bookings for <Room>as a function that returns aSet<Slot>(precedence#60). - Because each set is complete,
A[b]((b in the bookings for r) -> ...)can be true. - For relations about a slot, such as
<Integer> falls in <Slot>, let the rule head take the slot apart:n falls in h1:m1 to h2:m2 <=> .... - A room is free between two times when it is free at the first, and no booking starts at or after the first and before the second.
- Bookings clash when one starts during another. A booking does not clash with itself
(
!=).
Write the language above the given part so that the facts parse and every question gets the expected answer.
import nelumbo.collections
pattern TIME ::= <[> <Integer#100> : <Integer#100> <]>
Slot :: Object
struct Slot ::= <TIME> to <TIME> #35
Room :: Object
Room ::= Blue, Red, Green, Yellow
Set<Slot> ::= the bookings for <Room> #60
Boolean ::= <Integer> falls in <Slot> #30,
<Integer> is the start of <Slot> #30,
<Room> is free at <TIME> #30,
<Room> is free between <TIME> and <TIME> #30,
<Room> has clashing bookings #30
Integer h1, m1, h2, m2, n
Slot b, c
Room r
n falls in h1:m1 to h2:m2 <=> h1*60+m1 <= n & n < h2*60+m2
n is the start of h1:m1 to h2:m2 <=> n = h1*60+m1
r is free at h1:m1 <=> A[b]((b in the bookings for r) -> !(h1*60+m1 falls in b))
r is free between h1:m1 and h2:m2 <=> r is free at h1:m1 &
A[b]((b in the bookings for r) -> !E[n](n is the start of b & h1*60+m1 <= n & n < h2*60+m2))
r has clashing bookings <=> E[b,c](b in the bookings for r & c in the bookings for r & b != c &
E[n](n is the start of c & n falls in b))
// ---- given: today's bookings, complete per room, and the answers they should give ----
fact the bookings for Blue = {9:30 to 11:00, 13:00 to 14:30}
fact the bookings for Red = {9:00 to 10:30, 10:00 to 11:00}
fact the bookings for Green = {}
Blue is free at 10:00 ? [][()]
Blue is free at 12:00 ? [()][]
Blue is free at 11:00 ? [()][]
Blue is free between 11:00 and 13:00 ? [()][]
Blue is free between 10:30 and 12:00 ? [][()]
Blue is free between 12:00 and 15:00 ? [][()]
Green is free at 10:00 ? [()][]
Yellow is free at 10:00 ? [..][..]
Red has clashing bookings ? [()][]
Blue has clashing bookings ? [][()]
Access policy
A policy language for who may do what. A dictionary lists every role with its article, and the policy
is written as plain statements: every manager may approve invoices, no intern may
approve invoices. Transformations turn every statement into a fact or a rule. Allow rules fill the
facts side of an answer, deny rules the falsehood side. Where no policy applies, the answer is unknown,
not denied: nothing says whether Eva, an auditor, may approve invoices. When you are done, add
Ann is a manager: she is then both allowed and denied, and Nelumbo reports the
inconsistency instead of picking one.
Hints
- Declare the types
Person,Role,ActionandItemwith their constants, and anArticletype withaandan. - Only
<Person> may <Action> <Item>is a question: aBooleanphrase with precedence#30. The other lines are statements:Rootphrases with precedence#0, androle <Article> <Role>with#100. - Store a person's roles in a relation of your own, for example a private
<Person> has role <Role>. - A dictionary entry such as
role an internis transformed, as in Controlled English: it declares the sentences with that article,<Person> is an internandevery <Role> is an intern, and transformations for them. The first adds a fact, the second a rule: whoever has the one role has the other. - A policy statement is transformed into a rule:
every r may x iintoq may x i <=> true if q has role r,no r may x iinto the same withfalse. - In a transformation, use variables such as
{Role,Literal} rin the heads, and give every functor declared in a transformation body at least one hole (a known engine bug).
Write the language above the given part so that the dictionary, the policy and the staff parse and every question gets the expected answer.
import nelumbo.logic
Person :: Object
Person ::= Jan, Ann, Eva, Bob
Article :: Object
Article ::= a, an
Role :: Object
Role ::= manager, intern, employee, auditor
Action :: Object
Action ::= approve, read, sign
Item :: Object
Item ::= invoices, contracts
Boolean ::= <Person> may <Action> <Item> #30
private Boolean ::= <Person> has role <Role> #30
Root ::= role <Article> <Role> #100,
every <Role> may <Action> <Item> #0,
no <Role> may <Action> <Item> #0
{
Article ar
{Role,Literal} r, s
{Action,Literal} x
{Item,Literal} i
{Person,Literal} p
Person q
role ar r ::> {
Root ::= <{Person,Literal}> is <ar> <r> #0
Root ::= every <{Role,Literal}> is <ar> <r> #0
p is ar r ::> {
fact p has role r
}
every s is ar r ::> {
q has role r <=> true if q has role s
}
}
every r may x i ::> {
q may x i <=> true if q has role r
}
no r may x i ::> {
q may x i <=> false if q has role r
}
}
Person Who
// ---- given: the dictionary, the policy, the staff, and the answers they should give ----
role a manager
role an intern
role an employee
role an auditor
every manager is an employee
every intern is an employee
every employee may read contracts
every manager may approve invoices
no intern may approve invoices
no intern may sign contracts
every auditor may read invoices
Jan is a manager
Ann is an intern
Eva is an auditor
Jan may approve invoices ? [()][]
Ann may approve invoices ? [][()]
Eva may approve invoices ? [..][..]
Ann may read contracts ? [()][]
Eva may read contracts ? [..][..]
Ann may sign contracts ? [][()]
Jan may sign contracts ? [..][..]
Who may approve invoices ? [(Who=Jan),..][(Who=Ann),..]
Who may read contracts ? [(Who=Ann),(Who=Jan),..][..]