I am implementing a lexer for an experimental language by the name "Phoenix", the language supports four primary arithmetic expression for integers only (add, subtract, multiply, divide), variable assignment and print statement.
Typical input:
# this is a comment
value = 1 + 7 / 4 * (30)
print('Value = ', value)
lexer.go:
package lexer
import (
"bytes"
"fmt"
"io"
"os"
"phoenix/log"
"phoenix/token"
"unicode"
)
// Lexer type for our lexer
type Lexer struct {
SourceFileName string
fh *os.File
UnScannedChar byte
CurrentLine, CurrentCol uint64
}
// New sets up the lexer
func (l *Lexer) New(fileName string) {
tmpFh, err := os.Open(fileName)
l.fh = tmpFh
if err != nil {
log.Error(fmt.Sprintf("couldn't open file '%v'", fileName))
os.Exit(0)
}
l.SourceFileName = fileName
l.CurrentLine = 1
}
// NextChar updates Lexer.unScannedChar
func (l *Lexer) NextChar() (err error) {
tempChar := make([]byte, 1)
_, er := l.fh.Read(tempChar)
if er != nil {
if er == io.EOF {
err = io.EOF
return
}
log.Error(fmt.Sprintf("error while reading from file '%v'",
l.SourceFileName))
os.Exit(0)
}
l.UnScannedChar = tempChar[0]
if l.UnScannedChar == '\n' {
l.CurrentLine++
l.CurrentCol = 0
} else {
l.CurrentCol++
}
return
}
func (l *Lexer) isIdentifierStart() bool {
return unicode.IsLetter(rune(l.UnScannedChar)) ||
l.UnScannedChar == '_'
}
func (l *Lexer) isIdentifierPart() bool {
return l.isIdentifierStart() || unicode.IsNumber(rune(l.UnScannedChar))
}
func (l *Lexer) scanInteger() (newToken token.Token) {
var buffer bytes.Buffer
buffer.WriteString(string(l.UnScannedChar))
l.NextChar()
for unicode.IsDigit(rune(l.UnScannedChar)) {
buffer.WriteString(string(l.UnScannedChar))
l.NextChar()
}
newToken.Rep = token.INTEGER
newToken.Data = buffer.String()
return
}
func (l *Lexer) scanID() (newToken token.Token) {
var buffer bytes.Buffer
buffer.WriteString(string(l.UnScannedChar))
l.NextChar()
for l.isIdentifierPart() {
buffer.WriteString(string(l.UnScannedChar))
l.NextChar()
}
newToken.Data = buffer.String()
switch newToken.Data {
case "print":
newToken.Rep = token.PRINT
default:
newToken.Rep = token.ID
}
/* we need to seek back the reading cursor as the next call of NextToken()
will invoke NextChar(), neglecting the current unscanned character */
l.fh.Seek(-1, os.SEEK_CUR)
return
}
func (l *Lexer) scanString() (newToken token.Token) {
var buffer bytes.Buffer
l.NextChar()
for l.UnScannedChar != '\'' {
buffer.WriteString(string(l.UnScannedChar))
l.NextChar()
}
newToken.Rep = token.STRING
newToken.Data = buffer.String()
return
}
// NextToken scans the Lexer.UnScannedChar and creates its correspondant token
func (l *Lexer) NextToken() (newToken token.Token) {
err := l.NextChar()
if err == io.EOF {
newToken.Rep = token.EOF
return
}
// scan integers
if unicode.IsDigit(rune(l.UnScannedChar)) {
return l.scanInteger()
}
// scan identifiers and print keyword
if l.isIdentifierStart() {
return l.scanID()
}
// scan strings
if l.UnScannedChar == '\'' {
return l.scanString()
}
// skip comments
if l.UnScannedChar == '#' {
err := l.NextChar()
for err != io.EOF && l.UnScannedChar != '\n' {
err = l.NextChar()
if err == io.EOF {
newToken.Rep = token.EOF
return
}
}
return l.NextToken()
}
switch l.UnScannedChar {
case '\f', '\t', '\r', ' ':
return l.NextToken()
case '\n':
newToken.Rep = token.NEWLINE
case '(':
newToken.Rep = token.LPAREN
case ')':
newToken.Rep = token.RPAREN
case ',':
newToken.Rep = token.COMMA
case '+':
newToken.Rep = token.ADD
case '-':
newToken.Rep = token.SUB
case '*':
newToken.Rep = token.MUL
case '/':
newToken.Rep = token.DIV
case '=':
newToken.Rep = token.ASSIGN
default:
log.Error(fmt.Sprintf(
"at file %v, line %v: unknown character '%c'", l.SourceFileName,
l.CurrentLine, l.UnScannedChar))
newToken.Rep = token.UNKNOWN
}
return
}
token.go:
package token
const (
SINGLEQ rune = iota
LPAREN
RPAREN
COMMA
ASSIGN
ADD
SUB
MUL
DIV
STRING
INTEGER
REAL
ID
PRINT
NEWLINE
EOF
UNKNOWN
)
type Token struct {
Rep rune
Data string
}
func (t Token) String() string {
switch t.Rep {
case SINGLEQ:
return "'"
case LPAREN:
return "("
case RPAREN:
return ")"
case COMMA:
return ","
case ASSIGN:
return "="
case ADD:
return "+"
case SUB:
return "-"
case MUL:
return "*"
case DIV:
return "/"
case STRING:
return "string"
case INTEGER:
return "integer"
case ID:
return "identifier"
case PRINT:
return "print"
case NEWLINE:
return "newline"
case EOF:
return "EOF"
}
return "unknown"
}
I need the lexer to be as fast as possible and the code should be flexible to implement more extensions/features for the language.
So how can it be improved?