mirror of
https://github.com/RoBaertschi/tt.git
synced 2025-04-18 23:13:29 +00:00
Initial Commit
This commit is contained in:
commit
6f9d64b2bf
75
ast/ast.go
Normal file
75
ast/ast.go
Normal file
@ -0,0 +1,75 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"robaertschi.xyz/robaertschi/tt/token"
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
TokenLiteral() string
|
||||
String() string
|
||||
}
|
||||
|
||||
type Declaration interface {
|
||||
Node
|
||||
declarationNode()
|
||||
}
|
||||
|
||||
type Expression interface {
|
||||
Node
|
||||
expressionNode()
|
||||
}
|
||||
|
||||
type Program struct {
|
||||
Declarations []Declaration
|
||||
}
|
||||
|
||||
func (p *Program) TokenLiteral() string {
|
||||
if len(p.Declarations) > 0 {
|
||||
return p.Declarations[0].TokenLiteral()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Program) String() string {
|
||||
var builder strings.Builder
|
||||
|
||||
for _, decl := range p.Declarations {
|
||||
builder.WriteString(decl.String())
|
||||
builder.WriteRune('\n')
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type FunctionDeclaration struct {
|
||||
Token token.Token // The token.FN
|
||||
Body Expression
|
||||
Name string
|
||||
}
|
||||
|
||||
func (fd *FunctionDeclaration) declarationNode() {}
|
||||
func (fd *FunctionDeclaration) TokenLiteral() string { return fd.Token.Literal }
|
||||
func (fd *FunctionDeclaration) String() string {
|
||||
return fmt.Sprintf("fn %v() = %v;", fd.Name, fd.Body.String())
|
||||
}
|
||||
|
||||
// Represents a Expression that we failed to parse
|
||||
type ErrorExpression struct {
|
||||
InvalidToken token.Token
|
||||
}
|
||||
|
||||
func (e *ErrorExpression) expressionNode() {}
|
||||
func (e *ErrorExpression) TokenLiteral() string { return e.InvalidToken.Literal }
|
||||
func (e *ErrorExpression) String() string { return "<ERROR>" }
|
||||
|
||||
type IntegerExpression struct {
|
||||
Token token.Token // The token.INT
|
||||
Value int64
|
||||
}
|
||||
|
||||
func (ie *IntegerExpression) expressionNode() {}
|
||||
func (ie *IntegerExpression) TokenLiteral() string { return ie.Token.Literal }
|
||||
func (ie *IntegerExpression) String() string { return ie.Token.Literal }
|
171
lexer/lexer.go
Normal file
171
lexer/lexer.go
Normal file
@ -0,0 +1,171 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"robaertschi.xyz/robaertschi/tt/token"
|
||||
)
|
||||
|
||||
type ErrorCallback func(token.Loc, string, ...any)
|
||||
|
||||
type Lexer struct {
|
||||
input string
|
||||
position int
|
||||
readPosition int
|
||||
ch rune
|
||||
|
||||
linePosition int
|
||||
lineCount int
|
||||
|
||||
errors int
|
||||
errorCallback ErrorCallback
|
||||
|
||||
file string
|
||||
}
|
||||
|
||||
func New(input string, file string) (*Lexer, error) {
|
||||
l := &Lexer{input: input, file: file}
|
||||
if err := l.readChar(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l *Lexer) Iter() iter.Seq[token.Token] {
|
||||
return func(yield func(token.Token) bool) {
|
||||
for {
|
||||
if !yield(l.NextToken()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) WithErrorCallback(errorCallback ErrorCallback) {
|
||||
l.errorCallback = errorCallback
|
||||
}
|
||||
|
||||
func (l *Lexer) loc() token.Loc {
|
||||
return token.Loc{
|
||||
Line: l.lineCount,
|
||||
Col: l.position - l.linePosition,
|
||||
Pos: l.position,
|
||||
File: l.file,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) NextToken() token.Token {
|
||||
l.skipWhitespace()
|
||||
var tok token.Token
|
||||
tok.Loc = l.loc()
|
||||
|
||||
switch l.ch {
|
||||
case ';':
|
||||
tok = l.newToken(token.SEMICOLON)
|
||||
case '=':
|
||||
tok = l.newToken(token.EQUAL)
|
||||
case '(':
|
||||
tok = l.newToken(token.OPEN_PAREN)
|
||||
case ')':
|
||||
tok = l.newToken(token.CLOSE_PAREN)
|
||||
case -1:
|
||||
tok.Literal = ""
|
||||
tok.Type = token.EOF
|
||||
default:
|
||||
if isNumber(l.ch) {
|
||||
tok.Literal = l.readInteger()
|
||||
tok.Type = token.INT
|
||||
return tok
|
||||
} else if unicode.IsLetter(l.ch) {
|
||||
tok.Literal = l.readIdentifier()
|
||||
tok.Type = token.LookupKeyword(tok.Literal)
|
||||
return tok
|
||||
} else {
|
||||
if l.errorCallback != nil {
|
||||
l.errorCallback(tok.Loc, "Unknown character %r", l.ch)
|
||||
}
|
||||
tok = l.newToken(token.ILLEGAL)
|
||||
}
|
||||
}
|
||||
if err := l.readChar(); err != nil {
|
||||
if l.errorCallback != nil {
|
||||
l.errorCallback(tok.Loc, "%v", err.Error())
|
||||
}
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
func (l *Lexer) newToken(t token.TokenType) token.Token {
|
||||
return token.Token{
|
||||
Type: t,
|
||||
Literal: string(l.ch),
|
||||
Loc: l.loc(),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) readChar() (err error) {
|
||||
if l.readPosition < len(l.input) {
|
||||
l.position = l.readPosition
|
||||
if l.ch == '\n' {
|
||||
l.linePosition = l.position
|
||||
l.lineCount += 1
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.readPosition:])
|
||||
if r == utf8.RuneError && w == 1 {
|
||||
err = fmt.Errorf("Found illegal UTF-8 encoding")
|
||||
} else if r == '\uFEFF' && l.position > 0 {
|
||||
err = fmt.Errorf("Found illegal BOM")
|
||||
}
|
||||
l.readPosition += w
|
||||
l.ch = r
|
||||
} else {
|
||||
l.position = len(l.input)
|
||||
if l.ch == '\n' {
|
||||
l.linePosition = l.position
|
||||
l.lineCount += 1
|
||||
}
|
||||
l.ch = -1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lexer) readIdentifier() string {
|
||||
startPos := l.position
|
||||
|
||||
for unicode.IsLetter(l.ch) || isNumber(l.ch) || l.ch == '_' {
|
||||
l.readChar()
|
||||
}
|
||||
|
||||
return l.input[startPos:l.position]
|
||||
}
|
||||
|
||||
func (l *Lexer) readInteger() string {
|
||||
startPos := l.position
|
||||
|
||||
for isNumber(l.ch) {
|
||||
l.readChar()
|
||||
}
|
||||
|
||||
return l.input[startPos:l.position]
|
||||
}
|
||||
|
||||
func isNumber(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9'
|
||||
}
|
||||
|
||||
func (l *Lexer) skipWhitespace() {
|
||||
for unicode.IsSpace(l.ch) {
|
||||
l.readChar()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) error(loc token.Loc, format string, args ...any) {
|
||||
if l.errorCallback != nil {
|
||||
l.errorCallback(loc, format, args)
|
||||
}
|
||||
|
||||
l.errors += 1
|
||||
}
|
55
lexer/lexer_test.go
Normal file
55
lexer/lexer_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"robaertschi.xyz/robaertschi/tt/token"
|
||||
)
|
||||
|
||||
type lexerTest struct {
|
||||
input string
|
||||
expectedToken []token.Token
|
||||
}
|
||||
|
||||
func runLexerTest(t *testing.T, test lexerTest) {
|
||||
t.Helper()
|
||||
|
||||
l, err := New(test.input, "test.tt")
|
||||
l.WithErrorCallback(func(l token.Loc, s string, a ...any) {
|
||||
format := fmt.Sprintf(s, a)
|
||||
t.Errorf("Lexer error callback called: %s:%d:%d %s", l.File, l.Line, l.Col, format)
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("creating lexer failed: %v", err)
|
||||
}
|
||||
|
||||
for i, expectedToken := range test.expectedToken {
|
||||
actualToken := l.NextToken()
|
||||
t.Logf("expected: %v, got: %v", expectedToken, actualToken)
|
||||
|
||||
if expectedToken.Literal != actualToken.Literal {
|
||||
t.Errorf("%d: expected literal %q, got %q", i, expectedToken.Literal, actualToken.Literal)
|
||||
}
|
||||
|
||||
if expectedToken.Type != actualToken.Type {
|
||||
t.Errorf("%d: expected type %q, got %q", i, expectedToken.Type, actualToken.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicFunctionality(t *testing.T) {
|
||||
runLexerTest(t, lexerTest{
|
||||
input: "fn main() = 0;",
|
||||
expectedToken: []token.Token{
|
||||
{Type: token.FN, Literal: "fn"},
|
||||
{Type: token.IDENT, Literal: "main"},
|
||||
{Type: token.OPEN_PAREN, Literal: "("},
|
||||
{Type: token.CLOSE_PAREN, Literal: ")"},
|
||||
{Type: token.EQUAL, Literal: "="},
|
||||
{Type: token.INT, Literal: "0"},
|
||||
{Type: token.SEMICOLON, Literal: ";"},
|
||||
{Type: token.EOF, Literal: ""},
|
||||
},
|
||||
})
|
||||
}
|
18
parser/parser.go
Normal file
18
parser/parser.go
Normal file
@ -0,0 +1,18 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"robaertschi.xyz/robaertschi/tt/lexer"
|
||||
"robaertschi.xyz/robaertschi/tt/token"
|
||||
)
|
||||
|
||||
type ErrorCallback func(token.Token, string, ...any)
|
||||
|
||||
type Parser struct {
|
||||
lexer lexer.Lexer
|
||||
|
||||
curToken token.Token
|
||||
peekToken token.Token
|
||||
|
||||
errors int
|
||||
errorCallback ErrorCallback
|
||||
}
|
43
token/token.go
Normal file
43
token/token.go
Normal file
@ -0,0 +1,43 @@
|
||||
package token
|
||||
|
||||
type Loc struct {
|
||||
Line int
|
||||
Col int
|
||||
Pos int
|
||||
File string
|
||||
}
|
||||
|
||||
type TokenType string
|
||||
|
||||
type Token struct {
|
||||
Type TokenType
|
||||
Literal string
|
||||
Loc Loc
|
||||
}
|
||||
|
||||
var keywords = map[string]TokenType{
|
||||
"fn": FN,
|
||||
}
|
||||
|
||||
const (
|
||||
ILLEGAL TokenType = "ILLEGAL"
|
||||
EOF TokenType = "EOF"
|
||||
|
||||
IDENT TokenType = "IDENT"
|
||||
INT TokenType = "INT"
|
||||
|
||||
SEMICOLON = ";"
|
||||
EQUAL = "="
|
||||
OPEN_PAREN = "("
|
||||
CLOSE_PAREN = ")"
|
||||
|
||||
// Keywords
|
||||
FN = "FN"
|
||||
)
|
||||
|
||||
func LookupKeyword(literal string) TokenType {
|
||||
if value, ok := keywords[literal]; ok {
|
||||
return value
|
||||
}
|
||||
return IDENT
|
||||
}
|
1
utils/utils.go
Normal file
1
utils/utils.go
Normal file
@ -0,0 +1 @@
|
||||
package utils
|
Loading…
x
Reference in New Issue
Block a user