diff options
| author | Dan Willemsen <dwillemsen@google.com> | 2020-06-26 19:20:26 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-06-26 19:20:26 -0700 |
| commit | 10cc982b563c19890872b73e6d8fb44aeda646ae (patch) | |
| tree | 6b5075e832cbdf2a7996a25a26659363527b6e4c /golang/kati/parser.go | |
| parent | 003cf51e9b6da48063c90cf4c6710fde103c9c4a (diff) | |
| parent | 979e7ae6e417ae4ee45e835104b66191ae16a14c (diff) | |
| download | platform_build_kati-10cc982b563c19890872b73e6d8fb44aeda646ae.tar.gz platform_build_kati-10cc982b563c19890872b73e6d8fb44aeda646ae.tar.bz2 platform_build_kati-10cc982b563c19890872b73e6d8fb44aeda646ae.zip | |
Merge pull request #199 from danw/refactor
Refactor source tree into directories
Diffstat (limited to 'golang/kati/parser.go')
| -rw-r--r-- | golang/kati/parser.go | 821 |
1 files changed, 821 insertions, 0 deletions
diff --git a/golang/kati/parser.go b/golang/kati/parser.go new file mode 100644 index 0000000..aef20e2 --- /dev/null +++ b/golang/kati/parser.go @@ -0,0 +1,821 @@ +// Copyright 2015 Google Inc. All rights reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kati + +//go:generate go run testcase/gen_testcase_parse_benchmark.go +// +// $ go generate +// $ go test -bench . + +import ( + "bufio" + "bytes" + "crypto/sha1" + "errors" + "fmt" + "io" + "io/ioutil" + "strings" + "sync" + "time" + + "github.com/golang/glog" +) + +type makefile struct { + filename string + stmts []ast +} + +type ifState struct { + ast *ifAST + inElse bool + numNest int +} + +type parser struct { + rd *bufio.Reader + mk makefile + lineno int + elineno int // lineno == elineno unless there is trailing '\'. + linenoFixed bool + done bool + outStmts *[]ast + inRecipe bool + ifStack []ifState + + defineVar []byte + inDef []byte + + defOpt string + numIfNest int + err error +} + +func newParser(rd io.Reader, filename string) *parser { + p := &parser{ + rd: bufio.NewReader(rd), + } + p.mk.filename = filename + p.outStmts = &p.mk.stmts + return p +} + +func (p *parser) srcpos() srcpos { + return srcpos{ + filename: p.mk.filename, + lineno: p.lineno, + } +} + +func (p *parser) addStatement(stmt ast) { + *p.outStmts = append(*p.outStmts, stmt) + switch stmt.(type) { + case *maybeRuleAST: + p.inRecipe = true + case *assignAST, *includeAST, *exportAST: + p.inRecipe = false + } +} + +func (p *parser) readLine() []byte { + if !p.linenoFixed { + p.lineno = p.elineno + 1 + } + var line []byte + for !p.done { + buf, err := p.rd.ReadBytes('\n') + if !p.linenoFixed { + p.elineno++ + } + if err == io.EOF { + p.done = true + } else if err != nil { + p.err = fmt.Errorf("readline %s: %v", p.srcpos(), err) + p.done = true + } + line = append(line, buf...) + buf = bytes.TrimRight(buf, "\r\n") + glog.V(4).Infof("buf:%q", buf) + backslash := false + for len(buf) > 0 && buf[len(buf)-1] == '\\' { + buf = buf[:len(buf)-1] + backslash = !backslash + } + if !backslash { + glog.V(4).Infof("no concat line:%q", buf) + break + } + } + line = bytes.TrimRight(line, "\r\n") + return line +} + +func newAssignAST(p *parser, lhsBytes []byte, rhsBytes []byte, op string) (*assignAST, error) { + lhs, _, err := parseExpr(lhsBytes, nil, parseOp{alloc: true}) + if err != nil { + return nil, err + } + rhs, _, err := parseExpr(rhsBytes, nil, parseOp{alloc: true}) + if err != nil { + return nil, err + } + opt := "" + if p != nil { + opt = p.defOpt + } + return &assignAST{ + lhs: lhs, + rhs: rhs, + op: op, + opt: opt, + }, nil +} + +func (p *parser) handleDirective(line []byte, directives map[string]directiveFunc) bool { + w, data := firstWord(line) + if d, ok := directives[string(w)]; ok { + d(p, data) + return true + } + return false +} + +func (p *parser) handleRuleOrAssign(line []byte) { + rline := line + var semi []byte + if i := findLiteralChar(line, ';', 0, skipVar); i >= 0 { + // preserve after semicolon + semi = append(semi, line[i+1:]...) + rline = concatline(line[:i]) + } else { + rline = concatline(line) + } + if p.handleAssign(line) { + return + } + // not assignment. + // ie. no '=' found or ':' found before '=' (except ':=') + p.parseMaybeRule(rline, semi) + return +} + +func (p *parser) handleAssign(line []byte) bool { + aline, _ := removeComment(concatline(line)) + aline = trimLeftSpaceBytes(aline) + if len(aline) == 0 { + return false + } + // fmt.Printf("assign: %q=>%q\n", line, aline) + i := findLiteralChar(aline, ':', '=', skipVar) + if i >= 0 { + if aline[i] == '=' { + p.parseAssign(aline, i) + return true + } + if aline[i] == ':' && i+1 < len(aline) && aline[i+1] == '=' { + p.parseAssign(aline, i+1) + return true + } + } + return false +} + +func (p *parser) parseAssign(line []byte, sep int) { + lhs, op, rhs := line[:sep], line[sep:sep+1], line[sep+1:] + if sep > 0 { + switch line[sep-1] { + case ':', '+', '?': + lhs, op = line[:sep-1], line[sep-1:sep+1] + } + } + glog.V(1).Infof("parseAssign %s op:%q opt:%s", line, op, p.defOpt) + lhs = trimSpaceBytes(lhs) + rhs = trimLeftSpaceBytes(rhs) + aast, err := newAssignAST(p, lhs, rhs, string(op)) + if err != nil { + p.err = err + return + } + aast.srcpos = p.srcpos() + p.addStatement(aast) +} + +func (p *parser) parseMaybeRule(line, semi []byte) { + if len(line) == 0 { + p.err = p.srcpos().errorf("*** missing rule before commands.") + return + } + if line[0] == '\t' { + p.err = p.srcpos().errorf("*** commands commence before first target.") + return + } + var assign *assignAST + ci := findLiteralChar(line, ':', 0, skipVar) + if ci >= 0 { + eqi := findLiteralChar(line[ci+1:], '=', 0, skipVar) + if eqi == 0 { + panic(fmt.Sprintf("unexpected eq after colon: %q", line)) + } + if eqi > 0 { + var lhsbytes []byte + op := "=" + switch line[ci+1+eqi-1] { + case ':', '+', '?': + lhsbytes = append(lhsbytes, line[ci+1:ci+1+eqi-1]...) + op = string(line[ci+1+eqi-1 : ci+1+eqi+1]) + default: + lhsbytes = append(lhsbytes, line[ci+1:ci+1+eqi]...) + } + + lhsbytes = trimSpaceBytes(lhsbytes) + lhs, _, err := parseExpr(lhsbytes, nil, parseOp{}) + if err != nil { + p.err = p.srcpos().error(err) + return + } + var rhsbytes []byte + rhsbytes = append(rhsbytes, line[ci+1+eqi+1:]...) + if semi != nil { + rhsbytes = append(rhsbytes, ';') + rhsbytes = append(rhsbytes, concatline(semi)...) + } + rhsbytes = trimLeftSpaceBytes(rhsbytes) + semi = nil + rhs, _, err := parseExpr(rhsbytes, nil, parseOp{}) + if err != nil { + p.err = p.srcpos().error(err) + return + } + + // TODO(ukai): support override, export in target specific var. + assign = &assignAST{ + lhs: lhs, + rhs: rhs, + op: op, + } + assign.srcpos = p.srcpos() + line = line[:ci+1] + } + } + expr, _, err := parseExpr(line, nil, parseOp{}) + if err != nil { + p.err = p.srcpos().error(err) + return + } + // TODO(ukai): remove ast, and eval here. + rast := &maybeRuleAST{ + isRule: ci >= 0, + expr: expr, + assign: assign, + semi: semi, + } + rast.srcpos = p.srcpos() + glog.V(1).Infof("stmt: %#v", rast) + p.addStatement(rast) +} + +func (p *parser) parseInclude(op string, line []byte) { + // TODO(ukai): parse expr here + iast := &includeAST{ + expr: string(line), + op: op, + } + iast.srcpos = p.srcpos() + p.addStatement(iast) +} + +func (p *parser) parseIfdef(op string, data []byte) { + lhs, _, err := parseExpr(data, nil, parseOp{alloc: true}) + if err != nil { + p.err = p.srcpos().error(err) + return + } + iast := &ifAST{ + op: op, + lhs: lhs, + } + iast.srcpos = p.srcpos() + p.addStatement(iast) + p.ifStack = append(p.ifStack, ifState{ast: iast, numNest: p.numIfNest}) + p.outStmts = &iast.trueStmts +} + +func (p *parser) parseTwoQuotes(s []byte) (string, string, []byte, bool) { + var args []string + for i := 0; i < 2; i++ { + s = trimSpaceBytes(s) + if len(s) == 0 { + return "", "", nil, false + } + quote := s[0] + if quote != '\'' && quote != '"' { + return "", "", nil, false + } + end := bytes.IndexByte(s[1:], quote) + 1 + if end < 0 { + return "", "", nil, false + } + args = append(args, string(s[1:end])) + s = s[end+1:] + } + return args[0], args[1], s, true +} + +// parse +// "(lhs, rhs)" +// "lhs, rhs" +func (p *parser) parseEq(s []byte) (string, string, []byte, bool) { + if len(s) == 0 { + return "", "", nil, false + } + if s[0] == '(' { + in := s[1:] + glog.V(1).Infof("parseEq ( %q )", in) + term := []byte{','} + v, n, err := parseExpr(in, term, parseOp{matchParen: true}) + if err != nil { + glog.V(1).Infof("parse eq: %q: %v", in, err) + return "", "", nil, false + } + lhs := v.String() + n++ + n += skipSpaces(in[n:], nil) + term = []byte{')'} + in = in[n:] + v, n, err = parseExpr(in, term, parseOp{matchParen: true}) + if err != nil { + glog.V(1).Infof("parse eq 2nd: %q: %v", in, err) + return "", "", nil, false + } + rhs := v.String() + in = in[n+1:] + in = trimSpaceBytes(in) + return lhs, rhs, in, true + } + return p.parseTwoQuotes(s) +} + +func (p *parser) parseIfeq(op string, data []byte) { + lhsBytes, rhsBytes, extra, ok := p.parseEq(data) + if !ok { + p.err = p.srcpos().errorf(`*** invalid syntax in conditional.`) + return + } + if len(extra) > 0 { + glog.V(1).Infof("extra %q", extra) + warnNoPrefix(p.srcpos(), `extraneous text after %q directive`, op) + } + + lhs, _, err := parseExpr([]byte(lhsBytes), nil, parseOp{matchParen: true}) + if err != nil { + p.err = p.srcpos().error(err) + return + } + rhs, _, err := parseExpr([]byte(rhsBytes), nil, parseOp{matchParen: true}) + if err != nil { + p.err = p.srcpos().error(err) + return + } + + iast := &ifAST{ + op: op, + lhs: lhs, + rhs: rhs, + } + iast.srcpos = p.srcpos() + p.addStatement(iast) + p.ifStack = append(p.ifStack, ifState{ast: iast, numNest: p.numIfNest}) + p.outStmts = &iast.trueStmts +} + +func (p *parser) checkIfStack(curKeyword string) error { + if len(p.ifStack) == 0 { + return p.srcpos().errorf(`*** extraneous %q.`, curKeyword) + } + return nil +} + +func (p *parser) parseElse(data []byte) { + err := p.checkIfStack("else") + if err != nil { + p.err = err + return + } + state := &p.ifStack[len(p.ifStack)-1] + if state.inElse { + p.err = p.srcpos().errorf(`*** only one "else" per conditional.`) + return + } + state.inElse = true + p.outStmts = &state.ast.falseStmts + + nextIf := data + if len(nextIf) == 0 { + return + } + var ifDirectives = map[string]directiveFunc{ + "ifdef": ifdefDirective, + "ifndef": ifndefDirective, + "ifeq": ifeqDirective, + "ifneq": ifneqDirective, + } + p.numIfNest = state.numNest + 1 + if p.handleDirective(nextIf, ifDirectives) { + p.numIfNest = 0 + return + } + p.numIfNest = 0 + warnNoPrefix(p.srcpos(), "extraneous text after `else' directive") + return +} + +func (p *parser) parseEndif(data []byte) { + err := p.checkIfStack("endif") + if err != nil { + p.err = err + return + } + state := p.ifStack[len(p.ifStack)-1] + for t := 0; t <= state.numNest; t++ { + p.ifStack = p.ifStack[0 : len(p.ifStack)-1] + if len(p.ifStack) == 0 { + p.outStmts = &p.mk.stmts + } else { + state := p.ifStack[len(p.ifStack)-1] + if state.inElse { + p.outStmts = &state.ast.falseStmts + } else { + p.outStmts = &state.ast.trueStmts + } + } + } + if len(trimSpaceBytes(data)) > 0 { + warnNoPrefix(p.srcpos(), "extraneous text after `endif' directive") + } + return +} + +func (p *parser) parseDefine(data []byte) { + p.defineVar = nil + p.inDef = nil + p.defineVar = append(p.defineVar, trimSpaceBytes(data)...) + return +} + +func (p *parser) parseVpath(data []byte) { + vline, _ := removeComment(concatline(data)) + vline = trimLeftSpaceBytes(vline) + v, _, err := parseExpr(vline, nil, parseOp{}) + if err != nil { + p.err = p.srcpos().errorf("parse error %q: %v", string(vline), err) + return + } + vast := &vpathAST{ + expr: v, + } + vast.srcpos = p.srcpos() + p.addStatement(vast) +} + +type directiveFunc func(*parser, []byte) + +var makeDirectives map[string]directiveFunc + +func init() { + makeDirectives = map[string]directiveFunc{ + "include": includeDirective, + "-include": sincludeDirective, + "sinclude": sincludeDirective, + "ifdef": ifdefDirective, + "ifndef": ifndefDirective, + "ifeq": ifeqDirective, + "ifneq": ifneqDirective, + "else": elseDirective, + "endif": endifDirective, + "define": defineDirective, + "override": overrideDirective, + "export": exportDirective, + "unexport": unexportDirective, + "vpath": vpathDirective, + } +} + +func includeDirective(p *parser, data []byte) { + p.parseInclude("include", data) +} + +func sincludeDirective(p *parser, data []byte) { + p.parseInclude("-include", data) +} + +func ifdefDirective(p *parser, data []byte) { + p.parseIfdef("ifdef", data) +} + +func ifndefDirective(p *parser, data []byte) { + p.parseIfdef("ifndef", data) +} + +func ifeqDirective(p *parser, data []byte) { + p.parseIfeq("ifeq", data) +} + +func ifneqDirective(p *parser, data []byte) { + p.parseIfeq("ifneq", data) +} + +func elseDirective(p *parser, data []byte) { + p.parseElse(data) +} + +func endifDirective(p *parser, data []byte) { + p.parseEndif(data) +} + +func defineDirective(p *parser, data []byte) { + p.parseDefine(data) +} + +func overrideDirective(p *parser, data []byte) { + p.defOpt = "override" + defineDirective := map[string]directiveFunc{ + "define": defineDirective, + } + glog.V(1).Infof("override define? %q", data) + if p.handleDirective(data, defineDirective) { + return + } + // e.g. overrider foo := bar + // line will be "foo := bar". + if p.handleAssign(data) { + return + } + p.defOpt = "" + var line []byte + line = append(line, []byte("override ")...) + line = append(line, data...) + p.handleRuleOrAssign(line) + // TODO(ukai): evaluate now to detect invalid "override" directive here? +} + +func handleExport(p *parser, data []byte, export bool) (hasEqual bool) { + i := bytes.IndexByte(data, '=') + if i > 0 { + hasEqual = true + switch data[i-1] { + case ':', '+', '?': + i-- + } + data = data[:i] + } + east := &exportAST{ + expr: data, + hasEqual: hasEqual, + export: export, + } + east.srcpos = p.srcpos() + glog.V(1).Infof("export %v", east) + p.addStatement(east) + return hasEqual +} + +func exportDirective(p *parser, data []byte) { + p.defOpt = "export" + defineDirective := map[string]directiveFunc{ + "define": defineDirective, + } + glog.V(1).Infof("export define? %q", data) + if p.handleDirective(data, defineDirective) { + return + } + + if !handleExport(p, data, true) { + return + } + + // e.g. export foo := bar + // line will be "foo := bar". + p.handleAssign(data) +} + +func unexportDirective(p *parser, data []byte) { + handleExport(p, data, false) + return +} + +func vpathDirective(p *parser, data []byte) { + p.parseVpath(data) +} + +func (p *parser) parse() (mk makefile, err error) { + for !p.done { + line := p.readLine() + if glog.V(1) { + glog.Infof("%s: %q", p.srcpos(), line) + } + if p.defineVar != nil { + p.processDefine(line) + if p.err != nil { + return makefile{}, p.err + } + continue + } + p.defOpt = "" + if p.inRecipe { + if len(line) > 0 && line[0] == '\t' { + cast := &commandAST{cmd: string(line[1:])} + cast.srcpos = p.srcpos() + p.addStatement(cast) + continue + } + } + p.parseLine(line) + if p.err != nil { + return makefile{}, p.err + } + } + return p.mk, p.err +} + +func (p *parser) parseLine(line []byte) { + cline := concatline(line) + if len(cline) == 0 { + return + } + if glog.V(1) { + glog.Infof("concatline:%q", cline) + } + var dline []byte + cline, _ = removeComment(cline) + dline = append(dline, cline...) + dline = trimSpaceBytes(dline) + if len(dline) == 0 { + return + } + if glog.V(1) { + glog.Infof("directive?: %q", dline) + } + if p.handleDirective(dline, makeDirectives) { + return + } + if glog.V(1) { + glog.Infof("rule or assign?: %q", line) + } + p.handleRuleOrAssign(line) +} + +func (p *parser) processDefine(line []byte) { + line = append(line, '\n') + line = concatline(line) + if line[len(line)-1] != '\n' { + line = append(line, '\n') + } + if glog.V(1) { + glog.Infof("concatline:%q", line) + } + if !p.isEndef(line) { + p.inDef = append(p.inDef, line...) + if p.inDef == nil { + p.inDef = []byte{} + } + return + } + if p.inDef[len(p.inDef)-1] == '\n' { + p.inDef = p.inDef[:len(p.inDef)-1] + } + glog.V(1).Infof("multilineAssign %q %q", p.defineVar, p.inDef) + aast, err := newAssignAST(p, p.defineVar, p.inDef, "=") + if err != nil { + p.err = p.srcpos().errorf("assign error %q=%q: %v", p.defineVar, p.inDef, err) + return + } + aast.srcpos = p.srcpos() + aast.srcpos.lineno -= bytes.Count(p.inDef, []byte{'\n'}) + p.addStatement(aast) + p.defineVar = nil + p.inDef = nil + return +} + +func (p *parser) isEndef(line []byte) bool { + if bytes.Equal(line, []byte("endef")) { + return true + } + w, data := firstWord(line) + if bytes.Equal(w, []byte("endef")) { + data, _ = removeComment(data) + data = trimLeftSpaceBytes(data) + if len(data) > 0 { + warnNoPrefix(p.srcpos(), `extraneous text after "endef" directive`) + } + return true + } + return false +} + +func defaultMakefile() (string, error) { + candidates := []string{"GNUmakefile", "makefile", "Makefile"} + for _, filename := range candidates { + if exists(filename) { + return filename, nil + } + } + return "", errors.New("no targets specified and no makefile found") +} + +func parseMakefileReader(rd io.Reader, loc srcpos) (makefile, error) { + parser := newParser(rd, loc.filename) + parser.lineno = loc.lineno + parser.elineno = loc.lineno + parser.linenoFixed = true + return parser.parse() +} + +func parseMakefileString(s string, loc srcpos) (makefile, error) { + return parseMakefileReader(strings.NewReader(s), loc) +} + +func parseMakefileBytes(s []byte, loc srcpos) (makefile, error) { + return parseMakefileReader(bytes.NewReader(s), loc) +} + +type mkCacheEntry struct { + mk makefile + hash [sha1.Size]byte + err error + ts int64 +} + +type makefileCacheT struct { + mu sync.Mutex + mk map[string]mkCacheEntry +} + +var makefileCache = &makefileCacheT{ + mk: make(map[string]mkCacheEntry), +} + +func (mc *makefileCacheT) lookup(filename string) (makefile, [sha1.Size]byte, bool, error) { + var hash [sha1.Size]byte + mc.mu.Lock() + c, present := mc.mk[filename] + mc.mu.Unlock() + if !present { + return makefile{}, hash, false, nil + } + ts := getTimestamp(filename) + if ts < 0 || ts >= c.ts { + return makefile{}, hash, false, nil + } + return c.mk, c.hash, true, c.err +} + +func (mc *makefileCacheT) parse(filename string) (makefile, [sha1.Size]byte, error) { + glog.Infof("parse Makefile %q", filename) + mk, hash, ok, err := makefileCache.lookup(filename) + if ok { + if glog.V(1) { + glog.Infof("makefile cache hit for %q", filename) + } + return mk, hash, err + } + if glog.V(1) { + glog.Infof("reading makefile %q", filename) + } + c, err := ioutil.ReadFile(filename) + if err != nil { + return makefile{}, hash, err + } + hash = sha1.Sum(c) + mk, err = parseMakefile(c, filename) + if err != nil { + return makefile{}, hash, err + } + makefileCache.mu.Lock() + makefileCache.mk[filename] = mkCacheEntry{ + mk: mk, + hash: hash, + err: err, + ts: time.Now().Unix(), + } + makefileCache.mu.Unlock() + return mk, hash, err +} + +func parseMakefile(s []byte, filename string) (makefile, error) { + parser := newParser(bytes.NewReader(s), filename) + return parser.parse() +} |
