Skip to content

Commit

Permalink
Recursively check for epsilon closure states (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeylemon committed Mar 15, 2024
1 parent 76046f7 commit 0ac6c95
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 92 deletions.
19 changes: 14 additions & 5 deletions src/js/fsa/fsa.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { UnknownStateError, UnknownSymbolError } from '../util/errors.js'
import { removeDuplicates } from '../util/array.js'

export default class FSA {
/**
Expand Down Expand Up @@ -114,14 +115,23 @@ export default class FSA {
* @param {String} fromState The label of the state to find epsilon-reachable states from
* @returns {Array} The array of states that can be reached via an ε-transition
*/
getEpsilonClosureStates (fromState) {
getEpsilonClosureStates (fromState, checkedStates = []) {
if (!this.states.includes(fromState)) throw new UnknownStateError(fromState)

// Ensure fromState has any epsilon transitions
if (!this.transitions[fromState] || !this.transitions[fromState]['ε']) {
return [fromState]
} else {
return [fromState, ...this.transitions[fromState]['ε']]
}

const toStates = this.transitions[fromState]['ε']

// Recursively check for epsilon transitions
// Avoid infinite loop by omitting already-checked states
const allEpsilonClosureStates = toStates
.filter(s => checkedStates.indexOf(s) === -1)
.map(s => this.getEpsilonClosureStates(s, [...checkedStates, ...toStates]))

return removeDuplicates([fromState, ...allEpsilonClosureStates].flat())
}

/**
Expand Down Expand Up @@ -151,7 +161,6 @@ export default class FSA {
}
}

// Remove duplicate entries by spreading a set
return [...new Set(list)].sort()
return removeDuplicates(list)
}
}
9 changes: 4 additions & 5 deletions src/js/fsa/nfa_converter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import FSA from './fsa.js'
import { removeDuplicates } from '../util/array.js'

export default class NFAConverter {
/**
Expand Down Expand Up @@ -83,8 +84,7 @@ export default class NFAConverter {
list = this.getUnreachableStates(tempDFA, list.concat(nodesWithoutIncomingEdges))
}

// Remove duplicates from the list by spreading it as a Set
return [...new Set(list)]
return removeDuplicates(list)
}

/**
Expand Down Expand Up @@ -160,7 +160,7 @@ export default class NFAConverter {

// The new start state is the states that are reachable from the original start state
// e.g. '1' has an ε-transition to '3'; therefore, the new start state is '1,3'
const startState = [...new Set(this.nfa.getEpsilonClosureStates(this.nfa.startState))].sort().join(',')
const startState = removeDuplicates(this.nfa.getEpsilonClosureStates(this.nfa.startState)).join(',')

// The new list of accept states are any states from the powerset with the original accept state in them
// e.g. '1' is the accept state; therefore, '1', '1,2', '1,3', and '1,2,3' are accept states
Expand Down Expand Up @@ -208,8 +208,7 @@ export default class NFAConverter {
reachableStates = reachableStates.concat(this.nfa.getReachableStates(s, symbol))
})

// Remove any duplicates and sort the states alphabetically
reachableStates = [...new Set(reachableStates)].sort()
reachableStates = removeDuplicates(reachableStates)

// Remove Ø if the state has other possibilites
if (reachableStates.some(e => e !== 'Ø')) {
Expand Down
7 changes: 4 additions & 3 deletions src/js/fsa/visual_fsa.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import EventHandler from '../util/event_handler.js'
import { UnknownStateError } from '../util/errors.js'
import { removeDuplicates } from '../util/array.js'
import FSA from './fsa.js'
import Location from '../canvas/location.js'
import EditNodeMenu from '../elements/edit_node_menu.js'
Expand Down Expand Up @@ -234,7 +235,7 @@ export default class VisualFSA extends EventHandler {
if (symbol !== 'ε') { alphabet.push(symbol) }
}
}
this.fsa.alphabet = [...new Set(alphabet)].sort()
this.fsa.alphabet = removeDuplicates(alphabet)
}

/**
Expand All @@ -259,8 +260,8 @@ export default class VisualFSA extends EventHandler {
fromNode.transitionText[to].push(symbol)

// Remove duplicates in case the user somehow added two of the same transitions
fromNode.transitionText[to] = [...new Set(fromNode.transitionText[to])].sort()
this.fsa.transitions[from][symbol] = [...new Set(this.fsa.transitions[from][symbol])].sort()
fromNode.transitionText[to] = removeDuplicates(fromNode.transitionText[to])
this.fsa.transitions[from][symbol] = removeDuplicates(this.fsa.transitions[from][symbol])

this.updateAlphabet()
this.dispatchEvent('change')
Expand Down
10 changes: 5 additions & 5 deletions src/js/test/fsa.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('FSA 1', () => {
'3': {
'a': ['1'],
'b': undefined,
'ε': undefined
'ε': ['2']
}
}, '1', ['1'])

Expand All @@ -29,18 +29,18 @@ describe('FSA 1', () => {
})

it('should get epsilon closure states', done => {
fsa.getEpsilonClosureStates('1').should.eql(['1', '3'])
fsa.getEpsilonClosureStates('1').should.eql(['1', '2', '3'])
fsa.getEpsilonClosureStates('2').should.eql(['2'])
fsa.getEpsilonClosureStates('3').should.eql(['3'])
fsa.getEpsilonClosureStates('3').should.eql(['2', '3'])
done()
})

it('should get reachable states', done => {
fsa.getReachableStates('1', 'a').should.eql(['Ø'])
fsa.getReachableStates('1', 'b').should.eql(['2'])
fsa.getReachableStates('2', 'a').should.eql(['2', '3'])
fsa.getReachableStates('2', 'b').should.eql(['3'])
fsa.getReachableStates('3', 'a').should.eql(['1', '3'])
fsa.getReachableStates('2', 'b').should.eql(['2', '3'])
fsa.getReachableStates('3', 'a').should.eql(['1', '2', '3'])
fsa.getReachableStates('3', 'b').should.eql(['Ø'])
done()
})
Expand Down
74 changes: 0 additions & 74 deletions src/js/test/visual_fsa.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,78 +165,4 @@ describe('Visual FSA', () => {

done()
})

it('should step backward in the DFA conversion', done => {
const visualNFA = new VisualFSA(new DraggableCanvas('#nfa'), false)

visualNFA.addNode('1', new Location(100, 100))
visualNFA.addNode('2', new Location(100, 100))
visualNFA.setStartState('1')
visualNFA.addAcceptState('1')
visualNFA.addTransition('1', '2', 'b')
visualNFA.addTransition('1', '1', 'a')
visualNFA.addTransition('2', '1', 'a')
visualNFA.addTransition('2', '2', 'b')

const visualDFA = new VisualFSA(new DraggableCanvas('#dfa'), true)
const converter = new NFAConverter(visualNFA.fsa)

for (let i = 0; i < 10; i++) {
const [newDFA, step] = converter.stepForward()
visualDFA.performStep(step, newDFA)
}

visualDFA.fsa.states.should.eql(['1', '2', '1,2'])
visualDFA.fsa.startState.should.eql('1')
visualDFA.fsa.acceptStates.should.eql(['1', '1,2'])
visualDFA.fsa.transitions.should.eql({
'1': { a: ['1'], b: ['2'] },
'2': { a: ['1'], b: ['2'] },
'1,2': { a: ['1'], b: ['2'] }
})

{
const [prevDFA, step] = converter.stepBackward()
visualDFA.undoStep(step, prevDFA)
}

visualDFA.fsa.states.should.eql(['1', '2', '1,2', 'Ø'])
visualDFA.fsa.startState.should.eql('1')
visualDFA.fsa.acceptStates.should.eql(['1', '1,2'])
visualDFA.fsa.transitions.should.eql({
'Ø': { a: ['Ø'], b: ['Ø'] },
'1': { a: ['1'], b: ['2'] },
'2': { a: ['1'], b: ['2'] },
'1,2': { a: ['1'], b: ['2'] }
})

for (let i = 0; i < 2; i++) {
const [newDFA, step] = converter.stepForward()
visualDFA.performStep(step, newDFA)
}

visualDFA.fsa.states.should.eql(['1', '2'])
visualDFA.fsa.startState.should.eql('1')
visualDFA.fsa.acceptStates.should.eql(['1'])
visualDFA.fsa.transitions.should.eql({
'1': { a: ['1'], b: ['2'] },
'2': { a: ['1'], b: ['2'] }
})

{
const [prevDFA, step] = converter.stepBackward()
visualDFA.undoStep(step, prevDFA)
}

visualDFA.fsa.states.should.eql(['1', '2', '1,2'])
visualDFA.fsa.startState.should.eql('1')
visualDFA.fsa.acceptStates.should.eql(['1', '1,2'])
visualDFA.fsa.transitions.should.eql({
'1': { a: ['1'], b: ['2'] },
'2': { a: ['1'], b: ['2'] },
'1,2': { a: ['1'], b: ['2'] }
})

done()
})
})
9 changes: 9 additions & 0 deletions src/js/util/array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Remove duplicate elements from the array and sort it
*
* @param {Array} arr The array to remove duplicate elements from
* @returns {Array} The deduplicated array
*/
export function removeDuplicates (arr) {
return [...new Set(arr)].sort()
}

0 comments on commit 0ac6c95

Please sign in to comment.