From 475b15522813a979b256fa8081082175d345c7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petar=20Kapri=C5=A1?= Date: Mon, 25 Nov 2024 22:53:29 +0100 Subject: [PATCH] Add and test abstract syntax tree data type --- pj1-go/ast-print.go | 56 +++++++++++++++++++++ pj1-go/ast.go | 36 +++++++++++++ pj1-go/gen-ast.go | 114 ++++++++++++++++++++++++++++++++++++++++++ pj1-go/go.sum | 2 + pj1-go/lexer/lexer.go | 4 +- pj1-go/pj1.go | 20 ++++---- 6 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 pj1-go/ast-print.go create mode 100644 pj1-go/ast.go create mode 100644 pj1-go/gen-ast.go diff --git a/pj1-go/ast-print.go b/pj1-go/ast-print.go new file mode 100644 index 0000000..1ab2bed --- /dev/null +++ b/pj1-go/ast-print.go @@ -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)) +} +*/ diff --git a/pj1-go/ast.go b/pj1-go/ast.go new file mode 100644 index 0000000..bedfeb3 --- /dev/null +++ b/pj1-go/ast.go @@ -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() {} + diff --git a/pj1-go/gen-ast.go b/pj1-go/gen-ast.go new file mode 100644 index 0000000..0c27136 --- /dev/null +++ b/pj1-go/gen-ast.go @@ -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 ") + 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 +} diff --git a/pj1-go/go.sum b/pj1-go/go.sum index 2069d68..121af36 100644 --- a/pj1-go/go.sum +++ b/pj1-go/go.sum @@ -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/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= diff --git a/pj1-go/lexer/lexer.go b/pj1-go/lexer/lexer.go index 26ffabe..da32f2a 100644 --- a/pj1-go/lexer/lexer.go +++ b/pj1-go/lexer/lexer.go @@ -88,7 +88,7 @@ var keywords = map[string]TokenType{ type Token struct { ttype TokenType - lexeme string + Lexeme string literal interface{} line int } @@ -99,7 +99,7 @@ func NewToken(ttype TokenType, lexeme string, literal interface{}, line int) Tok } 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 { diff --git a/pj1-go/pj1.go b/pj1-go/pj1.go index 45e320a..2012544 100644 --- a/pj1-go/pj1.go +++ b/pj1-go/pj1.go @@ -10,16 +10,16 @@ import ( "git.bonsai.cool/kayprish/pj1/pj1-go/util" ) -func main() { - if len(os.Args) > 2 { - fmt.Println("Usage: pj1-go [script]") - os.Exit(64) - } else if len(os.Args) == 2 { - runFile(os.Args[0]) - } else { - runPrompt() - } -} +// func main() { +// if len(os.Args) > 2 { +// fmt.Println("Usage: pj1-go [script]") +// os.Exit(64) +// } else if len(os.Args) == 2 { +// runFile(os.Args[0]) +// } else { +// runPrompt() +// } +// } func runFile(path string) { bytes, err := ioutil.ReadFile(path)