Skip to main content
2 of 2
added 4 characters in body
Algo
  • 339
  • 2
  • 11

Lexer for an experimental tiny language in Go

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?

Algo
  • 339
  • 2
  • 11