CS374: Principles of Programming Languages - Lab: Finite Automata Simulators (15 Points)
Contents
Purpose, Task, and Criteria
Purpose: To build general simulators for deterministic finite automata (DFAs) and nondeterministic finite automata (NFAs) that read machine definitions from data files, so the theory beneath every lexer becomes a program you can run, and to build Thompson's construction once by hand.
Task: Individually or with a partner, build DFA and NFA simulators that read machines from JSON, design one machine of each kind, and build one Thompson's construction NFA by hand.
Criteria: I grade correct simulators that handle the stated edge cases, two annotated machine designs, and a by-hand Thompson construction. The rubric below breaks this down in full.
Assignment Goals
The goals of this assignment are:
- To implement general DFA and NFA simulators over machine definitions loaded from JSON
- To design one DFA and one NFA for specified languages and encode them as data
- To trace a small subset construction and build a Thompson's construction NFA by hand
- To connect automata to the regular expressions and lexer of the surrounding course
Background Reading and References
Please refer to the following readings and examples offering templates to help get you started:
- Finite Automata Activity
- Grammars and the Chomsky Hierarchy Activity
- FSM Simulator (step a DFA, NFA, or epsilon-NFA one symbol at a time; it writes epsilon as $)
- FSM2Regex (convert a regular expression to an automaton and back)
- Automata Studio (NFA to DFA by subset construction with the full subset table, and DFA minimization)
The Assignment
In this lab you write two programs that run finite automata: a DFA simulator and an NFA simulator. Each one reads a machine from a JSON file, so a single program runs any machine you or I give it. These machines sit underneath every lexer, including the one you build next. You also design one DFA and one NFA, and you build one classic construction by hand. Parts 0 and 3 are paper work; Parts 1 and 2 are code.
Pair policy. You may work in pairs or alone. A pair can share one screen, or split the DFA and NFA halves and review each other’s work. Both partners submit the same ZIP, name each other in the writeup, and earn the same grade. This lab needs no individual-work certification. The reflection asks who did what instead.
Background in the Class Notes
The class notes cover every idea this lab uses, with a worked example of each. Read these before Part 0, and come back when a step points to one:
- Python for Language Work: dictionaries, sets, f-strings, JSON, classes, and exceptions, if you have not used them
- What a DFA is, and how to trace one
- A DFA as JSON, and in Python
- Designing a DFA by asking what it must remember, with a worked machine
- What an NFA is
- Reading an NFA’s transitions
- Python’s
setandfrozenset - One step of an NFA: move, then close
- The ε-closure
- The subset construction, and with ε-moves and a dead state
- Thompson’s construction
Part 0: Before You Start - Regular Expressions and Finite Automata
Do this part on paper before you write any code. You may do it alone, even if you do the rest of the lab with a partner. A regular expression and a finite automaton are two ways to describe the same set of strings. Building both for one language is the fastest way to see that they agree.
Step 0.1: Write a Regular Expression and a Matching NFA
You write (on paper, then in writeup.md under a ## Part 0 heading): a regular expression, a drawn NFA, and four test strings. No code.
Example. An identifier is a letter followed by any number of letters or digits. Let L stand for any letter and D for any digit. The regex is L(L|D)*, and this two-state NFA accepts the same language:
On x1, the machine moves from q0 to q1 on x and loops on 1, so it accepts. On 1x, q0 has no arrow for a digit, so it rejects. The regex agrees, because 1x does not start with a letter. Choose a different token class for your own work.
Do this.
- Pick a token class, such as floating-point literals, integer literals with an optional sign, or string literals.
- Write a regular expression for it.
- Draw an NFA that accepts the same language. Mark the start state with an incoming arrow and each accepting state with a double circle.
- Test both on two strings the class should accept and two it should not. The regex and the NFA must agree on all four.
Step 0.2: Convert a Small NFA to a DFA by Hand
You write (on paper, then in writeup.md under the same ## Part 0 heading): a partial subset-construction table and one accepted string. No code.
The subset construction makes one DFA state for each set of NFA states the machine could be in at once. The class notes work a small example. You trace it in full in Part 3, so a short first pass now pays off twice.
Use this NFA over {a, b}. It accepts strings whose second-to-last symbol is a. It starts in q0, accepts in q2, and has no ε-moves. On each a, q0 may guess that this is the second-to-last symbol:
| state | on a |
on b |
|---|---|---|
| q0 | {q0, q1} | {q0} |
| q1 | {q2} | {q2} |
| q2 | ∅ | ∅ |
Here is the first row, worked, to show what each cell asks for. Start at {q0}. On a, the set holds only q0, and \(\delta(q0, a) = \{q0, q1\}\), so the cell is {q0, q1}. On b, \(\delta(q0, b) = \{q0\}\), so the cell is {q0}. The new set {q0, q1} is the next row to process. In that row, each cell is the union over both states. On a, for example, it is \(\delta(q0, a) \cup \delta(q1, a)\).
Do this.
- Copy the NFA and its table into your notes.
- Continue the subset construction from
{q0, q1}until no new sets appear, or for at least two or three input symbols. Write each DFA state as its full set of NFA states. For each cell, write the union you took, in the formq0 -> {...} ∪ q1 -> {...}.- Name one string the resulting DFA accepts.
- If the sets stopped being obvious at some step, circle that step and write one line about what got hard.
Bring to class. Bring your construction even if it stalled, with the stalling step marked. The stall is useful, because Part 2 has you automate that step. When you assemble your submission, put this page in
writeup.mdunder a Part 0 heading. A photo of the paper is fine.
Getting Started
You need Python 3.10 or newer, a terminal, and an editor such as VS Code. The lab uses only the standard library, so there is nothing to install. Keep your Part 0 work nearby so you have a machine in mind when you meet the JSON format. If the terminal is new to you, read the dev environment tutorial and the shell primer first. Together they cover every command on this page.
Check your Python version:
python3 --version
Python 3.11.4
Any version from 3.10 up works. If the shell cannot find python3, try python --version, and use whichever name works for the rest of this page. Next, create a project folder with a machines/ folder inside it, and move into it:
mkdir -p cs374-automata/machines
cd cs374-automata
Open the folder in your editor and create one empty file at the top level, writeup.md. It holds your Part 0 work, your test strings, your Thompson NFA, and your reflection. You create the two Python files when the steps ask for them, and each machine goes in machines/ as its own JSON file. This table shows which file each step writes, so you always know where new code goes:
| Step | File you work in | What you do there |
|---|---|---|
| 0.1, 0.2 | writeup.md |
add your Part 0 paper work |
| 1.1 | dfa_core.py |
create a ten-line DFA runner, then leave it alone |
| 1.1, 1.4 | machines/even_ones.json, machines/ends_in_ab.json |
create the DFA machine files |
| 1.2.1 to 1.2.5 | simulator.py |
create the file, write the three checker helpers one at a time, then integrate them into the loader |
| 1.3.1 to 1.3.4 | simulator.py |
add run_dfa one rule at a time, then main |
| 2.1, 2.3, 2.4 | machines/ends_in_ab_nfa.json, machines/ab_optional.json, machines/eps_cycle.json, machines/contains_aa.json |
create the NFA machine files |
| 2.2.1, 2.2.2 | simulator.py |
add the nfa_move helper, then a first run_nfa without ε-moves |
| 2.3.1, 2.3.2 | simulator.py |
add eps_closure, then change two lines of run_nfa to use it |
| 3.1 | writeup.md |
add your Thompson NFA for a(b\|c)* |
Time budget. This lab follows the class material on regular expressions and finite automata. The course schedule has the assigned and due dates. Plan about three hours (with your partner, if you have one) for Parts 1 and 2, and about an hour for the paper work in Parts 0 and 3. Write the reflection as you go, not at the end.
- On assignment: the loader and DFA simulator work on the provided machines.
- Midpoint: the NFA simulator and epsilon-closure work, and both designed machines are encoded and tested.
- Due date: the Thompson NFA and writeup are assembled and the ZIP is submitted.
Part 1: DFA Simulation and Design
Step 1.1: Read the Machine Format and Encode the Parity Machine
Every machine is a JSON (JavaScript Object Notation) file with these keys:
| Key | Type | Meaning |
|---|---|---|
states |
list of strings | all state names |
alphabet |
list of strings | all input symbols (each a single character) |
start |
string | the initial state |
accept |
list of strings | the accepting states |
delta |
object | transition function |
For a DFA, delta is a nested object. delta[state][symbol] gives the next state, and the object must contain every (state, symbol) pair over the alphabet.
Here is the two-state parity machine for “even number of 1s,” first as a diagram and then as the JSON you type. In the diagram, the arrow from nowhere marks the start state, a double circle marks an accepting state, and each arrow carries the symbol that triggers it.
{
"states": ["even", "odd"],
"alphabet": ["0", "1"],
"start": "even",
"accept": ["even"],
"delta": {
"even": {"0": "even", "1": "odd"},
"odd": {"0": "odd", "1": "even"}
}
}
Each arrow in the diagram is one entry in delta. The 1 arrow from even to odd, for example, is the "1": "odd" inside "even". The double circle is the accept list, and the arrow from nowhere is the start key. Save the JSON as machines/even_ones.json. Then trace "0110" and "100" through the diagram by hand before you trust the program:
| String | States visited | Result |
|---|---|---|
0110 |
even → even → odd → even → even | accept |
100 |
even → odd → odd → odd | reject |
Before You Code: How a DFA Lives in Python
When json.load reads the file above, each JSON object becomes a Python dictionary, so finding the next state takes two lookups: delta[state] picks the row for the current state, and [symbol] picks the arrow. Together, delta["even"]["1"] is 'odd'. The class notes walk through this with code you can run.
Start with this one machine, not with the full simulator. The smallest program that runs it is about ten lines long, and you write it now in its own file, dfa_core.py. Every later step builds on the same idea, so you see it work in its simplest form first.
You write: machines/even_ones.json (above) and a new file, dfa_core.py.
You run: python3 dfa_core.py 0110 and python3 dfa_core.py 100.
Write dfa_core.py one piece at a time. Each piece of the program matches one part of the diagram, so read the sentence beside each piece before you type it.
-
Bring in the two modules you need.
jsonreads the machine file, andsysgives you the input string from the command line.import json import sys -
Load the machine.
json.loadturns the file into a Python dictionary, somachine["start"],machine["accept"], andmachine["delta"]are the keys you saw in the table above.with open("machines/even_ones.json") as f: machine = json.load(f) -
Put the machine in its start state. This line is the arrow from nowhere in the diagram.
state = machine["start"] -
Read the input one symbol at a time.
sys.argv[1]is the first word you type after the program name, so inpython3 dfa_core.py 0110it is the string"0110", and looping over a string gives one character per pass.for symbol in sys.argv[1]: -
Inside the loop, follow one arrow.
machine["delta"][state]is the row for the current state, and[symbol]picks the arrow for this symbol. This line is the transition function \(\delta(q, a)\) written in Python. The rest of the file loads the machine and reports the result.state = machine["delta"][state][symbol] -
After the loop, check for a double circle. The input has run out, so the machine accepts exactly when the state it ended in is in the
acceptlist.if state in machine["accept"]: print("accept") else: print("reject")
Put together, the whole file is eleven lines:
import json
import sys
with open("machines/even_ones.json") as f:
machine = json.load(f)
state = machine["start"]
for symbol in sys.argv[1]:
state = machine["delta"][state][symbol]
if state in machine["accept"]:
print("accept")
else:
print("reject")
Do this.
- Type
dfa_core.pypiece by piece as shown above, and save it incs374-automata, next to themachines/folder.- From inside
cs374-automata, runpython3 dfa_core.py 0110and thenpython3 dfa_core.py 100.- Predict before you run it: what should
python3 dfa_core.py ""print, and why? Run it and check.
You should see.
accept, thenreject, matching the hand traces above. The empty string printsaccept, because the loop never runs and the machine stays ineven, which accepts.
Watch it run (optional). Add
print(symbol, "->", state)as a second line inside the loop, indented to match the line above it. Runpython3 dfa_core.py 0110again. The four lines it prints show each symbol and the state it led to, which is the “States visited” column of the trace table after the start state. Remove the line when you are done, or keep it; either is fine.
If it fails.
FileNotFoundError: you ran from a different folder.cdintocs374-automataand try again.JSONDecodeError: the JSON file has a missing comma or quote, and the message names the line.IndexError: list index out of range: you forgot the input string. Type it after the program name.KeyError: '2': you typed a symbol the machine does not know. This program has no checks; Step 1.3 adds them.
dfa_core.py is now finished. Leave it as it is, because you submit it. It works for one machine whose path is written into the program, and it crashes on bad input. The rest of Part 1 fixes both problems in a new file, simulator.py.
Step 1.2: Write the Loader
A wrong machine file is the most common bug in this lab. Your loader checks the file once, up front, and reports every problem at the same time. That saves you from chasing a KeyError deep inside the simulator.
You write: a new file, simulator.py, containing MachineError, is_nfa, three small checker helpers, and load_machine. Do not edit dfa_core.py.
You run: a one-line check after each helper, then python3 -c "import simulator; simulator.load_machine('machines/even_ones.json')", then the same command on a broken file.
load_machine(path) reads a JSON file and checks that the start state, the accept states, and every transition refer only to declared states and alphabet symbols. It collects every validation error in a list, then raises one MachineError that lists them, one per line. It does not stop at the first error. It accepts both delta shapes. A DFA’s delta is an object of objects. An NFA’s delta (Part 2) has "state,symbol" keys with list values, and the symbol half may be the special word eps. The check must let eps through for an NFA and reject every other symbol outside the alphabet.
That is several separate checks, so you write each check as its own small helper and test it on its own. Every helper follows one pattern. It starts an empty list, tests one kind of condition, and when the condition fails, appends a sentence that names the bad value. It returns the list and never raises. Word your messages like the expected output below, so you can compare yours line by line. Step 1.2.5 then integrates the helpers into load_machine, which joins their lists and raises once, so one run reports every problem.
Before you start, save this deliberately broken machine as machines/broken.json. It has a misspelled start state, a misspelled accept state, and no arrow out of odd on 1. It is a test file, and you do not need to submit it. Each helper’s check runs on it.
{
"states": ["even", "odd"],
"alphabet": ["0", "1"],
"start": "evn",
"accept": ["od"],
"delta": {
"even": {"0": "even", "1": "odd"},
"odd": {"0": "odd"}
}
}
The helpers take a machine that is already a dictionary, so their checks load the JSON with json.load directly. Only load_machine opens files itself.
Step 1.2.1: MachineError and is_nfa
Do this. Create a new file,
simulator.py, next todfa_core.py. Type in the lines below, then write the body ofis_nfafrom its# Stepcomments.
import json
import sys
class MachineError(Exception):
"""Raised by load_machine with every validation problem listed at once."""
def is_nfa(machine):
"""Return True if machine is an NFA and False if it is a DFA."""
# Step 1: Look at the values stored in machine["delta"]. A DFA's values
# are dictionaries, one row per state. An NFA's values are lists
# of target states (Part 2).
# Step 2: Return True if at least one of those values is a list, and
# False otherwise.
...
| Step | How to think about it |
|---|---|
is_nfa |
You only need to know whether some value is a list. isinstance(value, list) answers that for one value, and any(...) asks it of all of them at once. Part 2 is the first time it returns True. |
python3 -c "import json, simulator as s; print(s.is_nfa(json.load(open('machines/even_ones.json'))))"
You should see.
False: the parity machine is a DFA.
Step 1.2.2: Check the start and accept states
Do this. Add the helper below
is_nfa, write its body, and run the check.
def check_start_and_accept(machine, states):
"""Return a list of error sentences about the start and accept states (empty if none)."""
# Step 1: Start an empty list, errors.
# Step 2: If the start state is not in states (the set of declared states),
# append an error that names it.
# Step 3: For each accept state that is not in states, append an error
# that names it.
# Step 4: Return errors.
...
| Step | How to think about it |
|---|---|
| start and accept (Steps 2 and 3) | Membership in the set of declared states is the whole test. Inside an f-string, !r prints a value with quotes around it, so a stray space or a misspelling is easy to see. |
python3 -c "import json, simulator as s; m = json.load(open('machines/broken.json')); print(s.check_start_and_accept(m, set(m['states'])))"
You should see. A list of two sentences, one per misspelling. Your wording may differ:
["start state 'evn' is not a declared state", "accept state 'od' is not a declared state"]
Step 1.2.3: Check a DFA’s transitions
Do this. Add the helper below, write its body, and run the check.
def check_dfa_delta(machine, states, alphabet):
"""Return a list of error sentences about a DFA's delta (empty if none)."""
# Step 1: Start an empty list, errors.
# Step 2: For every declared state and every alphabet symbol, the pair must
# appear in delta, and its target must be a declared state. A
# missing pair and an undeclared target are two different errors.
# Step 3: Return errors.
...
| Step | How to think about it |
|---|---|
| DFA pairs (Step 2) | Loop over the declared states and the alphabet, not over the keys of delta. A row that is missing from delta is exactly what you are looking for, and a loop over delta would never visit it. dict.get with a default never crashes: a missing row can default to an empty dictionary, and a missing symbol then gives None. None means the pair is missing, so name both the state and the symbol. Otherwise, check that the target is declared. |
python3 -c "import json, simulator as s; m = json.load(open('machines/broken.json')); print(s.check_dfa_delta(m, set(m['states']), set(m['alphabet'])))"
You should see.
["missing transition for state 'odd' on symbol '1'"]Run the same command on
machines/even_ones.jsonas well. It prints[], because the parity machine has every pair.
Step 1.2.4: Check an NFA’s transitions
Do this. Add the helper below and write its body.
def check_nfa_delta(machine, states, alphabet):
"""Return a list of error sentences about an NFA's delta (empty if none)."""
# Step 1: Start an empty list, errors.
# Step 2: Check every "state,symbol" key. Its state half must be declared.
# Its symbol half must be in the alphabet or be the word "eps".
# Every target in its list must be declared.
# Step 3: Return errors.
...
| Step | How to think about it |
|---|---|
| NFA keys (Step 2) | Visit each key together with its list of targets; .items() gives you both. Each key is one string, so split it at the comma into its state half and its symbol half. str.partition or str.split both work. The symbol test has two ways to pass: in the alphabet, or the word eps. |
You cannot test this helper until Part 2 gives you an NFA file. Write it now anyway, because Part 2 relies on it.
Step 1.2.5: Integrate the checks into load_machine
Do this. Add
load_machinebelow the three helpers and write its body from its# Stepcomments. Each check is now one call.
def load_machine(path):
"""Read the JSON machine at path, check it, and return it as a dict.
Raise one MachineError that lists every problem, one per line.
"""
# Step 1: Open the file at path and parse it into a dictionary, machine.
# Step 2: Build a set of the declared states and a set of the alphabet
# symbols, so each check is a quick membership test.
# Step 3: Start the list errors with what check_start_and_accept returns.
# Step 4: If is_nfa(machine) is true, add what check_nfa_delta returns.
# Otherwise the machine is a DFA: add what check_dfa_delta returns.
# Step 5: If errors is not empty, raise one MachineError whose message is
# every error joined into one string, one per line. Otherwise,
# return machine.
...
| Step | How to think about it |
|---|---|
| combine (Steps 3 and 4) | Each helper returns a list, and + or list.extend joins two lists. Nothing raises until Step 5. |
| raise once (Step 5) | An empty list is false in an if. str.join with a newline turns the list into one message with one problem per line. |
simulator.py has no command line yet, so you test it by importing it. Test the loader on the good file first:
python3 -c "import simulator; simulator.load_machine('machines/even_ones.json')"
You should see. Nothing. Silence means the file passed.
Now check that the loader reports every problem at once, not just the first, on the broken machine you saved at the start of this step:
python3 -c "import simulator; simulator.load_machine('machines/broken.json')"
You should see. A traceback that ends with one
MachineErrorlisting all three problems, one per line. Your wording may differ, but every problem must appear:simulator.MachineError: start state 'evn' is not a declared state accept state 'od' is not a declared state missing transition for state 'odd' on symbol '1'If only the first line appears, you raised the error inside a check instead of appending to
errorsand raising once at the end.
Step 1.3: Write run_dfa and the Command Line
dfa_core.py crashes on an unknown symbol, and it has the machine path written into the program. This step fixes both problems in simulator.py and adds trace mode. The loop inside run_dfa is the same loop you wrote in Step 1.1; the new lines are the checks around it.
You write: run_dfa and main in simulator.py, below load_machine.
You run: a one-line check after each pass of run_dfa, then python3 simulator.py machines/even_ones.json 0110 --trace, plus the two other commands at the end of this step.
run_dfa does what dfa_core.py does, plus three things a real tool needs: it rejects an unknown symbol without crashing, it treats the empty string as valid input, and it can print a trace. You build it in three passes, and you check each pass before starting the next.
Step 1.3.1: The core loop
A DFA has exactly one arrow for each state and symbol, so the machine is always in exactly one state. That is why one variable can hold it, and why each symbol takes one lookup. Add this skeleton below load_machine, and replace each TODO with one line of code:
def run_dfa(machine, s, trace=False):
"""Run the DFA on the string s. Return True to accept, False to reject."""
# TODO: set state to the machine's start state.
for symbol in s:
# TODO: follow one arrow. Look up the row for state, then the arrow for symbol.
# TODO: return whether state is in the machine's accept list.
These are the same lines you wrote in dfa_core.py. Here is the run on 01 with the parity machine, so you can compare it with what your code does:
state = even the start state
read 0: delta["even"]["0"] is even, so state = even
read 1: delta["even"]["1"] is odd, so state = odd
"odd" is not in the accept list, so return False
Plain [...] lookups are safe here, because the loader already proved that every state and symbol pair exists. You need no special case for the empty string: the loop never runs, so the last line tests the start state.
python3 -c "import simulator as s; m = s.load_machine('machines/even_ones.json'); print(s.run_dfa(m, '0110'), s.run_dfa(m, '100'), s.run_dfa(m, ''))"
You should see.
True False True
Step 1.3.2: Reject an unknown symbol
The loader proved that every symbol in the alphabet has an arrow, but it knows nothing about the input string. Try one with a symbol the machine does not have:
python3 -c "import simulator as s; m = s.load_machine('machines/even_ones.json'); print(s.run_dfa(m, '0120'))"
This crashes with KeyError: '2', because the row for even has no key "2". The fix has to run before the lookup, so add it as the first thing inside the loop:
for symbol in s:
# TODO: if symbol is not in the machine's alphabet, print the reason below and return False.
# reject: symbol '2' is not in the alphabet
f"{symbol!r}" prints the symbol with the quotes around it. Run the same command again.
You should see. The reason, then
False, and no traceback:reject: symbol '2' is not in the alphabet False
Step 1.3.3: Add the trace
The trace lets you watch the machine run, which is how you will debug the machines you design in Step 1.4. It prints the start state once, and then the state after each move. Add two prints, each under if trace: so that a normal run stays quiet:
# TODO: after setting the start state, if trace is on, print: start: even
# TODO: after the move in the loop, if trace is on, print: read 1 -> odd
python3 -c "import simulator as s; m = s.load_machine('machines/even_ones.json'); print(s.run_dfa(m, '01', trace=True))"
You should see.
start: even read 0 -> even read 1 -> odd False
Step 1.3.4: Integrate: add main and run the program
Do this.
- Add a
mainthat takes the machine path and the input string from the command line and honors a--traceflag.mainand theif __name__ == "__main__":lines go last in the file, so every function they call is defined above them.- Type
mainexactly as shown; it is complete.
def main(argv):
# Usage: python3 simulator.py <machine.json> <string> [--trace]
trace = "--trace" in argv
args = [a for a in argv if a != "--trace"]
machine = load_machine(args[0])
s = args[1] if len(args) > 1 else ""
run = run_nfa if is_nfa(machine) else run_dfa # run_nfa arrives in Part 2
print("accept" if run(machine, s, trace) else "reject")
if __name__ == "__main__":
main(sys.argv[1:])
main mentions run_nfa, which you do not write until Part 2. The line still works for a DFA, because Python evaluates only the branch of run_nfa if is_nfa(machine) else run_dfa that it takes. When you finish this step, simulator.py holds these pieces, from top to bottom:
import json, import sys
class MachineError (Step 1.2.1)
def is_nfa (Step 1.2.1)
def check_start_and_accept (Step 1.2.2)
def check_dfa_delta (Step 1.2.3)
def check_nfa_delta (Step 1.2.4)
def load_machine (Step 1.2.5)
def run_dfa (Steps 1.3.1 to 1.3.3)
def main (Step 1.3.4)
if __name__ == "__main__" (Step 1.3.4, always last)
Test all three rules:
python3 simulator.py machines/even_ones.json 0110 --trace
python3 simulator.py machines/even_ones.json ""
python3 simulator.py machines/even_ones.json 0120
You should see. For the first command, one state per symbol and then the verdict:
start: even read 0 -> even read 1 -> odd read 1 -> even read 0 -> even acceptFor the empty string,
acceptalone, because the start stateevenaccepts. For the third command, a one-line reason such asreject: symbol '2' is not in the alphabet, thenreject, and no traceback.
Checkpoint. Test the parity machine on at least four strings it accepts and four it rejects. Record each string and its result in
writeup.mdunder a heading for the machine.
Step 1.4: Design the Ends-in-ab DFA
To design a DFA, decide what each state must remember. For this language the question is short: how much of the suffix ab has the machine just seen? The parity machine shows the method. Each of its two states stands for one fact about the input so far (“even number of 1s” or “odd”), and each arrow says how one more symbol changes that fact. Do the same here, with one state for each answer to “how much of ab have I just seen?” Every state needs an arrow on both a and b. The class notes build the mirror image, ends in ba, with the subset construction, if you want to check your approach after you design yours.
Do this.
- Design a DFA for Ends in ab: strings over
{a, b}that end with the suffixab.- Draw it on paper first, then encode it as
machines/ends_in_ab.jsonin the same format as the parity machine.- In
writeup.md, annotate each state with one sentence about what it remembers about the input so far.- Test at least four strings it accepts and four it rejects, and record them next to the state annotations.
You write: machines/ends_in_ab.json, plus a heading for this machine in writeup.md with your state annotations and test strings. No Python changes; simulator.py already runs any DFA.
You run: the three commands below, then your own eight test strings.
Worked example: "aab" -> accept; "ba" -> reject; "ab" -> accept; "" -> reject. Hint: you need at least three states. You can start from this skeleton. The state names are a suggestion, one for each answer to “how much of ab have I just seen?”, and you may rename them. Replace each "TODO" with a state name, and decide which state or states go in accept:
{
"states": ["none", "a", "ab"],
"alphabet": ["a", "b"],
"start": "none",
"accept": ["TODO"],
"delta": {
"none": {"a": "TODO", "b": "TODO"},
"a": {"a": "TODO", "b": "TODO"},
"ab": {"a": "TODO", "b": "TODO"}
}
}
Your loader catches any "TODO" you forget to replace, because "TODO" is not a declared state.
python3 simulator.py machines/ends_in_ab.json aab
python3 simulator.py machines/ends_in_ab.json ba
python3 simulator.py machines/ends_in_ab.json ""
You should see.
accept,reject,reject. If the empty string accepts, you marked the start state as accepting. The empty string does not end inab.
If it fails.
MachineErrornaming a missing (state, symbol) pair: a DFA needs an arrow out of every state on bothaandb, including the “just sawab” state."abb"accepts: afterab, readingbmust forget the suffix entirely, not step back one state."aab"rejects: aftera, anotheramust stay in the “just sawa” state, because the neweracould still start the suffix.
Part 2: NFA Simulation and Design
Step 2.1: Read the NFA Machine Format
For an NFA, delta maps "state,symbol" string keys to lists of states. The special symbol "eps" marks an ε-transition, a move the machine may take without reading input. A state may have any number of targets for a symbol, including none. The other keys work as they do for a DFA. Do not list eps in alphabet: it labels a transition, and it is not an input symbol.
Watch it run. The FSM Simulator steps a DFA, NFA, or ε-NFA one input symbol at a time and highlights the active set, the same set your
run_nfacomputes. Its format differs from ours in two ways. It writes ε as$where our JSON writeseps, and it lists transitions asq0:a>q0,q1instead of JSON keys.
Here is part of an NFA as a diagram and then as JSON. The fragment does not say which states accept, so no state has a double circle.
"delta": {
"q0,a": ["q0", "q1"],
"q0,eps": ["q2"],
"q1,b": ["q2"]
}
The a arrows out of q0 go two ways, so "q0,a" lists two targets. That is the nondeterminism. Pairs with no arrow, such as q1 on a or q2 on anything, have no key. Read transitions with machine["delta"].get(key, []), so a missing key means “no moves” and does not raise a KeyError.
Before you write any NFA code, encode an NFA whose behavior you already know, so you have something to test against. This one accepts strings over {a, b} that end in ab, and the class notes trace aab through it. Step 2.2 checks your run_nfa against that trace.
| state | on a |
on b |
|---|---|---|
| q0 | {q0, q1} | {q0} |
| q1 | ∅ | {q2} |
| q2 | ∅ | ∅ |
You write: machines/ends_in_ab_nfa.json. It is a test file, and you do not need to submit it.
You run: python3 -c "import simulator; simulator.load_machine('machines/ends_in_ab_nfa.json')"
Do this.
- Start from the JSON below. The first key,
"q0,a", is done for you; it is the{q0, q1}cell in rowq0, columna, of the table above.- Add one key for each other non-empty cell of that table. A cell that shows ∅ gets no key.
- Run the loader command. Silence means the file passed, and it also means the NFA branch of your loader accepts a correct file.
{
"states": ["q0", "q1", "q2"],
"alphabet": ["a", "b"],
"start": "q0",
"accept": ["q2"],
"delta": {
"q0,a": ["q0", "q1"]
}
}
If it fails. A
MachineErrorabout a symbol such as'a"'or' b'usually means a stray quote or space inside a key. Each key is exactly a state, a comma, and a symbol, with no spaces. When you finish, the file has three keys.
Before You Code: Keys and Sets
In the NFA format, "q0,a": ["q0", "q1"] reads as “from state q0, reading a, the machine can go to q0 or to q1.” The key holds the state and the symbol in one string, and the value lists every arrow with that label. A pair with no arrow has no key, so read with delta.get(key, []). The class notes read a whole NFA this way.
The machine is in a set of states, so you use a Python set: moved = set() starts one, moved.update(targets) adds every item of a list, "q2" in active tests membership, and sorted(active) prints it in a fixed order. A frozenset is a set that cannot change, which is what eps_closure returns. The class notes show each of these with examples.
Step 2.2: Write run_nfa Without ε-Moves
Get the set of states working first, on a machine with no ε-moves. The “ends in ab” NFA is that machine, and the class notes already trace aab through it. Step 2.3 then adds ε-moves by changing two lines.
You write: nfa_move and a first version of run_nfa in simulator.py, below run_dfa and above main.
You run: the commands after each sub-step.
Step 2.2.1: One move, and why it is a union
Each state in the active set is a guess that might be right, and the machine does not know which one. So on the next symbol, every active state moves, and the next active set keeps everything any of them can reach. Collecting those results is the union in the class notes’ rule. Here is the active set {q0, q1} reading b:
active state lookup targets
q0 ---> "q0,b": ["q0"] ---> q0 \
union: {q0, q2}
q1 ---> "q1,b": ["q2"] ---> q2 /
Neither result can be dropped. Keep only q0’s result on aab, and the machine never reaches q2, so it wrongly rejects. Keep only q1’s result on aabab, and the machine is left in q2, which has no arrows, so it wrongly rejects that too. The union keeps every guess alive.
In pseudocode:
move(active, symbol):
moved = empty set
for each state q in active:
moved = moved ∪ delta(q, symbol) # no arrow adds nothing
return moved
Add this skeleton to simulator.py, and replace each TODO with one line:
def nfa_move(machine, active, symbol):
"""Return every state reachable from active on one symbol."""
moved = set()
for state in active:
# TODO: build this state's key, as in "q0,b".
# TODO: look up its targets in machine["delta"], with [] if the key is missing.
# TODO: add every target to moved.
return moved
python3 -c "import simulator as s; m = s.load_machine('machines/ends_in_ab_nfa.json'); print(sorted(s.nfa_move(m, {'q0', 'q1'}, 'b')), sorted(s.nfa_move(m, {'q0'}, 'a')), s.nfa_move(m, {'q2'}, 'a'))"
You should see.
['q0', 'q2'] ['q0', 'q1'] set(). The first list is the diagram above. The last value is the empty set, becauseq2has noaarrow.
If it fails. If the first list holds only one state, your code kept one active state’s targets and lost the other’s. Check that
moved = set()stays above the loop, and that the loop adds tomovedand never assigns a new value to it.
Step 2.2.2: One move per symbol
run_nfa has the same shape as run_dfa, with a set of states where run_dfa had one state:
run(s):
active = { start }
for each symbol in s:
active = move(active, symbol)
accept if any state in active is an accepting state
The last line is the NFA’s rule for accepting: the string is in the language if at least one guess worked. In Python, any(state in active for state in machine["accept"]) asks that question. Add this skeleton below nfa_move:
def run_nfa(machine, s, trace=False):
"""Run the NFA on the string s. Return True to accept, False to reject."""
# TODO: set active to a set holding only the start state.
if trace:
print(f"start: {sorted(active)}")
for symbol in s:
# TODO: replace active with the result of one move on this symbol.
if trace:
print(f"read {symbol} -> {sorted(active)}")
# TODO: return whether any accepting state is in active.
You need no special cases. If the active set becomes empty, it stays empty, and the string is rejected. A symbol outside the alphabet has no key, so it empties the set too. main already calls run_nfa when is_nfa returns true, so run the whole program:
python3 simulator.py machines/ends_in_ab_nfa.json aab --trace
python3 simulator.py machines/ends_in_ab_nfa.json aba
python3 simulator.py machines/ends_in_ab_nfa.json ""
You should see. The same active sets as the class notes’ trace of
aab, and then two rejects:start: ['q0'] read a -> ['q0', 'q1'] read a -> ['q0', 'q1'] read b -> ['q0', 'q2'] accept reject reject
Step 2.3: Add ε-Moves
An ε-move reads no input, so your run_nfa never takes one. Here is a machine that shows the problem. It is ab? from the class notes: an a, optionally followed by a b.
q0 --a--> q1 --b--> q2 (accepts)
\ ^
`---ε---'
You write: two test files, machines/ab_optional.json and machines/eps_cycle.json; eps_closure in simulator.py, below run_nfa and above main; and two changed lines in run_nfa.
You run: the commands after each sub-step.
Save this as machines/ab_optional.json, a test file you do not submit:
{
"states": ["q0", "q1", "q2"],
"alphabet": ["a", "b"],
"start": "q0",
"accept": ["q2"],
"delta": {"q0,a": ["q1"], "q1,b": ["q2"], "q1,eps": ["q2"]}
}
python3 simulator.py machines/ab_optional.json a --trace
You should see this wrong answer. The machine should accept
a, but the active set stops atq1. Fromq1it can reachq2for free, so the active set should be{q1, q2}.start: ['q0'] read a -> ['q1'] reject
If it fails. A
MachineErrorthat mentionsepsmeans your loader rejectsepsas an unknown symbol. Allow it for NFAs incheck_nfa_delta(Step 1.2.4).
Step 2.3.1: Follow the free moves (eps_closure)
An ε-arrow is stored like any other arrow, with eps as the symbol, so "q1,eps" reads as “from q1, for free, the machine can go to these states.” You look it up the same way you look up "q0,a". eps stays out of alphabet, because no input string contains it.
One lookup is not enough, because free moves chain:
q0 --ε--> q1 --ε--> q2
If the machine can be in q0, it can also be in q1, and so also in q2. The lookup on q0 finds only q1, so you have to look up each new state too. Arrows can also lead back to an earlier state, so you have to remember what you already found, or a cycle runs forever. The ε-closure of a set of states is that set plus everything reachable from it by ε-arrows alone. In pseudocode, with a to-do list called the frontier:
closure(states):
closure = states # zero moves count
frontier = states # states whose ε-arrows we have not followed
while frontier is not empty:
q = take one state off frontier
for each target r of an ε-arrow out of q:
if r is not in closure: # this check is what stops a cycle
add r to closure
add r to frontier
return closure
Add this skeleton below run_nfa:
def eps_closure(machine, states):
"""Return every state reachable from states by ε-moves alone, as a frozenset."""
closure = set(states)
frontier = list(states)
while frontier:
state = frontier.pop()
# TODO: look up this state's ε-targets, with [] if the key is missing.
# TODO: for each target not in closure, add it to closure and append it to frontier.
return frozenset(closure)
Now test the hard case, a cycle. Save this as machines/eps_cycle.json, another test file you do not submit:
{
"states": ["q0", "q1", "q2"],
"alphabet": ["a"],
"start": "q0",
"accept": ["q2"],
"delta": {"q0,eps": ["q1"], "q1,eps": ["q2"], "q2,eps": ["q0"]}
}
Here is the closure of {q0} on this machine, one pass of the while loop per line:
closure = {q0}, frontier = [q0]
take q0: targets [q1], q1 is new. closure = {q0, q1}, frontier = [q1]
take q1: targets [q2], q2 is new. closure = {q0, q1, q2}, frontier = [q2]
take q2: targets [q0], q0 is not new. closure = {q0, q1, q2}, frontier = []
python3 -c "import simulator as s; m = s.load_machine('machines/eps_cycle.json'); print(sorted(s.eps_closure(m, {'q0'})))"
python3 -c "import simulator as s; m = s.load_machine('machines/ab_optional.json'); print(sorted(s.eps_closure(m, {'q1'})), sorted(s.eps_closure(m, {'q0'})))"
You should see.
['q0', 'q1', 'q2'] ['q1', 'q2'] ['q0']The second line shows that closure only follows arrows forward, and that a state with no ε-arrows closes to itself.
If it fails.
- The command never finishes: the
ifcheck is missing, so the cycle runs forever. Press Ctrl+C to stop it.- Only
['q0']: new states are not going onto the frontier, so you followed one arrow and stopped.
Step 2.3.2: Close after each move, and at the start
The machine can take free moves at any time, so the active set must always include them. That gives two places to close, the move, then ε-close rule. Make one change at a time, and watch what each one fixes.
Close after each move. In the loop of run_nfa, change the move line so that active becomes the eps_closure of what nfa_move returns.
python3 simulator.py machines/ab_optional.json a --trace
python3 simulator.py machines/ab_optional.json ab --trace
python3 simulator.py machines/ab_optional.json abb
You should see. Both traces accept, and
abbis rejected:start: ['q0'] read a -> ['q1', 'q2'] accept start: ['q0'] read a -> ['q1', 'q2'] read b -> ['q2'] accept reject
Close at the start. The machine can also take free moves before it reads anything. The cycle machine’s start state reaches its accepting state by ε-moves alone, so it should accept the empty string:
python3 simulator.py machines/eps_cycle.json ""
You should see this wrong answer.
reject. The loop never runs on the empty string, so nothing has closed the start state yet.
Change the first line of run_nfa so that active starts as the eps_closure of the set holding the start state. Then run the cycle machine again, and rerun the first machine to confirm nothing broke:
python3 simulator.py machines/eps_cycle.json ""
python3 simulator.py machines/eps_cycle.json a
python3 simulator.py machines/ends_in_ab_nfa.json aab --trace
You should see.
accept, thenreject, and then the sameaabtrace as Step 2.2.2. The stringarejects because no state has anaarrow, so the active set becomes empty. Theaabtrace does not change, because that machine has no ε-arrows and each closure leaves its set as it was.
Step 2.4: Design the Contains-aa NFA
An NFA can guess. Here the guess is “the aa starts here,” and the machine keeps every guess alive at once.
Do this.
- Design an NFA for Contains aa: strings over
{a, b}that contain the substringaasomewhere.- Encode it as
machines/contains_aa.json.- Test at least four strings it accepts and four it rejects, and record them in
writeup.md.- Include the
--traceoutput for at least one accepted string, so the writeup shows the execution path.
Worked example: "baaab" -> accept; "ababab" -> reject. Hint: let the machine guess where aa occurs. The “ends in ab” NFA in Step 2.1 shows the pattern. Its start state loops on every symbol while the machine has not yet guessed, and a chain of states spells out the pattern. Here aa may appear anywhere, not only at the end, so your accepting state must also loop on every symbol once the pattern has appeared. Your design must use nondeterminism; a DFA in disguise does not count.
You write: machines/contains_aa.json, plus a heading for this machine in writeup.md with your test strings and one --trace output. No Python changes; simulator.py already runs any NFA.
You run: the two commands below, then your own eight test strings.
You can start from this skeleton. Three states are enough: q0 has not yet guessed, q1 has just read the first a of the guessed aa, and q2 has seen aa. Draw the arrows on paper first. Then replace the placeholder key with one "state,symbol" key for each state and symbol that has at least one arrow, and list its targets. A pair with no arrow gets no key. Your loader reports the placeholder if you forget to replace it, because "TODO" is not a declared state:
{
"states": ["q0", "q1", "q2"],
"alphabet": ["a", "b"],
"start": "q0",
"accept": ["q2"],
"delta": {
"TODO,a": ["TODO"]
}
}
Watch out. At least one
"state,symbol"key in your JSON should list two or more targets. If every list has one entry and there is noepskey, you have written a DFA in NFA format.
python3 simulator.py machines/contains_aa.json baaab --trace
python3 simulator.py machines/contains_aa.json ababab
You should see. A trace whose active set grows when the machine guesses that an
astarts theaa, ending inaccept; andrejectforababab. Your state names will differ, but the shape looks like this:start: ['q0'] read b -> ['q0'] read a -> ['q0', 'q1'] read a -> ['q0', 'q1', 'q2'] read a -> ['q0', 'q1', 'q2'] read b -> ['q0', 'q2'] accept
Part 3: Thompson’s Construction by Hand
Part 3 is paper work for your writeup, with no code. Thompson’s construction turns a regular expression into an NFA by following fixed rules, and it joins the pieces with ε-arrows. First see the rules build a few small fragments. Then you build one machine yourself.
Below, 1 -x-> 2 means an arrow from state 1 to state 2 on x, and 2 -ε-> 3 means a free move. Number the states in the order you create them, and keep the numbers when you reuse a fragment.
A single symbol. Two new states and one arrow. The fragment for x is:
x: 1 -x-> 2 start 1, accept 2
y: 3 -y-> 4 start 3, accept 4
Union. A new start with ε-arrows into both fragments, and a new accept with ε-arrows out of both. The fragment for x|y reuses the two above:
x|y: 5 -ε-> 1 5 -ε-> 3 new start 5
2 -ε-> 6 4 -ε-> 6 new accept 6
Concatenation. One ε-arrow from the first fragment’s accept to the second fragment’s start, and no new states beyond the fragments themselves. To build (x|y)z, make the fragment for z, and join it to the union:
z: 7 -z-> 8 start 7, accept 8
(x|y)z: 6 -ε-> 7 start 5, accept 8
States 2, 4, and 6 accepted in their own fragments, but they do not accept now. Only the outermost accept does. Here is the finished machine:
|  |
Star. A new start, a new accept, and four ε-arrows: into the fragment, out of it, back around it, and straight past it for zero repetitions. To build (ab)*, first make ab by concatenation, and then wrap it:
a: 1 -a-> 2 start 1, accept 2
b: 3 -b-> 4 start 3, accept 4
ab: 2 -ε-> 3 start 1, accept 4
(ab)*: 5 -ε-> 1 enter
4 -ε-> 6 stop
4 -ε-> 1 go around again
5 -ε-> 6 zero times start 5, accept 6
Step 3.1: Build a(b|c)*
You write (in writeup.md under a ## Part 3: Thompson's Construction heading): the finished NFA for a(b|c)*, either as a drawing or as a list of arrows in the form above. No code.
Do this.
- Apply Thompson’s construction to the regular expression
a(b|c)*. Work from the inside out: the single symbols, then the union, then the star, then the concatenation.- Number every state, mark every ε-arrow, and say which state is the start and which state accepts.
Deliverables
Submit a ZIP that contains the files below. List your Python version in the writeup so I can reproduce your results.
| File or artifact | What it shows | Rubric row |
|---|---|---|
dfa_core.py |
the ten-line DFA runner from Step 1.1, unchanged after that step | Part 1 |
simulator.py |
load_machine, run_dfa, nfa_move, run_nfa, eps_closure, and the command-line entry point |
Parts 1 and 2 |
machines/even_ones.json |
the provided parity machine, unchanged | Part 1 |
machines/ends_in_ab.json |
your designed DFA | Part 1 |
machines/contains_aa.json |
your designed NFA | Part 2 |
writeup.md |
Part 0 paper work; state annotations and test strings for both designed machines; your Thompson NFA for a(b|c)*; your name (and your partner’s, if you worked in a pair) |
Parts 0, 1, 2, 3 |
Self-Check Before You Submit
python3 dfa_core.py 0110printsaccept, andpython3 dfa_core.py 100printsreject.python3 simulator.py machines/even_ones.json 0110printsaccept, and the same command with100printsreject.- The empty string and an out-of-alphabet symbol each produce a deliberate answer, not a traceback.
--traceprints a state (DFA) or a sorted set of states (NFA) after every symbol.load_machineonmachines/broken.jsonraises oneMachineErrorthat lists all three problems.eps_closurestops onmachines/eps_cycle.jsonand returns all three states.python3 simulator.py machines/ends_in_ab_nfa.json aab --traceprints the same active sets as the hand trace in the class notes.- Each state of the Ends-in-ab DFA has a one-sentence annotation, and both designed machines have at least four accepted and four rejected test strings recorded.
- The Contains-aa NFA has at least one state with two or more targets on the same symbol.
writeup.mdhas the Part 0 work, the Thompson NFA fora(b|c)*, both names, and your Python version.
Reflection Prompts
- In Part 0 you converted an NFA to a DFA, and in Part 3 you built an NFA from a regular expression by following rules. Compare the two tasks: which took more judgment, and why?
- Your simulators treat machines as data loaded from JSON. Name one way this helped your testing that hard-coded machines would not have.
- If you worked in a pair, who did what? Name one thing your partner caught that you would have missed. If you worked alone, say so.
Submission
In your submission, please include answers to any questions asked on the assignment page, as well as the questions listed below, in your README file.
If you wrote code as part of this assignment, please describe your design, approach, and implementation in a separate document prepared using a word processor or typesetting program such as LaTeX. This document should include specific instructions on how to build and run your code, and a description of each code module or function that you created suitable for re-use by a colleague.
In your README, please include answers to the following questions:
- Describe what you did, how you did it, what challenges you encountered, and how you solved them.
- Please answer any questions found throughout the narrative of this assignment.
- If collaboration with a buddy was permitted, did you work with a buddy on this assignment? If so, who? If not, do you certify that this submission represents your own original work?
- Please identify any and all portions of your submission that were not originally written by you (for example, code originally written by your buddy, or anything taken or adapted from a non-classroom resource). It is always OK to use your textbook and instructor notes; however, you are certifying that any portions not designated as coming from an outside person or source are your own original work.
- Approximately how many hours it took you to finish this assignment (I will not judge you for this at all...I am simply using it to gauge if the assignments are too easy or hard)?
- Your overall impression of the assignment. Did you love it, hate it, or were you neutral? One word answers are fine, but if you have any suggestions for the future let me know.
- Using the grading specifications on this page, discuss briefly the grade you would give yourself and why. Discuss each item in the grading specification.
- Any other concerns that you have. For instance, if you have a bug that you were unable to solve but you made progress, write that here. The more you articulate the problem the more partial credit you will receive (it is fine to leave this blank).
- Please describe any use of outside resources you may have engaged in the completion of this assignment, including the use of generative Artificial Intelligence.
Assignment Rubric
| Description | Pre-Emerging (< 50%) | Beginning (50%) | Progressing (85%) | Proficient (100%) |
|---|---|---|---|---|
| Part 0: Before You Start - Regular Expressions and Finite Automata (10%) | Neither the regular expression nor the NFA is attempted | A regular expression is written but no NFA is drawn, or the subset construction is not started | A regex and a matching NFA are given and the subset construction is begun, but it stalls without the stalling step identified, or no accepted string is named | A regular expression for a token class of your choice is given with an NFA that accepts the same language; a small NFA is converted to a DFA by hand over two or three input symbols; one string the DFA accepts is named; and if the state set stopped being obvious, that exact step is marked |
| DFA Simulation and Design (Goals 1, 2) (36%) | The DFA simulator fails to run, or fails most provided machines because of major structural errors | The DFA simulator runs but fails several test cases because of minor issues such as incorrect transition lookups or missing alphabet validation | The DFA simulator passes the provided test cases but mishandles edge cases such as the empty string or symbols outside the alphabet, or the designed DFA lacks state annotations | A correct DFA simulator passes all provided test machines, handles the empty string and out-of-alphabet symbols deliberately, supports trace mode, and runs the designed DFA with documented state meanings and passing tests |
| NFA Simulation and Design (Goals 1, 2) (36%) | The NFA simulator is missing, or fails to compute epsilon-closures correctly | The NFA simulator runs but produces incorrect results on several machines because of epsilon-closure errors or incorrect powerset tracking | The NFA simulator passes the provided test cases but would fail on machines with epsilon cycles, or the designed NFA does not actually use nondeterminism | A correct NFA simulator computes epsilon-closures with cycle detection, tracks the set of active states, and passes all provided test machines plus the designed NFA with traced execution paths |
| By-Hand Constructions (Goals 3, 4) (18%) | The Thompson construction is not attempted, or is fundamentally incorrect | The NFA for a(b|c)* is attempted but contains significant errors, such as a wrong or missing operator rule | The NFA for a(b|c)* follows the rules with minor errors, such as a missing or misdirected epsilon arrow or an unnumbered state | The finished Thompson NFA for a(b|c)* is correct, with every state numbered, every epsilon arrow marked, and the start and accepting states identified |
Please refer to the Style Guide for code quality examples and guidelines.