Files
filament/tools/codegen-options/parser.go
2022-05-12 13:54:28 -07:00

321 lines
9.8 KiB
Go

/*
* Copyright (C) 2022 The Android Open Source Project
*
* 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 main
import (
"bufio"
"log"
"os"
"regexp"
"strings"
)
func Parse(sourcePath string) []Scope {
sourceFile, err := os.Open(sourcePath)
if err != nil {
log.Fatal(err)
}
defer sourceFile.Close()
context := parserContext{}
lineScanner := bufio.NewScanner(sourceFile)
for lineNumber := 1; lineScanner.Scan(); lineNumber++ {
context.scanCppCodeline(lineScanner.Text(), lineNumber)
}
if err := lineScanner.Err(); err != nil {
log.Fatal(err)
}
context.addTypeQualifiers()
return context.definitions
}
type Scope interface {
BaseName() string
QualifiedName() string
}
type StructField struct {
Type string
Name string
DefaultValue string
Description string
SkipJson bool
SkipJavaScript bool
LineNumber int
}
type StructDefinition struct {
StructName string
Qualifier string
Fields []StructField
}
type EnumDefinition struct {
EnumName string
Qualifier string
Values []string
}
type generalScope struct{}
func (defn StructDefinition) BaseName() string { return defn.StructName }
func (defn StructDefinition) QualifiedName() string { return defn.Qualifier + defn.StructName }
func (defn EnumDefinition) BaseName() string { return defn.EnumName }
func (defn EnumDefinition) QualifiedName() string { return defn.Qualifier + defn.EnumName }
func (defn generalScope) BaseName() string { return "" }
func (defn generalScope) QualifiedName() string { return "" }
type parserContext struct {
stack []Scope
definitions []Scope
insideComment bool
cppTokenizer *regexp.Regexp
floatMatcher *regexp.Regexp
vectorMatcher *regexp.Regexp
fieldParser *regexp.Regexp
}
// https://github.com/google/re2/wiki/Syntax
func (context *parserContext) compileRegexps() {
context.cppTokenizer = regexp.MustCompile(`((?:/\*)|(?:\*/)|(?:;)|(?://)|(?:\})|(?:\{))`)
context.floatMatcher = regexp.MustCompile(`(\-?[0-9]+\.[0-9]*)f?`)
context.vectorMatcher = regexp.MustCompile(`\{(\s*\-?[0-9\.]+\s*(,\s*\-?[0-9\.]+\s*){1,})\}`)
const kFieldType = `(?P<type>.*)`
const kFieldName = `(?P<name>[A-Za-z0-9_]+)`
const kFieldValue = `(?P<value>(.*?))`
const kFieldDesc = `(?://\s*\!\<\s*(?P<description>.*))?`
context.fieldParser = regexp.MustCompile(
`^\s*` + kFieldType + `\s+` + kFieldName + `\s*=\s*` + kFieldValue + `\s*;\s*` + kFieldDesc)
}
func (context parserContext) generateQualifier() string {
qualifier := ""
for _, defn := range context.stack {
if defn.BaseName() != "" {
qualifier = qualifier + defn.BaseName() + "::"
}
}
return qualifier
}
func (context parserContext) addTypeQualifiers() {
typeMap := make(map[string]Scope)
for _, defn := range context.definitions {
typeMap[defn.BaseName()] = defn
}
for _, defn := range context.definitions {
switch structDefn := defn.(type) {
case *StructDefinition:
for fieldIndex, field := range structDefn.Fields {
if typeDefn, found := typeMap[field.Type]; found {
mutable := &structDefn.Fields[fieldIndex]
// If this is an enum, add qualifier to the RHS of the assignment.
// Remember that all enums must be class enums.
if end := strings.LastIndex(field.DefaultValue, "::"); end != -1 {
enumValue := field.DefaultValue[end+2:]
mutable.DefaultValue = typeDefn.QualifiedName() + "::" + enumValue
}
mutable.Type = typeDefn.QualifiedName()
continue
}
if isSimpleType(field.Type) {
continue
}
// If this field is neither a simple type nor a type that was defined in the source
// file, then emit a warning. Unknown custom types are error prone because it is
// difficult to know if additional scoping qualifiers are required, or how they are
// bound in the target language.
log.Printf("%d: WARNING: %v is an unrecognized type", field.LineNumber, field.Type)
}
}
}
}
// Validate and transform the RHS of an assignment.
// For vectors, this converts curly braces into square brackets.
func (context parserContext) distillValue(cppvalue string, lineNumber int) string {
cppvalue = strings.TrimSpace(cppvalue)
// Remove trailing "f" from floats, which isn't allowed in JavaScript.
if context.floatMatcher.MatchString(cppvalue) {
cppvalue = context.floatMatcher.ReplaceAllString(cppvalue, "$1")
}
// There are many ways to declare vector values (multi-arg constructor, single-arg
// constructor, curly braces with type, curly braces without type), so just poop out if
// the syntax is anything other than "curly braces without type".
if strings.Contains(cppvalue, "math::") || strings.Contains(cppvalue, "Color") {
log.Fatalf("%d: vectors must have the form {x, y ...}", lineNumber)
}
// Assume it's a vector if there's a curly brace.
if strings.Contains(cppvalue, "{") {
if context.vectorMatcher.MatchString(cppvalue) {
cppvalue = context.vectorMatcher.ReplaceAllString(cppvalue, "[$1]")
} else {
log.Fatalf("%d: vectors must have the form {x, y ...}", lineNumber)
}
}
if len(cppvalue) == 0 {
log.Fatalf("%d: empty value specified", lineNumber)
}
return cppvalue
}
func (context *parserContext) scanCppCodeline(codeline string, lineNumber int) {
if context.cppTokenizer == nil {
context.compileRegexps()
}
codeline = context.cppTokenizer.ReplaceAllString(codeline, " $1 ")
scanner := bufio.NewScanner(strings.NewReader(codeline))
scanner.Split(bufio.ScanWords)
inPlaceDefinition := ""
scanStructField := func(defn *StructDefinition, firstWord string) {
var field = StructField{
SkipJson: strings.Contains(codeline, `%codegen_skip_json%`),
SkipJavaScript: strings.Contains(codeline, `%codegen_skip_javascript%`),
LineNumber: lineNumber,
}
// Normally when we're inside a struct, the first word on each codeline is the field type,
// and the second word is the field name. However if a nested struct is defined, then the
// type is potentially anonymous and the first word is the field name.
if !scanner.Scan() {
log.Fatalf("%d: bad struct field", lineNumber)
}
// Check if this field type has an in place definition. For example:
// struct OuterType {
// int foo;
// struct Baz { int bar } baz;
// };
// In the above example, inPlaceDefinition == Baz.
if inPlaceDefinition != "" {
field.Type = inPlaceDefinition
field.Name = firstWord
defn.Fields = append(defn.Fields, field)
return
}
if !context.fieldParser.MatchString(codeline) {
log.Fatalf("%d: unexpected form in struct field declaration", lineNumber)
}
// To make the regex usage somewhat readable, extract the named subgroups into a map rather
// than referring to each result by index.
subexpList := context.fieldParser.FindStringSubmatch(codeline)
subexpMap := make(map[string]string)
for i, name := range context.fieldParser.SubexpNames() {
if i != 0 && name != "" {
subexpMap[name] = subexpList[i]
}
}
field.Type = subexpMap["type"]
field.Name = subexpMap["name"]
field.Description = subexpMap["description"]
field.DefaultValue = context.distillValue(subexpMap["value"], lineNumber)
defn.Fields = append(defn.Fields, field)
}
for scanner.Scan() {
depth := len(context.stack) - 1
token := scanner.Text()
switch {
case token == "//":
return
case token == "/*":
context.insideComment = true
case token == "*/":
context.insideComment = false
case context.insideComment:
// Do nothing inside a comment.
case token == ";":
// Do nothing.
case token == "{":
context.stack = append(context.stack, &generalScope{})
case token == "}":
if depth < 0 {
log.Fatalf("%d: bizarre nesting", lineNumber)
}
switch defn := context.stack[depth].(type) {
case *StructDefinition, *EnumDefinition:
inPlaceDefinition = defn.BaseName()
context.definitions = append(context.definitions, defn)
}
context.stack = context.stack[:depth]
case token == "struct":
if !scanner.Scan() {
log.Fatalf("%d: bizarre struct", lineNumber)
}
if !strings.Contains(codeline, "{") || strings.Contains(codeline, "}") {
log.Fatalf("%d: bad formatting", lineNumber)
}
stackEntry := StructDefinition{scanner.Text(), context.generateQualifier(), nil}
context.stack = append(context.stack, &stackEntry)
return
case token == "enum":
if !scanner.Scan() || scanner.Text() != "class" || !scanner.Scan() {
log.Fatalf("%d: bad enum", lineNumber)
}
if !strings.Contains(codeline, "{") || strings.Contains(codeline, "}") {
log.Fatalf("%d: bad formatting", lineNumber)
}
stackEntry := EnumDefinition{scanner.Text(), context.generateQualifier(), nil}
context.stack = append(context.stack, &stackEntry)
return
case depth > 0:
switch defn := context.stack[depth].(type) {
case *StructDefinition:
scanStructField(defn, token)
return
case *EnumDefinition:
if strings.Contains(codeline, "=") {
log.Fatalf("%d: custom values are not allowed", lineNumber)
}
defn.Values = append(defn.Values, strings.Trim(token, ","))
return
}
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("%d: %s", lineNumber, err)
}
}
func isSimpleType(cpptype string) bool {
if strings.HasPrefix(cpptype, "math::") {
return true
}
switch cpptype {
case "bool", "int", "uint8_t", "uint16_t", "uint32_t", "uint64_t":
return true
case "float", "double":
return true
case "LinearColor", "LinearColorA":
return true
}
return false
}