Skip to content

Commit

Permalink
Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ro5bert committed Oct 2, 2019
1 parent 702ce3d commit a7598db
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 0 deletions.
13 changes: 13 additions & 0 deletions lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type lexerResult struct {
err error
}

// statefn is a state combined with an associated action. See Rob Pike's talk on lexical scanning.
type statefn func(byte, *lexer) (lexemeType, statefn, error)

type lexer struct {
Expand All @@ -69,6 +70,8 @@ type lexer struct {
allowEOF bool
}

// removeAllWS removes all the whitespace from a string and returns the new string, where whitespace is identified
// according to unicode.IsSpace.
func removeAllWS(str string) string {
var b strings.Builder
b.Grow(len(str))
Expand All @@ -80,6 +83,7 @@ func removeAllWS(str string) string {
return b.String()
}

// lex lexes the given string in a separate goroutine and outputs the resultant lexerResults over the returned channel.
func lex(input string) chan lexerResult {
l := &lexer{
input: removeAllWS(input),
Expand All @@ -91,6 +95,7 @@ func lex(input string) chan lexerResult {
return l.c
}

// run is the main loop for a lexer. It should be called in a separate goroutine.
func (l *lexer) run() {
for sfn := lexStatement; sfn != nil; {
n, eof := l.next()
Expand All @@ -113,6 +118,8 @@ func (l *lexer) run() {
close(l.c)
}

// next returns the next byte in the input string. The boolean return value indicates if the end of the string was
// reached (i.e. EOF); if it is true, the byte return value should be disregarded.
func (l *lexer) next() (byte, bool) {
if l.nextIdx == len(l.input) {
return 0, true
Expand All @@ -123,11 +130,14 @@ func (l *lexer) next() (byte, bool) {
return next, false
}

// nest increments nestCnt and sets allowEOF as appropriate.
func (l *lexer) nest() {
l.nestCnt++
l.allowEOF = false
}

// denest decrements nestCnt and sets allowEOF as appropriate. The boolean return value indicates success; false is
// returned if nestCnt was already zero when denest was called (i.e. parentheses not matched).
func (l *lexer) denest() bool {
if l.nestCnt == 0 {
// false return indicates failure.
Expand All @@ -138,6 +148,8 @@ func (l *lexer) denest() bool {
return true
}

// lexStatement is a statefn for parsing the start of a statement (this includes opening parentheses, "0", "1", and
// letters) or negation.
func lexStatement(n byte, l *lexer) (lexemeType, statefn, error) {
// By default, allow EOF if there are no unmatched parentheses.
// Some branches in the below switch set the allowEOF flag based on other conditions.
Expand All @@ -160,6 +172,7 @@ func lexStatement(n byte, l *lexer) (lexemeType, statefn, error) {
return 0, nil, fmt.Errorf("unexpected char '%c'; expected '%c', '(', '0', '1', or a statement", n, negateSym)
}

// lexOperator is a statefn for parsing a binary operator or a closing parenthesis.
func lexOperator(n byte, l *lexer) (lexemeType, statefn, error) {
switch n {
case ')':
Expand Down
29 changes: 29 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
// vera is a package for parsing logical expressions.
package vera

import (
"fmt"
"strings"
)

// alphaToIdx maps the given byte corresponding to an English letter to the range [0, 51] (e.g 'A' is mapped to 0 and
// 'z' is mapped to 51).
func alphaToIdx(ascii byte) byte {
if ascii >= 'a' {
ascii -= 6
}
return ascii - 65
}

// idxToAlpha does the reverse of alphaToIdx.
func idxToAlpha(idx byte) byte {
if idx >= 26 {
idx += 6
}
return idx + 65
}

// Truth represents a set of truth values.
// The truth values are represented by Val, which is treated like a bit field where each bit represents whether the
// corresponding atomic statement is true (1) or false (0). The bits are in alphabetical order such that, if all 52
// atomic statements are used, the 0th bit corresponds to 'z' and the 51st bit corresponds to 'A'. However, if some of
// the 52 possible atomic statements are not used, they will not be included in the bit field (e.g if only 'a' and 'G'
// are used, the 0th bit will correspond to 'a' and the 1st bit will correspond to 'G'; the remaining bits are
// meaningless).
// As a result of the truth values being represented as a uint64, it is very easy to iterate over all possible truth
// values for a statement; for example:
// stmt, t, err := vera.Parse(...)
// // check err
// for t.Val = 0; t.Val < 1 << len(t.Names); t.Val++ {
// // Do something with t such as call stmt.Eval.
// }
// If the names associated with each bit value are needed, they are stored in the Names slice which uses the same
// indexing scheme as the bits in Val (e.g. t.Val&(1<<i)>0 accesses the value of the statement named t.Names[i] for some
// Truth t and integer i < len(t.Names)).
type Truth struct {
Val uint64
shiftMap *[52]byte
Names []byte
}

// get returns the value of the given atomic statement for this set of truth values.
func (t Truth) get(stmt byte) bool {
return t.Val&(1<<t.shiftMap[alphaToIdx(stmt)]) > 0
}
Expand Down Expand Up @@ -68,13 +90,16 @@ func newTruth(atomics uint64) Truth {
return Truth{0, &shiftMap, names}
}

// operator represents a binary logical operator.
type operator func(bool, bool) bool

type Stmt interface {
fmt.Stringer
Eval(Truth) bool
}

// surroundIfBinary returns the string representation of the given Stmt and surrounds it in parentheses if it is a
// binaryStmt.
func surroundIfBinary(s Stmt) string {
if _, ok := s.(binaryStmt); ok {
return "(" + s.String() + ")"
Expand Down Expand Up @@ -159,6 +184,7 @@ func bicond(left bool, right bool) bool {
return left == right
}

// byteToOp takes an operator symbol in the form of a byte and returns the associated operator function.
func byteToOp(b byte) operator {
switch b {
case andSym:
Expand All @@ -177,11 +203,14 @@ func byteToOp(b byte) operator {
}
}

// Parse parses the given input string, returning a Stmt which can then be evaluated at certain sets of truth values
// using the given Truth. An error is also returned in the case of failure.
func Parse(input string) (Stmt, Truth, error) {
stmt, atomics, err := parseRecursive(lex(input))
return stmt, newTruth(atomics), err
}

// stmtBuilder is used internally inside parseRecursive to manage negations.
type stmtBuilder struct {
inner Stmt
negated bool
Expand Down
14 changes: 14 additions & 0 deletions truthtable.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
)

// CharSet is a set of characters for rendering a table via RenderTT.
type CharSet struct {
RowSep string
ColSep string
Expand All @@ -22,6 +23,7 @@ type CharSet struct {
BRCorner string
}

// PrettyBoxCS is a CharSet using Unicode box drawing characters.
var PrettyBoxCS = &CharSet{
RowSep: "─",
ColSep: "│",
Expand All @@ -36,6 +38,7 @@ var PrettyBoxCS = &CharSet{
BRCorner: "┘",
}

// ASCIIBoxCS is a CharSet using only ASCII characters.
var ASCIIBoxCS = &CharSet{
RowSep: "-",
ColSep: "|",
Expand All @@ -51,6 +54,9 @@ var ASCIIBoxCS = &CharSet{
}

// TODO: improve customizability

// RenderTT writes a truth table for the given Stmt/Truth pair to the given io.Writer. The appearance of the table is
// dictated by the given CharSet and colorize parameter.
func RenderTT(stmt Stmt, truth Truth, out io.Writer, cs *CharSet, colorize bool) error {
color.NoColor = !colorize
if len(truth.Names) == 0 {
Expand Down Expand Up @@ -79,18 +85,22 @@ func RenderTT(stmt Stmt, truth Truth, out io.Writer, cs *CharSet, colorize bool)
return nil
}

// printTopLine draws the top line in the table (i.e. above the header).
func printTopLine(nAtomics int, outputWidth int, out io.Writer, cs *CharSet) error {
return printLine(nAtomics, outputWidth, out, cs.RowSep, cs.TLCorner, cs.TopT, cs.TRCorner)
}

// printHeaderLine draws the line between the header and the data in the table.
func printHeaderLine(nAtomics int, outputWidth int, out io.Writer, cs *CharSet) error {
return printLine(nAtomics, outputWidth, out, cs.RowSep, cs.LeftT, cs.Center, cs.RightT)
}

// printBottomLine draws the bottom line in the table (i.e. below the data).
func printBottomLine(nAtomics int, outputWidth int, out io.Writer, cs *CharSet) error {
return printLine(nAtomics, outputWidth, out, cs.RowSep, cs.BLCorner, cs.BottomT, cs.BRCorner)
}

// calcInputWidth calculates the total width of all the input columns given the number of atomic statements.
func calcInputWidth(nAtomics int) int {
return nAtomics + 2*(nAtomics-1)
}
Expand All @@ -106,6 +116,8 @@ func printLine(nAtomics int, outputWidth int, out io.Writer, rowSep string, l st
return err
}

// printHeader prints the header, consisting of the names of the atomic statements and a nicely-formatted version of the
// original input statement.
func printHeader(atomics []byte, stmt string, out io.Writer, cs *CharSet) error {
var sb strings.Builder
sb.Grow(calcInputWidth(len(atomics)))
Expand All @@ -118,11 +130,13 @@ func printHeader(atomics []byte, stmt string, out io.Writer, cs *CharSet) error
return printRow(sb.String(), stmt, out, cs)
}

// centerText centers the given ASCII string in spaces such that the returned string has length >= width.
func centerText(text string, width int) string {
// Assumes text is ASCII.
return fmt.Sprintf("%[1]*s", -width, fmt.Sprintf("%[1]*s", (width+len(text))/2, text))
}

// printData prints a single row of truth values and their associated output.
func printData(truth uint64, nAtomics int, output bool, outputWidth int, out io.Writer, cs *CharSet) error {
var sb strings.Builder
for i := nAtomics - 1; i >= 0; i-- {
Expand Down

0 comments on commit a7598db

Please sign in to comment.