This commit is contained in:
Greg 2024-10-03 13:07:17 +03:00
parent 7e49d405f5
commit c12d2047b1
3 changed files with 411 additions and 380 deletions

93
ast.go Normal file
View file

@ -0,0 +1,93 @@
package main
import (
"fmt"
"strings"
)
type Expr interface {
expr()
accept(v Visitor)
}
type Unary struct {
op Token
right Expr
}
func (u *Unary) expr() {}
func (u *Unary) accept(v Visitor) {
v.visitUnary(u)
}
type Grouping struct {
expression Expr
}
func (g *Grouping) expr() {}
func (g *Grouping) accept(v Visitor) {
v.visitGrouping(g)
}
type Literal struct {
value any
}
func (l *Literal) expr() {}
func (l *Literal) accept(v Visitor) {
v.visitLiteral(l)
}
type Binary struct {
left Expr
op Token
right Expr
}
func (b *Binary) expr() {}
func (b *Binary) accept(v Visitor) {
v.visitBinary(b)
}
type Visitor interface {
visitBinary(b *Binary)
visitLiteral(l *Literal)
visitGrouping(g *Grouping)
visitUnary(u *Unary)
}
type AstStringer struct {
str strings.Builder
}
func (as AstStringer) String(expr Expr) string {
expr.accept(&as)
return as.str.String()
}
func (as *AstStringer) visitBinary(b *Binary) {
as.str.WriteString("(")
as.str.WriteString(b.op.lexeme)
as.str.WriteString(" ")
b.left.accept(as)
as.str.WriteString(" ")
b.right.accept(as)
as.str.WriteString(")")
}
func (as *AstStringer) visitLiteral(l *Literal) {
as.str.WriteString(fmt.Sprintf("%v", l.value))
}
func (as *AstStringer) visitGrouping(g *Grouping) {
as.str.WriteString("(group ")
g.expression.accept(as)
as.str.WriteString(")")
}
func (as *AstStringer) visitUnary(u *Unary) {
as.str.WriteString(fmt.Sprintf("(%s ", u.op.lexeme))
u.right.accept(as)
as.str.WriteString(")")
}

396
glox.go
View file

@ -2,39 +2,31 @@ package main
import ( import (
"bufio" "bufio"
"fmt"
"log" "log"
"os" "os"
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
) )
func main() { func main() {
expr := &Binary{ expr := &Binary{
left: &Literal{value: 1}, &Unary{Token{MINUS, "-", nil, 1}, &Literal{123}},
op: Token{typ: PLUS, lexeme: "+", literal: "", line: 1}, Token{STAR, "*", nil, 1},
right: &Binary{ &Grouping{&Grouping{&Binary{
left: &Literal{value: 3}, &Unary{Token{MINUS, "-", nil, 1}, &Literal{123}},
op: Token{typ: PLUS, lexeme: "+", literal: "", line: 1}, Token{STAR, "*", nil, 1},
right: &Literal{value: 4}, &Grouping{&Grouping{&Literal{45.67}}}}}},
},
} }
println((&AstStringer{}).String(expr)) println(AstStringer{}.String(expr))
// switch len(os.Args) { switch len(os.Args) {
// case 1: case 1:
// runPrompt() runPrompt()
// case 2: case 2:
// runFile(os.Args[1]) runFile(os.Args[1])
// default: default:
// println("Usage: glox [file]") println("Usage: glox [file]")
// os.Exit(1) os.Exit(1)
// } }
} }
func runPrompt() { func runPrompt() {
@ -73,362 +65,6 @@ func run(source []byte) {
} }
} }
var hadError = false
//go:generate go run golang.org/x/tools/cmd/stringer -type=TokenType
type TokenType int
const (
// one char
LEFT_PAREN TokenType = iota
RIGHT_PAREN
LEFT_BRACE
RIGHT_BRACE
COMMA
DOT
MINUS
PLUS
SEMICOLON
SLASH
STAR
// one or two chars
BANG
BANG_EQUAL
EQUAL
EQUAL_EQUAL
GREATER
GREATER_EQUAL
LESS
LESS_EQUAL
// Literals
IDENTIFIER
STRING
NUMBER
// keywords
AND
CLASS
ELSE
FALSE
FUN
FOR
IF
NIL
OR
PRINT
RETURN
SUPER
THIS
TRUE
VAR
WHILE
EOF
)
var keywords = map[string]TokenType{
"and": AND,
"class": CLASS,
"else": ELSE,
"false": FALSE,
"for": FOR,
"fun": FUN,
"if": IF,
"nil": NIL,
"or": OR,
"print": PRINT,
"return": RETURN,
"super": SUPER,
"this": THIS,
"true": TRUE,
"var": VAR,
"while": WHILE,
}
type Expr interface {
expr()
accept(v Visitor)
}
type Unary struct {
op Token
right Expr
}
type Grouping struct {
expr Expr
}
type Literal struct {
value any
}
func (l *Literal) expr() {}
func (l *Literal) accept(v Visitor) {
v.visitLiteral(l)
}
type Binary struct {
left Expr
op Token
right Expr
}
func (b *Binary) expr() {}
func (b *Binary) accept(v Visitor) {
v.visitBinary(b)
}
type Visitor interface {
visitBinary(b *Binary)
visitLiteral(l *Literal)
}
type AstStringer struct {
str strings.Builder
}
func (as *AstStringer) String(expr Expr) string {
expr.accept(as)
return as.str.String()
}
func (as *AstStringer) visitBinary(b *Binary) {
as.str.WriteString("(")
as.str.WriteString(b.op.lexeme)
as.str.WriteString(" ")
b.left.accept(as)
as.str.WriteString(" ")
b.right.accept(as)
as.str.WriteString(")")
}
func (as *AstStringer) visitLiteral(l *Literal) {
as.str.WriteString(fmt.Sprintf("%v", l.value))
}
type Token struct {
typ TokenType
lexeme string
literal any
line int
}
func (t *Token) String() string {
return fmt.Sprintf("%s - %s - %v", t.typ, t.lexeme, t.literal)
}
type Scanner struct {
source []byte
tokens []Token
start int
current int
line int
}
func newScanner(source []byte) *Scanner {
return &Scanner{source: source, start: 0, current: 0, line: 1}
}
func (s *Scanner) scan() []Token {
for !s.isAtEnd() {
s.start = s.current
s.scanToken()
}
s.tokens = append(s.tokens, Token{EOF, "EOF", struct{}{}, s.line})
return s.tokens
}
func (s *Scanner) printError(message string) {
fmt.Printf("[line %d] Error: %s\n", s.line, message)
hadError = true
}
func (s *Scanner) scanToken() {
c := s.advance()
switch c {
case '(':
s.addToken(LEFT_PAREN, struct{}{})
case ')':
s.addToken(RIGHT_PAREN, struct{}{})
case '{':
s.addToken(LEFT_BRACE, struct{}{})
case '}':
s.addToken(RIGHT_BRACE, struct{}{})
case ',':
s.addToken(COMMA, struct{}{})
case '.':
s.addToken(DOT, struct{}{})
case '-':
s.addToken(MINUS, struct{}{})
case '+':
s.addToken(PLUS, struct{}{})
case ';':
s.addToken(SEMICOLON, struct{}{})
case '*':
s.addToken(STAR, struct{}{})
case '!':
if s.match('=') {
s.addToken(BANG_EQUAL, struct{}{})
} else {
s.addToken(BANG, struct{}{})
}
case '=':
if s.match('=') {
s.addToken(EQUAL_EQUAL, struct{}{})
} else {
s.addToken(EQUAL, struct{}{})
}
case '<':
if s.match('=') {
s.addToken(LESS_EQUAL, struct{}{})
} else {
s.addToken(LESS, struct{}{})
}
case '>':
if s.match('=') {
s.addToken(GREATER_EQUAL, struct{}{})
} else {
s.addToken(GREATER, struct{}{})
}
case '/':
if s.match('/') {
for s.peek() != '\n' && !s.isAtEnd() {
s.advance()
}
} else {
s.addToken(SLASH, struct{}{})
}
case '"':
s.string()
case ' ':
case '\t':
case '\r':
break
case '\n':
s.line++
default:
if unicode.IsDigit(c) {
s.number()
break
}
if s.isAlpha(c) {
s.identifier()
break
}
s.printError(fmt.Sprintf("Unexpected character %s", string(c)))
}
}
func (s *Scanner) identifier() {
for unicode.IsDigit(s.peek()) || s.isAlpha(s.peek()) {
s.advance()
}
str := s.source[s.start:s.current]
if id, found := keywords[string(str)]; found {
s.addToken(id, struct{}{})
} else {
s.addToken(IDENTIFIER, struct{}{})
}
}
func (s *Scanner) string() {
for s.peek() != '"' && !s.isAtEnd() {
if s.peek() == '\n' {
s.line++
}
s.advance()
}
if s.isAtEnd() {
s.printError("Unterminated string")
return
}
s.advance()
str := string(s.source[s.start+1 : s.current-1])
s.addToken(STRING, str)
}
func (s *Scanner) number() {
for unicode.IsDigit(s.peek()) {
s.advance()
}
if s.peek() == '.' && unicode.IsDigit(s.peekNext()) {
s.advance()
}
for unicode.IsDigit(s.peek()) {
s.advance()
}
num, err := strconv.ParseFloat(string(s.source[s.start:s.current]), 64)
if err != nil {
s.printError(err.Error())
}
s.addToken(NUMBER, num)
}
func (s *Scanner) isAlpha(ch rune) bool {
return regexp.MustCompile(`^[A-Za-z_]+$`).MatchString(string(ch))
}
func (s *Scanner) addToken(typ TokenType, literal any) {
text := string(s.source[s.start:s.current])
s.tokens = append(s.tokens, Token{typ: typ, lexeme: text, literal: literal, line: s.line})
}
func (s *Scanner) advance() rune {
char, size := utf8.DecodeRune(s.source[s.current:])
s.current += size
return char
}
func (s *Scanner) peek() rune {
char, _ := utf8.DecodeRune(s.source[s.current:])
return char
}
func (s *Scanner) peekNext() rune {
_, size := utf8.DecodeRune(s.source[s.current+1:])
if s.current+size >= len(s.source) {
return '\000'
}
next, _ := utf8.DecodeRune(s.source[s.current+size:])
return next
}
func (s *Scanner) match(ch rune) bool {
if s.isAtEnd() {
return false
}
decoded, size := utf8.DecodeRune(s.source[s.current:])
s.current += size
return ch == decoded
}
func (s *Scanner) isAtEnd() bool {
return s.current >= len(s.source)
}
func panic(err error) { func panic(err error) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

302
scanner.go Normal file
View file

@ -0,0 +1,302 @@
package main
import (
"fmt"
"regexp"
"strconv"
"unicode"
"unicode/utf8"
)
var hadError = false
//go:generate go run golang.org/x/tools/cmd/stringer -type=TokenType
type TokenType int
const (
// one char
LEFT_PAREN TokenType = iota
RIGHT_PAREN
LEFT_BRACE
RIGHT_BRACE
COMMA
DOT
MINUS
PLUS
SEMICOLON
SLASH
STAR
// one or two chars
BANG
BANG_EQUAL
EQUAL
EQUAL_EQUAL
GREATER
GREATER_EQUAL
LESS
LESS_EQUAL
// Literals
IDENTIFIER
STRING
NUMBER
// keywords
AND
CLASS
ELSE
FALSE
FUN
FOR
IF
NIL
OR
PRINT
RETURN
SUPER
THIS
TRUE
VAR
WHILE
EOF
)
var keywords = map[string]TokenType{
"and": AND,
"class": CLASS,
"else": ELSE,
"false": FALSE,
"for": FOR,
"fun": FUN,
"if": IF,
"nil": NIL,
"or": OR,
"print": PRINT,
"return": RETURN,
"super": SUPER,
"this": THIS,
"true": TRUE,
"var": VAR,
"while": WHILE,
}
type Token struct {
typ TokenType
lexeme string
literal any
line int
}
func (t *Token) String() string {
return fmt.Sprintf("%s - %s - %v", t.typ, t.lexeme, t.literal)
}
type Scanner struct {
source []byte
tokens []Token
start int
current int
line int
}
func newScanner(source []byte) *Scanner {
return &Scanner{source: source, start: 0, current: 0, line: 1}
}
func (s *Scanner) scan() []Token {
for !s.isAtEnd() {
s.start = s.current
s.scanToken()
}
s.tokens = append(s.tokens, Token{EOF, "EOF", struct{}{}, s.line})
return s.tokens
}
func (s *Scanner) scanToken() {
c := s.advance()
switch c {
case '(':
s.addToken(LEFT_PAREN, struct{}{})
case ')':
s.addToken(RIGHT_PAREN, struct{}{})
case '{':
s.addToken(LEFT_BRACE, struct{}{})
case '}':
s.addToken(RIGHT_BRACE, struct{}{})
case ',':
s.addToken(COMMA, struct{}{})
case '.':
s.addToken(DOT, struct{}{})
case '-':
s.addToken(MINUS, struct{}{})
case '+':
s.addToken(PLUS, struct{}{})
case ';':
s.addToken(SEMICOLON, struct{}{})
case '*':
s.addToken(STAR, struct{}{})
case '!':
if s.match('=') {
s.addToken(BANG_EQUAL, struct{}{})
} else {
s.addToken(BANG, struct{}{})
}
case '=':
if s.match('=') {
s.addToken(EQUAL_EQUAL, struct{}{})
} else {
s.addToken(EQUAL, struct{}{})
}
case '<':
if s.match('=') {
s.addToken(LESS_EQUAL, struct{}{})
} else {
s.addToken(LESS, struct{}{})
}
case '>':
if s.match('=') {
s.addToken(GREATER_EQUAL, struct{}{})
} else {
s.addToken(GREATER, struct{}{})
}
case '/':
if s.match('/') {
for s.peek() != '\n' && !s.isAtEnd() {
s.advance()
}
} else {
s.addToken(SLASH, struct{}{})
}
case '"':
s.string()
case ' ':
case '\t':
case '\r':
break
case '\n':
s.line++
default:
if unicode.IsDigit(c) {
s.number()
break
}
if s.isAlpha(c) {
s.identifier()
break
}
s.printError(fmt.Sprintf("Unexpected character %s", string(c)))
}
}
func (s *Scanner) addToken(typ TokenType, literal any) {
text := string(s.source[s.start:s.current])
s.tokens = append(s.tokens, Token{typ: typ, lexeme: text, literal: literal, line: s.line})
}
func (s *Scanner) identifier() {
for unicode.IsDigit(s.peek()) || s.isAlpha(s.peek()) {
s.advance()
}
str := s.source[s.start:s.current]
if id, found := keywords[string(str)]; found {
s.addToken(id, struct{}{})
} else {
s.addToken(IDENTIFIER, struct{}{})
}
}
func (s *Scanner) string() {
for s.peek() != '"' && !s.isAtEnd() {
if s.peek() == '\n' {
s.line++
}
s.advance()
}
if s.isAtEnd() {
s.printError("Unterminated string")
return
}
s.advance()
str := string(s.source[s.start+1 : s.current-1])
s.addToken(STRING, str)
}
func (s *Scanner) number() {
for unicode.IsDigit(s.peek()) {
s.advance()
}
if s.peek() == '.' && unicode.IsDigit(s.peekNext()) {
s.advance()
}
for unicode.IsDigit(s.peek()) {
s.advance()
}
num, err := strconv.ParseFloat(string(s.source[s.start:s.current]), 64)
if err != nil {
s.printError(err.Error())
}
s.addToken(NUMBER, num)
}
func (s *Scanner) isAlpha(ch rune) bool {
return regexp.MustCompile(`^[A-Za-z_]+$`).MatchString(string(ch))
}
func (s *Scanner) advance() rune {
char, size := utf8.DecodeRune(s.source[s.current:])
s.current += size
return char
}
func (s *Scanner) peek() rune {
char, _ := utf8.DecodeRune(s.source[s.current:])
return char
}
func (s *Scanner) peekNext() rune {
_, size := utf8.DecodeRune(s.source[s.current+1:])
if s.current+size >= len(s.source) {
return '\000'
}
next, _ := utf8.DecodeRune(s.source[s.current+size:])
return next
}
func (s *Scanner) match(ch rune) bool {
if s.isAtEnd() {
return false
}
decoded, size := utf8.DecodeRune(s.source[s.current:])
s.current += size
return ch == decoded
}
func (s *Scanner) isAtEnd() bool {
return s.current >= len(s.source)
}
func (s *Scanner) printError(message string) {
fmt.Printf("[line %d] Error: %s\n", s.line, message)
hadError = true
}