Files
filament/tools/codegen-options/parser.go
Philip Rideout 63cfb817d7 Use generated code for all "Options" JS bindings. (#5536)
TypeScript bindings and Java are still TBD.
2022-05-11 08:02:35 -07:00

228 lines
7.0 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)
}
return context.definitions
}
type Scope interface {
BaseName() string
QualifiedName() string
}
type StructField struct {
Type string
Name string
DefaultValue string
SkipJson bool
SkipJavaScript bool
}
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
assignmentMatcher *regexp.Regexp
floatMatcher *regexp.Regexp
vecMatcher *regexp.Regexp
}
func (context parserContext) generateQualifier() string {
qualifier := ""
for _, defn := range context.stack {
if defn.BaseName() != "" {
qualifier = qualifier + defn.BaseName() + "::"
}
}
return qualifier
}
// 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.vecMatcher.MatchString(cppvalue) {
cppvalue = context.vecMatcher.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 {
// https://github.com/google/re2/wiki/Syntax
context.cppTokenizer = regexp.MustCompile(`((?:/\*)|(?:\*/)|(?:;)|(?://)|(?:\})|(?:\{))`)
context.assignmentMatcher = regexp.MustCompile(`=(.*);`)
context.floatMatcher = regexp.MustCompile(`(\-?[0-9]+\.[0-9]*)f?`)
context.vecMatcher = regexp.MustCompile(`\{(\s*\-?[0-9\.]+\s*(,\s*\-?[0-9\.]+\s*){1,})\}`)
}
codeline = context.cppTokenizer.ReplaceAllString(codeline, " $1 ")
scanner := bufio.NewScanner(strings.NewReader(codeline))
scanner.Split(bufio.ScanWords)
inPlaceDefinition := ""
for scanner.Scan() {
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 == "}":
depth := len(context.stack) - 1
if depth < 0 {
log.Fatalf("%d: bizarre nesting", lineNumber)
}
switch defn := context.stack[depth].(type) {
case *generalScope:
// Do nothing here since this scope is neither a struct definition
// nor an enum definition. (e.g. it could be a namespace or initializer)
default:
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 len(context.stack) > 1:
depth := len(context.stack) - 1
switch defn := context.stack[depth].(type) {
case *StructDefinition:
scanner.Scan()
defaultValueTokens := context.assignmentMatcher.FindStringSubmatch(codeline)
defaultValue := ""
if len(defaultValueTokens) > 0 {
defaultValue = defaultValueTokens[1]
}
defaultValue = context.distillValue(defaultValue, lineNumber)
var field StructField
skipJson := strings.Contains(codeline, `%codegen_skip_json%`)
skipJs := strings.Contains(codeline, `%codegen_skip_javascript%`)
if inPlaceDefinition != "" {
field = StructField{inPlaceDefinition, token, defaultValue, skipJson, skipJs}
} else {
field = StructField{token, scanner.Text(), defaultValue, skipJson, skipJs}
}
defn.Fields = append(defn.Fields, field)
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)
}
}