Automaten für reguläre Ausdrücke erstellen
Zum Code
Ein DFA ist ein deterministischer finiter Automat, manchmal auch deterministischer endlicher Automat genannt. Um zu erfahren, was ein Automat oder ein regulärer Ausdruck ist, empfehle ich, mal im Internet zu suchen oder bei genügend Interesse gleich die passenden Bücher zu kaufen (Reguläre Ausdrücke von O'Reilly und Compilerbau Teil 1 im Oldenbourgverlag, das "Drachenbuch").

Dieses Projekt erzeugt Zustände für einen DFA in VB. Der DFA-Code liegt dem Projekt bei, es ist die Funktion Test_DFA im Modul DFAs. Um einen eigenen DFA zu erstellen, muss man die Funktion nur kopieren und ggf. anpassen, u.a. den Pfad der Übergangstabelle.

Diese DFAs hier können im Prinzip nur sagen, ob ein übergebener String ab einer angegebenen Position einem regulären Ausdruck entspricht, also akzeptiert wird. Die Automaten geben die letzte Stelle des längsten gefundenen Musters zurück. Ein Beispiel für den Einsatz eines DFAs ist der Scanner, welcher praktischerweise mit dem Programm selber erstellt wurde.

Das Projekt besteht aus drei Teilen, dem Scanner, dem LR(1)-Parser und dem Konstruktor. Der Scanner wandelt den Eingabestring, welcher einen korrekten regulären Ausdruck beinhaltet, in einen Symbolstrom um. Der Parser konstruiert einen Syntaxbaum und der Konstruktor bildet die verschiedenen Mengen und schließlich die Zustände.

Ein Erfassen von Teilmengen mit Klammern ist auf Grund der Natur der DFAs nicht möglich (siehe "Reguläre Ausdrücke", O'Reilly). Dafür benötigt man einen NFA (Nichtdeterministischen finiten Automaten) oder einen hybriden Automaten. Wer Spaß dran hat - die Menge Followpos ist nichts anderes als ein NFA.

Der Vorteil eines DFAs ist die Geschwindigkeit. DFAs sind textorientiert, ihre Laufzeit entspricht O(n) wenn die Eingabe n Zeichen hat. Die Laufzeit von NFAs entspricht O(r*n) wenn r die Länge des regulären Ausdrucks ist. Der Nachteil eines DFAs ist der Platzbedarf im Vergleich zu einem NFA. Dieser beträgt beim DFA O(2^r) während er beim NFA nur O(r) beträgt (siehe hierzu das "Drachenbuch" s.o.).

Der vorliegende Code hat einige Einschränkungen bei den regulären Ausdrücken. Er unterstützt nicht alle Konstrukte, die man aus Sprachen wie Perl oder PHP kennt.

Ihm sind einfache Zeichenklassen \w, \d, \s, \W, \D, \S, "." und [] bekannt. Unterstütze Metazeichen sind \n für vbLf, \r für vbCr und \t für vbTab. Beliebige ASCII-Zeichen können durch \xFF angegeben werden, wobei die beiden Fs durch den hexadezimalen ASCII-Code des Zeichens zu ersetzen sind. Die Metazeichen sowie die Zeichenklassen bis auf den Punkt werden auch innerhalb von Zeichenklassen [] selber unterstützt. Ein Bindestrich kann hier einen Bereich angeben, dies funktioniert aber nur zwischen Buchstaben, Zahlen und dem Metazeichen \xFF. Z.B. repräsentiert [a-f] eine Zeichenklasse mit allen Kleinbuchstaben von a bis f. Hier werden alle ASCII-Zeichen zwischen dem ersten Zeichen und dem zweiten Zeichen eingefügt! Der Backslash dient als Escapezeichen, muss daher auch selbst escaped werden. Durch ein Dach ^ als erstes Zeichen wird die Zeichenklasse invertiert.

Die Metaoperationen *, + und ? sind verfügbar, alle jedoch gierig. Da DFAs erstellt werden, welche textgesteuert sind, sind nicht-gierige Ausdrücke nicht ohne weiteres möglich. Dies den DFAs beizubringen ist meist unsauber - ein NFA könnte das von Haus aus.

Der Backslash kann die besondere Bedeutung beliebiger Zeichen innerhalb und ausserhalb von Zeichenklassen aufheben. Ausdrücke können beliebig durch Klammern verschachtelt werden. Die Alternation mit "|" wird unterstützt.

Bei fehlerhaften regulären Ausdrücken als Eingabe kann es vorkommen, dass kein Fehler gemeldet wird oder der Automat sich seltsam verhält. Sollten Fehler entdeckt werden, bitte ich darum, mir eine Email zu schicken.

History
05.10.2003 Hinzugefügt

Autor: Dominik Auras <Dominik_auf_vbInside.de>

Code aus Konstruktor.bas
Option Explicit

Public Type Menge
Element() As Long
End Type

Public Type Zustand
pos As Menge
acc As Boolean
tran() As Long
End Type

Private cp As Long, pos_ptr() As Long, follow() As Menge
Private Stream() As Symbol, sym_stack() As String
Private eingabezeichen() As Long, ce As Long, match() As Menge
Private zustaende() As Zustand, rootsym As Long

Public classes() As Menge

Public Sub BildeMengen(ByRef x() As Symbol, root As Long, _
endsymptr As Long, positions() As Long, followpos() As Menge, _
Eingabe() As Long, matches() As Menge, x_zustaende() As Zustand)
ReDim pos_ptr(100)
ReDim follow(100)
ReDim eingabezeichen(100)
ReDim sym_stack(0)
cp = 0: ce = 0: rootsym = root

Stream = x

Phase1 root

If cp > 0 Then
ReDim Preserve pos_ptr(cp - 1)
ReDim Preserve follow(cp - 1)
Else
ReDim pos_ptr(0)
ReDim follow(0)
pos_ptr(0) = -1
End If

If ce > 0 Then
ReDim Preserve eingabezeichen(ce - 1)
Else
ReDim eingabezeichen(0)
eingabezeichen(0) = -1
End If

Phase2

BerechneZustaende endsymptr

positions = pos_ptr
followpos = follow
Eingabe = eingabezeichen
matches = match
x_zustaende = zustaende

x = Stream
End Sub

Private Sub BerechneZustaende(endsymptr As Long)
Dim i As Long, n As Long, c As Long, f As Menge, g As Long
Dim s_menge As Menge, h As Long, Ende As Long, attr As Sym_Attr

ReDim zustaende(1 To 1)

If Not FindAttr(Stream(endsymptr), pos, attr) Then
warn "Keine Endposition vorhanden"
End If
Ende = attr.val

ReDim zustaende(1).pos.Element(UBound(Stream(rootsym).First))
For i = 0 To UBound(Stream(rootsym).First)
zustaende(1).pos.Element(i) = Stream(rootsym).First(i)
Next i

ReDim zustaende(1).tran(255)

i = 1
Do
For c = 0 To 255
If Not Schnittmenge_Leer(zustaende(i).pos, match(c)) Then
s_menge = Schnittmenge(zustaende(i).pos, match(c))
f = followpos(s_menge)
Init_IfNecessary f.Element
n = Menge_In_Zustaende(f)

If n > 0 Then
zustaende(i).tran(c) = n
Else
ReDim Preserve zustaende(1 To UBound(zustaende) + 1)
zustaende(UBound(zustaende)).pos = f
ReDim zustaende(UBound(zustaende)).tran(255)
zustaende(i).tran(c) = UBound(zustaende)

If Element_In_Menge(f, Ende) Then
zustaende(UBound(zustaende)).acc = True
End If
End If
End If
Next c

i = i + 1
If i > UBound(zustaende) Then
Exit Do
End If
Loop
End Sub

Private Sub Phase2()
Dim i As Long, n As Long, sym As Symbol, mat As Boolean
Dim attr As Sym_Attr

If eingabezeichen(0) = -1 Then
Exit Sub
End If

ReDim match(255)

For i = 0 To 255
Init_IfNecessary match(i).Element

For n = 0 To UBound(pos_ptr)
sym = Stream(pos_ptr(n))
mat = False
If sym.id = Char Then
mat = sym.lexeme = Chr$(i)
ElseIf sym.id = Meta Then
If FindAttr(sym, Klasse, attr) Then
If Element_In_Class(Chr$(i), attr.val) Then
mat = True
End If
Else
Debug.Print "Fehler in Phase 2"
End If
End If

FindAttr sym, pos, attr
If mat Then
AddElement match(i), attr.val
End If
Next n
Next i
End Sub

Private Function Phase1(root As Long) As Boolean
Dim i As Long, b_nullable() As Boolean, n As Long
Dim child0 As Symbol, child1 As Symbol

On Error Resume Next
i = Stream(root).Child(0)
If Err.Number <> 0 Then
Err.Clear
ReDim Stream(root).Child(0)
Stream(root).Child(0) = -1
End If

If UBound(Stream(root).Child) = 0 And Stream(root).Child(0) = _
-1 Then
SetAttr Stream(root), pos, cp
pos_ptr(cp) = root

SetAttr Stream(root), nullable, -1
Phase1 = False

ReDim Stream(root).First(0)
Stream(root).First(0) = cp

ReDim Stream(root).Last(0)
Stream(root).Last(0) = cp

If Not OnStack_String(sym_stack, Stream(root).lexeme) Then
push_s sym_stack, Stream(root).lexeme
eingabezeichen(ce) = root
ce = ce + 1
If ce > UBound(eingabezeichen) Then
ReDim Preserve eingabezeichen(UBound(eingabezeichen) * 2)
End If
End If

cp = cp + 1

If cp > UBound(pos_ptr) Then
ReDim Preserve pos_ptr(UBound(pos_ptr) * 2)
End If
End If

ReDim b_nullable(UBound(Stream(root).Child))
For i = 0 To UBound(Stream(root).Child)
b_nullable(i) = True
If Stream(root).Child(i) >= 0 Then
b_nullable(i) = Phase1(Stream(root).Child(i))
End If
Next i

child0 = Stream(Stream(root).Child(0))
child1 = Stream(Stream(root).Child(1))

Select Case Stream(root).id
Case Sym_ID.Meta_Or
Phase1 = b_nullable(0) Or b_nullable(1)
SetAttr Stream(root), nullable, IIf(Phase1, 1, -1)

Join_Arr child0.First, child1.First, Stream(root).First

Join_Arr child0.Last, child1.Last, Stream(root).Last
Case Sym_ID.MetaOp
If Stream(root).lexeme = "+" Then
Phase1 = False
Else
Phase1 = True
End If
SetAttr Stream(root), nullable, IIf(Phase1, 1, -1)

Stream(root).First = child0.First

Stream(root).Last = child0.Last

If Stream(root).lexeme = "+" Or Stream(root).lexeme = "*" Then
For i = 0 To UBound(child0.Last)
If child0.Last(i) >= 0 Then
For n = 0 To UBound(child0.First)
If child0.First(n) >= 0 Then
AddElement follow(child0.Last(i)), child0.First(n)
End If
Next n
End If
Next i
End If
Case Sym_ID.Cat
Phase1 = b_nullable(0) And b_nullable(1)
SetAttr Stream(root), nullable, IIf(Phase1, 1, -1)

If b_nullable(0) Then
Join_Arr child0.First, child1.First, Stream(root).First
Else
Stream(root).First = child0.First
End If

If b_nullable(1) Then
Join_Arr child0.Last, child1.Last, Stream(root).Last
Else
Stream(root).Last = child1.Last
End If

For i = 0 To UBound(child0.Last)
If child0.Last(i) >= 0 Then
For n = 0 To UBound(child1.First)
If child1.First(n) >= 0 Then
AddElement follow(child0.Last(i)), child1.First(n)
End If
Next n
End If
Next i
End Select
End Function

Private Sub Join_Arr(src1() As Long, src2() As Long, dest() As Long)
Dim i As Long

ReDim dest(UBound(src1) + UBound(src2) + 1)

For i = 0 To UBound(src1)
dest(i) = src1(i)
Next i

For i = 0 To UBound(src2)
dest(UBound(src1) + 1 + i) = src2(i)
Next i
End Sub

Public Sub AddElement(Menge As Menge, x As Long)
Dim i As Long

On Error Resume Next
i = Menge.Element(0)
If Err.Number <> 0 Then
Err.Clear
ReDim Menge.Element(0)
Menge.Element(0) = -1
End If

For i = 0 To UBound(Menge.Element)
If Menge.Element(i) = x Then
Exit Sub
End If
Next i

If Not (UBound(Menge.Element) = 0 And Menge.Element(0) = -1) Then
ReDim Preserve Menge.Element(UBound(Menge.Element) + 1)
End If

Menge.Element(UBound(Menge.Element)) = x
End Sub

Public Function CatElemente(x As Menge) As String
Dim i As Long

On Error Resume Next
i = x.Element(0)
If Err.Number <> 0 Then
Err.Clear
CatElemente = ""
Exit Function
End If

CatElemente = ""
For i = 0 To UBound(x.Element)
If Not x.Element(i) = -1 Then
CatElemente = CatElemente & CStr(x.Element(i)) & ", "
End If
Next i
CatElemente = Left$(CatElemente, Len(CatElemente) - 2)
End Function

Private Function Element_In_Class(a As String, class As Long) As Boolean
Dim i As Long, aa As Long

aa = Asc(a)
For i = 0 To UBound(classes(class).Element)
If classes(class).Element(i) = aa Then
Element_In_Class = True
Exit Function
End If
Next i
End Function

Public Sub init_classes(class() As Menge)
Dim i As Long, n As String

'\w
For i = Asc("a") To Asc("z")
AddElement class(1), i
Next i

For i = Asc("A") To Asc("Z")
AddElement class(1), i
Next i

n = "äöüÄÖÜß"
For i = 1 To Len(n)
AddElement class(1), Asc(Mid$(n, i, 1))
Next i

'\d
For i = Asc("0") To Asc("9")
AddElement class(2), i
Next i

'.
For i = 0 To Asc(vbLf) - 1
AddElement class(3), i
Next i

For i = Asc(vbLf) + 1 To 255
AddElement class(3), i
Next i

'\W
n = "abcdefghijklmnopqrstuvwxyzäöüßÄÖÜABCDEFGHIJKLMNOPQRSTUVWXYZ"
For i = 0 To 255
If InStr(n, Chr$(i)) = 0 Then
AddElement class(4), i
End If
Next i

'\D
For i = 0 To Asc("0") - 1
AddElement class(5), i
Next i

For i = Asc("9") + 1 To 255
AddElement class(5), i
Next i

'\s
AddElement class(6), Asc(" ")
AddElement class(6), Asc(vbTab)
AddElement class(6), Asc(vbLf)

'\S
n = " " & vbTab & vbLf
For i = 0 To 255
If InStr(n, Chr$(i)) = 0 Then
AddElement class(7), i
End If
Next i
End Sub

Private Function Schnittmenge_Leer(m1 As Menge, m2 As Menge) As Boolean
Dim i As Long, n As Long

For i = 0 To UBound(m1.Element)
For n = 0 To UBound(m2.Element)
If m1.Element(i) = m2.Element(n) Then
Schnittmenge_Leer = False
Exit Function
End If
Next n
Next i
Schnittmenge_Leer = True
End Function

Private Function Schnittmenge(m1 As Menge, m2 As Menge) As Menge
Dim i As Long, n As Long

For i = 0 To UBound(m1.Element)
For n = 0 To UBound(m2.Element)
If m1.Element(i) = m2.Element(n) Then
AddElement Schnittmenge, m1.Element(i)
End If
Next n
Next i

Init_IfNecessary Schnittmenge.Element
For i = UBound(Schnittmenge.Element) To 0 Step -1
For n = 0 To i - 1
If Schnittmenge.Element(n) > Schnittmenge.Element(i) Then
Schnittmenge.Element(n) = Schnittmenge.Element(n) Xor _
Schnittmenge.Element(i)
Schnittmenge.Element(i) = Schnittmenge.Element(n) Xor _
Schnittmenge.Element(i)
Schnittmenge.Element(n) = Schnittmenge.Element(n) Xor _
Schnittmenge.Element(i)
End If
Next n
Next i
End Function

Private Function Menge_In_Zustaende(m As Menge) As Long
Dim i As Long, n As Long, mat As Boolean

For i = 1 To UBound(zustaende)
If UBound(zustaende(i).pos.Element) = UBound(m.Element) Then
For n = 0 To UBound(zustaende(i).pos.Element)
mat = zustaende(i).pos.Element(n) = m.Element(n)
If Not mat Then
Exit For
End If
Next n
If mat Then
Menge_In_Zustaende = i: Exit Function
End If
End If
Next i
Menge_In_Zustaende = -1
End Function

Private Function followpos(m As Menge) As Menge
Dim i As Long, f As Menge, n As Long

For i = 0 To UBound(m.Element)
If Not m.Element(i) = -1 Then
f = follow(m.Element(i))
Init_IfNecessary f.Element
For n = 0 To UBound(f.Element)
If Not f.Element(n) = -1 Then
AddElement followpos, f.Element(n)
End If
Next n
End If
Next i

Init_IfNecessary followpos.Element
For i = UBound(followpos.Element) To 0 Step -1
For n = 0 To i - 1
If followpos.Element(n) > followpos.Element(i) Then
followpos.Element(n) = followpos.Element(n) Xor _
followpos.Element(i)
followpos.Element(i) = followpos.Element(n) Xor _
followpos.Element(i)
followpos.Element(n) = followpos.Element(n) Xor _
followpos.Element(i)
End If
Next n
Next i
End Function

Private Sub Init_IfNecessary(arr() As Long)
Dim i As Long

On Error Resume Next
i = arr(0)
If Err.Number <> 0 Then
Err.Clear
ReDim arr(0)
arr(0) = -1
End If
End Sub

Private Function Menge_Leer(m As Menge) As Boolean
Dim i As Long

On Error Resume Next
i = m.Element(0)
If Err.Number <> 0 Then
Err.Clear
Menge_Leer = True
Exit Function
End If

If UBound(m.Element) = 0 And m.Element(0) = -1 Then
Menge_Leer = True
Exit Function
End If

Menge_Leer = False
End Function

Public Function Element_In_Menge(m1 As Menge, e As Long) As Boolean
Dim i As Long, n As Long

If e = -1 Then
Exit Function
End If

Init_IfNecessary m1.Element
For i = 0 To UBound(m1.Element)
If m1.Element(i) = e Then
Element_In_Menge = True
Exit Function
End If
Next i
Element_In_Menge = False
End Function

Public Sub warn(x As String)
Debug.Print x
End Sub