Product config makefiles to Starlark converter

Test: treehugger; internal tests in mk2rbc_test.go
Bug: 172923994
Change-Id: I43120b9c181ef2b8d9453e743233811b0fec268b
diff --git a/mk2rbc/node.go b/mk2rbc/node.go
new file mode 100644
index 0000000..d4b4222
--- /dev/null
+++ b/mk2rbc/node.go
@@ -0,0 +1,237 @@
+// Copyright 2021 Google LLC
+//
+// 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 mk2rbc
+
+import (
+	"fmt"
+	"strings"
+
+	mkparser "android/soong/androidmk/parser"
+)
+
+// A parsed node for which starlark code will be generated
+// by calling emit().
+type starlarkNode interface {
+	emit(ctx *generationContext)
+}
+
+// Types used to keep processed makefile data:
+type commentNode struct {
+	text string
+}
+
+func (c *commentNode) emit(gctx *generationContext) {
+	chunks := strings.Split(c.text, "\\\n")
+	gctx.newLine()
+	gctx.write(chunks[0]) // It has '#' at the beginning already.
+	for _, chunk := range chunks[1:] {
+		gctx.newLine()
+		gctx.write("#", chunk)
+	}
+}
+
+type inheritedModule struct {
+	path            string // Converted Starlark file path
+	originalPath    string // Makefile file path
+	moduleName      string
+	moduleLocalName string
+	loadAlways      bool
+}
+
+func (im inheritedModule) name() string {
+	return MakePath2ModuleName(im.originalPath)
+}
+
+func (im inheritedModule) entryName() string {
+	return im.moduleLocalName + "_init"
+}
+
+type inheritNode struct {
+	*inheritedModule
+}
+
+func (inn *inheritNode) emit(gctx *generationContext) {
+	// Unconditional case:
+	//    rblf.inherit(handle, <module>, module_init)
+	// Conditional case:
+	//    if <module>_init != None:
+	//      same as above
+	gctx.newLine()
+	if inn.loadAlways {
+		gctx.writef("%s(handle, %q, %s)", cfnInherit, inn.name(), inn.entryName())
+		return
+	}
+	gctx.writef("if %s != None:", inn.entryName())
+	gctx.indentLevel++
+	gctx.newLine()
+	gctx.writef("%s(handle, %q, %s)", cfnInherit, inn.name(), inn.entryName())
+	gctx.indentLevel--
+}
+
+type includeNode struct {
+	*inheritedModule
+}
+
+func (inn *includeNode) emit(gctx *generationContext) {
+	gctx.newLine()
+	if inn.loadAlways {
+		gctx.writef("%s(g, handle)", inn.entryName())
+		return
+	}
+	gctx.writef("if %s != None:", inn.entryName())
+	gctx.indentLevel++
+	gctx.newLine()
+	gctx.writef("%s(g, handle)", inn.entryName())
+	gctx.indentLevel--
+}
+
+type assignmentFlavor int
+
+const (
+	// Assignment flavors
+	asgnSet         assignmentFlavor = iota // := or =
+	asgnMaybeSet    assignmentFlavor = iota // ?= and variable may be unset
+	asgnAppend      assignmentFlavor = iota // += and variable has been set before
+	asgnMaybeAppend assignmentFlavor = iota // += and variable may be unset
+)
+
+type assignmentNode struct {
+	lhs      variable
+	value    starlarkExpr
+	mkValue  *mkparser.MakeString
+	flavor   assignmentFlavor
+	isTraced bool
+	previous *assignmentNode
+}
+
+func (asgn *assignmentNode) emit(gctx *generationContext) {
+	gctx.newLine()
+	gctx.inAssignment = true
+	asgn.lhs.emitSet(gctx, asgn)
+	gctx.inAssignment = false
+
+	if asgn.isTraced {
+		gctx.newLine()
+		gctx.tracedCount++
+		gctx.writef(`print("%s.%d: %s := ", `, gctx.starScript.mkFile, gctx.tracedCount, asgn.lhs.name())
+		asgn.lhs.emitGet(gctx, true)
+		gctx.writef(")")
+	}
+}
+
+type exprNode struct {
+	expr starlarkExpr
+}
+
+func (exn *exprNode) emit(gctx *generationContext) {
+	gctx.newLine()
+	exn.expr.emit(gctx)
+}
+
+type ifNode struct {
+	isElif bool // true if this is 'elif' statement
+	expr   starlarkExpr
+}
+
+func (in *ifNode) emit(gctx *generationContext) {
+	ifElif := "if "
+	if in.isElif {
+		ifElif = "elif "
+	}
+
+	gctx.newLine()
+	if bad, ok := in.expr.(*badExpr); ok {
+		gctx.write("# MK2STAR ERROR converting:")
+		gctx.newLine()
+		gctx.writef("#   %s", bad.node.Dump())
+		gctx.newLine()
+		gctx.writef("# %s", bad.message)
+		gctx.newLine()
+		// The init function emits a warning if the conversion was not
+		// fullly successful, so here we (arbitrarily) take the false path.
+		gctx.writef("%sFalse:", ifElif)
+		return
+	}
+	gctx.write(ifElif)
+	in.expr.emit(gctx)
+	gctx.write(":")
+}
+
+type elseNode struct{}
+
+func (br *elseNode) emit(gctx *generationContext) {
+	gctx.newLine()
+	gctx.write("else:")
+}
+
+// switchCase represents as single if/elseif/else branch. All the necessary
+// info about flavor (if/elseif/else) is supposed to be kept in `gate`.
+type switchCase struct {
+	gate  starlarkNode
+	nodes []starlarkNode
+}
+
+func (cb *switchCase) newNode(node starlarkNode) {
+	cb.nodes = append(cb.nodes, node)
+}
+
+func (cb *switchCase) emit(gctx *generationContext) {
+	cb.gate.emit(gctx)
+	gctx.indentLevel++
+	hasStatements := false
+	emitNode := func(node starlarkNode) {
+		if _, ok := node.(*commentNode); !ok {
+			hasStatements = true
+		}
+		node.emit(gctx)
+	}
+	if len(cb.nodes) > 0 {
+		emitNode(cb.nodes[0])
+		for _, node := range cb.nodes[1:] {
+			emitNode(node)
+		}
+		if !hasStatements {
+			gctx.emitPass()
+		}
+	} else {
+		gctx.emitPass()
+	}
+	gctx.indentLevel--
+}
+
+// A single complete if ... elseif ... else ... endif sequences
+type switchNode struct {
+	ssCases []*switchCase
+}
+
+func (ssw *switchNode) newNode(node starlarkNode) {
+	switch br := node.(type) {
+	case *switchCase:
+		ssw.ssCases = append(ssw.ssCases, br)
+	default:
+		panic(fmt.Errorf("expected switchCase node, got %t", br))
+	}
+}
+
+func (ssw *switchNode) emit(gctx *generationContext) {
+	if len(ssw.ssCases) == 0 {
+		gctx.emitPass()
+	} else {
+		ssw.ssCases[0].emit(gctx)
+		for _, ssCase := range ssw.ssCases[1:] {
+			ssCase.emit(gctx)
+		}
+	}
+}