I find the problem particularly challenging because there are so many combinations and I have no clue these how to exhaustively enumerate all possibilities.
As with most grammars, an infinite set of sentences can be produced. But there really aren't very many combinations of any given size, because:
- The grammar is linear (no production has more than one non-terminal)
- There are only two productions.
The simple way to enumerate all possible sentences is to a breadth-first search over the possible leftmost derivations, starting with a sentential form consisting only of the start symbol. (You could use rightmost derivations, but leftmost seems more natural.)
That's a very simple operation, particularly for a computer, although in a grammar this simple, it's easy to do by hand:
0 S start symbol
1 1 in (0) replace S->1
2 S 0 in (0) replace S->S 0
3 1 0 in (2) replace S->1
4 S 0 0 in (2) replace S->S 0
...
Here's a bit of Javascript which can generate sentences (or sentential forms if the all
argument is true
) for arbitrary context-free grammars. (The grammar parser is primitive, and still lacks error messages. But it works on correctly entered grammars. I might work on it some more tomorrow.)
function Grammar(s) {
let tokens = /(\w+|"[^"]+"|'[^']*')|(:)|([|;])|(\S)/g
let which = m => m.findIndex((v, i)=>v&&i)
let grammar = new Map()
grammar.start = undefined
let put = (lhs, rhs) => {
if (grammar.has(lhs)) grammar.get(lhs).push(rhs)
else grammar.set(lhs, [rhs])
}
let lhs = undefined
let rhs = []
for (let m of s.matchAll(tokens)) {
switch (which(m)) {
case 1: rhs.push(m[1]); break; /* Symbol */
case 2: if (lhs && rhs.length)
put(lhs, rhs)
else if (!lhs && rhs.length == 1) {
if (!grammar.start)
grammar.start = rhs[0]
}
else break;
lhs = rhs.pop()
rhs = []
break;
case 3: if (lhs) {
put(lhs, rhs)
rhs = []
if (m[3] == ';') lhs = undefined
}
break
case 4: break
}
}
if (lhs) put(lhs, rhs);
return grammar
}
let grammar = Grammar(`
expr: atom
| expr '+' expr
| expr '*' expr
| '-' expr
atom: NUM
| '(' expr ')'
`)
function* derivationGenerator(grammar, all) {
level = [ [grammar.start] ]
while (level.length) {
nextLevel = []
for (const form of level) {
let idx = form.findIndex(sym => grammar.has(sym))
if (idx < 0 || all) yield form
if (idx >= 0) {
let before = form.slice(0, idx)
let after = form.slice(idx+1)
grammar.get(form[idx]).forEach(
rhs => nextLevel.push(before.concat(rhs, after)))
}
}
level = nextLevel
}
}
function take(gen, n, handle) {
for (let v of gen) {
handle(v)
if (--n <= 0) return gen
}
}
take(derivationGenerator(grammar), 10, form => console.log(...form))