Skip to content

Commit f5b6b14

Browse files
committed
Improved handling of epsilon productions
1 parent b4e615c commit f5b6b14

File tree

3 files changed

+76
-9
lines changed

3 files changed

+76
-9
lines changed

CHANGELOG.txt

+12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
Changelog
22
=========
33

4+
v1.10 (2025-03-27)
5+
------------------
6+
7+
- Minor cleanups and improvements.
8+
- Added Production.is_epsilon method.
9+
- Added Productions.has_epsilon_productions method.
10+
- Added Grammar.has_epsilon_productions method.
11+
- Improved TopDownInstantaneousDescription and BottomUpInstantaneousDescription to handle
12+
epsilon productions.
13+
- Added Tree.__eq__ and Tree.__hash__ methods (to allow Stack of trees comparison, required
14+
to avoid loops in simulations of automata combinations).
15+
416
v1.9.1 (2025-03-24)
517
-------------------
618

src/liblet/automaton.py

+56-9
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,16 @@ def is_done(self):
294294
return self.top() == self.head() == HASH
295295

296296
def match(self):
297-
"""Attempts a match move and returns the corresponding new instantaneous description."""
297+
"""Performs a match move.
298+
299+
If the top of the stack is ε, it will be removed and the tape head will be left unchanged.
300+
301+
Raises:
302+
ValueError: If the top of the stack and tape head symbol are not equal.
303+
Returns:
304+
A new updated instantaneous description.
305+
"""
306+
# the stack and tape can never be empty
298307
if (self.top() == ε) or (self.top() in self.G.T and self.top() == self.head()):
299308
c = copy(self)
300309
c.stack = copy(c.stack)
@@ -305,14 +314,24 @@ def match(self):
305314
raise ValueError('The top of the stack and tape head symbol are not equal.')
306315

307316
def predict(self, P):
308-
"""Attempts a prediction move, given the specified production, and returns the corresponding new instantaneous description."""
317+
"""Performs a prediction move, given the specified production.
318+
319+
If the production is an epsilon production, the stack will be left unchanged.
320+
321+
Args:
322+
P (:class:`~liblet.grammar.Production`): The production to predict.
323+
Raises:
324+
ValueError: If the production does not belong to the grammar, or if the lhs does not correspond to the top of the stack.
325+
Returns:
326+
A new updated instantaneous description.
327+
"""
309328
if P in self.G.P and self.top() == P.lhs:
310329
c = copy(self)
311330
c.stack = copy(c.stack)
312331
c.stack.pop()
313332
c.steps += (P,)
314-
for X in reversed(P.rhs):
315-
if ε != X:
333+
if not P.is_epsilon():
334+
for X in reversed(P.rhs):
316335
c.stack.push(X)
317336
return c
318337
raise ValueError('The top of the stack does not correspond to the production lhs.')
@@ -337,17 +356,45 @@ def is_done(self):
337356
"""Returns `True` if the computation is done, that is if the stack contains the a tree rooted in G.S and the head is at the tape end."""
338357
return len(self.stack) == 1 and len(self.tape) == self.head_pos and self.top() == self.G.S
339358

340-
def shift(self):
341-
"""Performs a shift move and returns the corresponding new instantaneous description."""
359+
def shift(self, consume=True):
360+
"""Performs a shift move.
361+
362+
If the top of the stack is ε, it will be removed before shifting.
363+
364+
Args:
365+
consume (bool): If `True`, the head position is increased so that the symbol under the tape head is consumed.
366+
Otherwise, the head does not move and an ε is pushed. The default is `True`.
367+
368+
Returns:
369+
A new updated instantaneous description.
370+
"""
342371
c = copy(self)
343-
c.stack.push(Tree(c.head()))
344-
c.head_pos += 1
372+
if self.stack and self.top() == ε:
373+
c.stack.pop()
374+
if consume:
375+
if self.head_pos < len(self.tape):
376+
c.stack.push(Tree(c.head()))
377+
c.head_pos += 1
378+
else:
379+
raise ValueError('The head is already at the end of the tape.')
380+
else:
381+
c.stack.push(Tree(ε))
345382
return c
346383

347384
def reduce(self, P):
348-
"""Attempts a reduce move, given the specified production, and returns the corresponding new instantaneous description."""
385+
"""Performs a reduce move, given the specified production.
386+
387+
Args:
388+
P (:class:`~liblet.grammar.Production`): The production to reduce.
389+
Raises:
390+
ValueError: If the production does not belong to the grammar, if the rhs does not correspond to the symbols on the stack or the stack is empty.
391+
Returns:
392+
A new updated instantaneous description.
393+
"""
349394
if P not in self.G.P:
350395
raise ValueError('The production does not belong to the grammar.')
396+
if not self.stack:
397+
raise ValueError('The stack is empty.')
351398
c = copy(self)
352399
children = [c.stack.pop() for _ in P.rhs][::-1]
353400
if tuple(t.root for t in children) != P.rhs:

src/liblet/grammar.py

+8
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ def from_string(cls, prods, context_free=True):
191191
P.extend(Production(lhs, tuple(rh.split())) for rh in rha.split('|'))
192192
return cls(P)
193193

194+
def has_epsilon_productions(self):
195+
"""Returns `True` if the right-hand side of any of the production is ``(ε,)``."""
196+
return any(P.is_epsilon() for P in self)
197+
194198
def _repr_html_(self): # pragma: no cover
195199
from liblet.display import liblet_table
196200

@@ -383,6 +387,10 @@ def alternatives(self, N):
383387
"""
384388
return (P.rhs for P in self.P if P.lhs == N)
385389

390+
def has_epsilon_productions(self):
391+
"""Returns `True` if the grammar has epsilon productions."""
392+
return self.P.has_epsilon_productions()
393+
386394
def restrict_to(self, symbols):
387395
"""Returns a grammar using only the given symbols.
388396

0 commit comments

Comments
 (0)