Skip to content

Commit

Permalink
Implement FIRST and FOLLOW functions for context-free grammars
Browse files Browse the repository at this point in the history
  • Loading branch information
moorara committed Dec 29, 2024
1 parent c85f5ad commit 3e92dda
Show file tree
Hide file tree
Showing 13 changed files with 1,309 additions and 315 deletions.
268 changes: 227 additions & 41 deletions grammar/cfg.go

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions grammar/cfg_first.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package grammar

import (
"github.com/moorara/algo/set"
. "github.com/moorara/algo/symboltable"
)

var eqTerminalsAndEmpty = func(lhs, rhs *TerminalsAndEmpty) bool {
return lhs.Terminals.Equals(rhs.Terminals) && lhs.IncludesEmpty == rhs.IncludesEmpty
}

// FIRST is the FIRST function associated with a context-free grammar.
//
// FIRST(α), where α is any string of grammar symbols (terminals and non-terminals),
// is the set of terminals that begin strings derived from α.
// If α ⇒* ε, then ε is also in FIRST(α).
type FIRST func(String[Symbol]) TerminalsAndEmpty

// TerminalsAndEmpty is the return type for the FIRST function.
//
// It contains:
//
// - A set of terminals that may appear at the beginning of strings derived from α.
// - A flag indicating whether the empty string ε is included in the FIRST set..
type TerminalsAndEmpty struct {
Terminals set.Set[Terminal]
IncludesEmpty bool
}

func newTerminalsAndEmpty(terms ...Terminal) *TerminalsAndEmpty {
return &TerminalsAndEmpty{
Terminals: set.New(eqTerminal, terms...),
IncludesEmpty: false,
}
}

// firstBySymbolTable is the type for a table that stores the FIRST set for each grammar symbol.
type firstBySymbolTable SymbolTable[Symbol, *TerminalsAndEmpty]

func newFirstBySymbolTable() firstBySymbolTable {
return NewQuadraticHashTable(hashSymbol, eqSymbol, eqTerminalsAndEmpty, HashOpts{})
}

// firstByStringTable is the type for a table that stores the FIRST set for strings of grammar symbols.
type firstByStringTable SymbolTable[String[Symbol], *TerminalsAndEmpty]

func newFirstByStringTable() firstByStringTable {
return NewQuadraticHashTable(hashString, eqString, eqTerminalsAndEmpty, HashOpts{})
}
42 changes: 42 additions & 0 deletions grammar/cfg_first_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package grammar

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewTerminalsAndEmpty(t *testing.T) {
tests := []struct {
name string
terms []Terminal
}{
{
name: "OK",
terms: []Terminal{"a", "b", "c", "d", "e", "f"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
f := newTerminalsAndEmpty(tc.terms...)
assert.NotNil(t, f)
assert.True(t, f.Terminals.Contains(tc.terms...))
assert.False(t, f.IncludesEmpty)
})
}
}

func TestNewFirstBySymbolTable(t *testing.T) {
t.Run("OK", func(t *testing.T) {
firstBySymbol := newFirstBySymbolTable()
assert.NotNil(t, firstBySymbol)
})
}

func TestNewFirstByStringTable(t *testing.T) {
t.Run("OK", func(t *testing.T) {
firstByString := newFirstByStringTable()
assert.NotNil(t, firstByString)
})
}
43 changes: 43 additions & 0 deletions grammar/cfg_follow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package grammar

import (
"github.com/moorara/algo/set"
. "github.com/moorara/algo/symboltable"
)

var eqTerminalsAndEndmarker = func(lhs, rhs *TerminalsAndEndmarker) bool {
return lhs.Terminals.Equals(rhs.Terminals) && lhs.IncludesEndmarker == rhs.IncludesEndmarker
}

// FOLLOW is the FIRST function associated with a context-free grammar.
//
// FOLLOW(A), for non-terminal A, is the set of terminals 𝑎
// that can appear immediately to the right of A in some sentential form;
// that is; the set of terminals 𝑎 such that there exists a derivation of the form S ⇒* αAaβ
// for some α and β strings of grammar symbols (terminals and non-terminals).
type FOLLOW func(NonTerminal) TerminalsAndEndmarker

// TerminalsAndEndmarker is the return type for the FOLLOW function.
//
// It contains:
//
// - A set of terminals that can appear immediately after the given non-terminal.
// - A flag indicating whether the special endmarker symbol is included in the FOLLOW set.
type TerminalsAndEndmarker struct {
Terminals set.Set[Terminal]
IncludesEndmarker bool
}

func newTerminalsAndEndmarker(terms ...Terminal) *TerminalsAndEndmarker {
return &TerminalsAndEndmarker{
Terminals: set.New(eqTerminal, terms...),
IncludesEndmarker: false,
}
}

// followTable is the type for a table that stores the FOLLOW set for each non-terminal.
type followTable SymbolTable[NonTerminal, *TerminalsAndEndmarker]

func newFollowTable() followTable {
return NewQuadraticHashTable(hashNonTerminal, eqNonTerminal, eqTerminalsAndEndmarker, HashOpts{})
}
35 changes: 35 additions & 0 deletions grammar/cfg_follow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package grammar

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewTerminalsAndEndmarker(t *testing.T) {
tests := []struct {
name string
terms []Terminal
}{
{
name: "OK",
terms: []Terminal{"a", "b", "c", "d", "e", "f"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
f := newTerminalsAndEndmarker(tc.terms...)
assert.NotNil(t, f)
assert.True(t, f.Terminals.Contains(tc.terms...))
assert.False(t, f.IncludesEndmarker)
})
}
}

func TestNewFollowTable(t *testing.T) {
t.Run("OK", func(t *testing.T) {
follow := newFollowTable()
assert.NotNil(t, follow)
})
}
Loading

0 comments on commit 3e92dda

Please sign in to comment.