Add and test abstract syntax tree data type

This commit is contained in:
Petar Kapriš 2024-11-25 22:53:29 +01:00
parent e4733b20a6
commit 475b155228
6 changed files with 220 additions and 12 deletions

56
pj1-go/ast-print.go Normal file
View file

@ -0,0 +1,56 @@
package main
import (
"fmt"
"strings"
)
func print(expr Expr) string {
switch e := expr.(type) {
case Binary:
return parenthesize(e.operator.Lexeme,
e.left,
e.right)
case Grouping:
return parenthesize("group", e.expression)
case Literal:
if e.value == nil {
return "nil"
}
return fmt.Sprintf("%v", e.value)
case Unary:
return parenthesize(e.operator.Lexeme, e.right)
}
return "ERROR: reached impossible branch in print function"
}
func parenthesize(name string, exprs ...Expr) string {
var sb strings.Builder
sb.WriteString("(" + name)
for _, expr := range exprs {
sb.WriteString(" ")
sb.WriteString(print(expr))
}
sb.WriteString(")")
return sb.String()
}
// This was briefly a testing function for the AST class at the end of ch. 5
/*
func main() {
var expression Expr = Binary{
Unary{
lexer.NewToken(lexer.MINUS, "-", nil, 1),
Literal{123},
},
lexer.NewToken(lexer.STAR, "*", nil, 1),
Grouping{
Literal{45.67},
},
}
fmt.Println(print(expression))
}
*/

36
pj1-go/ast.go Normal file
View file

@ -0,0 +1,36 @@
// Code generated by tools/gen-ast.go DO NOT EDIT.
package main
import "git.bonsai.cool/kayprish/pj1/pj1-go/lexer"
type Expr interface {
isExpr()
}
type Binary struct {
left Expr
operator lexer.Token
right Expr
}
func (x Binary) isExpr() {}
type Grouping struct {
expression Expr
}
func (x Grouping) isExpr() {}
type Literal struct {
value interface{}
}
func (x Literal) isExpr() {}
type Unary struct {
operator lexer.Token
right Expr
}
func (x Unary) isExpr() {}

114
pj1-go/gen-ast.go Normal file
View file

@ -0,0 +1,114 @@
//go:build generate
//go:generate go run ./gen-ast.go ./
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"unicode"
"unicode/utf8"
)
var modulePath string
// returns a string which contains the current module path, keep in mind this
// program is meant to be run in the root of the module using go generate
func getModulePath() string {
goModFile, err := os.Open("./go.mod")
if err != nil {
fmt.Fprintln(os.Stderr, "Could not open go.mod file, is this command being run in the module root folder?")
os.Exit(1)
}
scanner := bufio.NewScanner(goModFile)
scanner.Scan()
fstLine := scanner.Text()
if fstLine[:7] != "module " {
fmt.Fprintln(os.Stderr, "The first line of go.mod seems malformed")
os.Exit(1)
}
goModFile.Close()
path := strings.TrimSpace(fstLine[7:])
return path
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "Usage: go run gen-ast.go <output directory>")
os.Exit(64)
}
outputDir := os.Args[1]
modulePath = getModulePath()
defineAst(outputDir, "Expr",
[]string{"Binary : left Expr, operator lexer.Token, right Expr",
"Grouping : expression Expr",
"Literal : value interface{}",
// a literal can be any value that can be printed
"Unary : operator lexer.Token, right Expr"})
}
func lowerFirst(s string) string {
if s == "" {
return ""
}
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToLower(r)) + s[n:]
}
func defineAst(outputDir string, baseName string, types []string) {
fileName := "ast"
var path string = outputDir + "/" + fileName + ".go"
f, err := os.Create(path)
if err != nil {
fmt.Fprintln(os.Stderr, "Could not open file \""+path+"\"")
return
}
defer f.Close()
fmt.Fprintln(f, "// Code generated by tools/gen-ast.go DO NOT EDIT.")
fmt.Fprintln(f, "package main")
fmt.Fprintln(f)
fmt.Fprintln(f, "import \""+modulePath+"/lexer\"")
fmt.Fprintln(f)
// Creates a dummy interface just to limit types which can be
// considered an "Expr"
fmt.Fprintln(f, "type "+baseName+" interface {")
fmt.Fprintln(f, "\tis"+baseName+"()")
fmt.Fprintln(f, "}")
fmt.Fprintln(f)
// The AST types.
for _, t := range types {
tSplit := strings.Split(t, ":")
typeName := strings.TrimSpace(tSplit[0])
fields := strings.TrimSpace(tSplit[1])
defineType(f, baseName, typeName, fields)
}
}
func defineType(f io.Writer, baseName string, typeName string, fieldList string) {
fmt.Fprintln(f, "type "+typeName+" struct {")
// Fields.
var fields []string = strings.Split(fieldList, ", ")
for _, field := range fields {
fmt.Fprintln(f, "\t"+field)
}
fmt.Fprintln(f, "}")
fmt.Fprintln(f)
// Interface dummy function
fmt.Fprintln(f, "func (x "+typeName+") is"+baseName+"() {}")
fmt.Fprintln(f)
// TODO: may have to generate constructor, according to top of
// page 110, right now it seems that defining structs without
// the constructor does the job
}

View file

@ -1,3 +1,5 @@
git.bonsai.cool/kayprish/pj1/pj1-go v0.0.0-20240807135935-04e669c15630 h1:SO1oOi4BAVPmoCkBCeZEcNXiCDZJrM49Y5Fhm/bBuBU=
git.bonsai.cool/kayprish/pj1/pj1-go v0.0.0-20240807135935-04e669c15630/go.mod h1:f4dHsvhBf6lTSjuA+gqssxFOHdkOjPnS4QeufMWvHOM=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=

View file

@ -88,7 +88,7 @@ var keywords = map[string]TokenType{
type Token struct { type Token struct {
ttype TokenType ttype TokenType
lexeme string Lexeme string
literal interface{} literal interface{}
line int line int
} }
@ -99,7 +99,7 @@ func NewToken(ttype TokenType, lexeme string, literal interface{}, line int) Tok
} }
func (t Token) String() string { func (t Token) String() string {
return fmt.Sprintf("%v %v %v", t.ttype, t.lexeme, t.literal) return fmt.Sprintf("%v %v %v", t.ttype, t.Lexeme, t.literal)
} }
type Lexer struct { type Lexer struct {

View file

@ -10,16 +10,16 @@ import (
"git.bonsai.cool/kayprish/pj1/pj1-go/util" "git.bonsai.cool/kayprish/pj1/pj1-go/util"
) )
func main() { // func main() {
if len(os.Args) > 2 { // if len(os.Args) > 2 {
fmt.Println("Usage: pj1-go [script]") // fmt.Println("Usage: pj1-go [script]")
os.Exit(64) // os.Exit(64)
} else if len(os.Args) == 2 { // } else if len(os.Args) == 2 {
runFile(os.Args[0]) // runFile(os.Args[0])
} else { // } else {
runPrompt() // runPrompt()
} // }
} // }
func runFile(path string) { func runFile(path string) {
bytes, err := ioutil.ReadFile(path) bytes, err := ioutil.ReadFile(path)