blob: bb8660056a8f664c7cba7033afd84867faf02ecb [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package javascript implements Javascript code generation from compiled VDL packages.
package javascript
// Generates the javascript source code for vdl files. The generated output in javascript
// differs from most other languages, since we don't generate stubs. Instead generate an
// object that contains the parsed VDL structures that will be used by the Javascript code
// to valid servers.
import (
"bytes"
"encoding/binary"
"fmt"
"path/filepath"
"strconv"
"strings"
"text/template"
"v.io/v23/vdl"
"v.io/x/ref/lib/vdl/compile"
"v.io/x/ref/lib/vdl/vdlutil"
)
type data struct {
Pkg *compile.Package
Env *compile.Env
GenerateImport func(string) string
UserImports jsImports
PathToCoreJS string
TypeNames typeNames
}
// Generate takes a populated compile.Package and produces a byte slice
// containing the generated Javascript code.
func Generate(pkg *compile.Package, env *compile.Env, genImport func(string) string, pathToCoreJS string) []byte {
data := data{
Pkg: pkg,
Env: env,
GenerateImport: genImport,
UserImports: jsImportsForFiles(pkg.Files...),
TypeNames: newTypeNames(pkg),
PathToCoreJS: pathToCoreJS,
}
var buf bytes.Buffer
if err := javascriptTemplate.Execute(&buf, data); err != nil {
// We shouldn't see an error; it means our template is buggy.
panic(fmt.Errorf("vdl: couldn't execute template: %v", err))
}
return buf.Bytes()
}
var javascriptTemplate *template.Template
func bitlen(kind vdl.Kind) int {
switch kind {
case vdl.Float32, vdl.Complex64:
return 32
case vdl.Float64, vdl.Complex128:
return 64
}
panic(fmt.Errorf("vdl: bitLen unhandled kind %v", kind))
}
func genMethodTags(names typeNames, method *compile.Method) string {
tags := method.Tags
result := "["
for _, tag := range tags {
result += typedConst(names, tag) + ", "
}
result += "]"
return result
}
// Format the given int64 into a JS BigInt.
func formatUint64BigInt(v uint64) string {
buffer := make([]byte, 8)
binary.BigEndian.PutUint64(buffer, v)
sign := "0"
if v > 0 {
sign = "1"
}
return fmt.Sprintf("new vdl.BigInt(%s, %s)", sign, formatByteBuffer(buffer))
}
// Format the given int64 into a JS BigInt.
func formatInt64BigInt(v int64) string {
buffer := make([]byte, 8)
var sign int64 = 0
if v > 0 {
sign = 1
} else if v < 0 {
sign = -1
}
binary.BigEndian.PutUint64(buffer, uint64(v*sign)) // Adjust value by sign.
return fmt.Sprintf("new vdl.BigInt(%d, %s)", sign, formatByteBuffer(buffer))
}
// Given a buffer of bytes, create the JS Uint8Array that corresponds to it (to be used with BigInt).
func formatByteBuffer(buffer []byte) string {
buf := bytes.TrimLeft(buffer, "\x00") // trim leading zeros
str := "new Uint8Array(["
for i, b := range buf {
if i > 0 {
str += ", "
}
str += fmt.Sprintf("%#x", b)
}
str += "])"
return str
}
// untypedConst generates a javascript string representing a constant that is
// not wrapped with type information.
func untypedConst(names typeNames, v *vdl.Value) string {
switch v.Kind() {
case vdl.Bool:
if v.Bool() {
return "true"
} else {
return "false"
}
case vdl.Byte:
return strconv.FormatUint(uint64(v.Byte()), 10)
case vdl.Uint16, vdl.Uint32:
return strconv.FormatUint(v.Uint(), 10)
case vdl.Int16, vdl.Int32:
return strconv.FormatInt(v.Int(), 10)
case vdl.Uint64:
return formatUint64BigInt(v.Uint())
case vdl.Int64:
return formatInt64BigInt(v.Int())
case vdl.Float32, vdl.Float64:
return strconv.FormatFloat(v.Float(), 'g', -1, bitlen(v.Kind()))
case vdl.String:
return strconv.Quote(v.RawString())
case vdl.Any:
if v.Elem() != nil {
return typedConst(names, v.Elem())
}
return "null"
case vdl.Optional:
if v.Elem() != nil {
return untypedConst(names, v.Elem())
}
return "null"
case vdl.Complex64, vdl.Complex128:
return fmt.Sprintf("new vdl.Complex(%f, %f)", real(v.Complex()), imag(v.Complex()))
case vdl.Enum:
return fmt.Sprintf("'%s'", v.EnumLabel())
case vdl.Array, vdl.List:
result := "["
for ix := 0; ix < v.Len(); ix++ {
val := untypedConst(names, v.Index(ix))
result += "\n" + val + ","
}
result += "\n]"
if v.Type().Elem().Kind() == vdl.Byte {
return "new Uint8Array(" + result + ")"
}
return result
case vdl.Set:
result := "new Set(["
for _, key := range vdl.SortValuesAsString(v.Keys()) {
result += "\n " + untypedConst(names, key) + ", "
}
result += "])"
return result
case vdl.Map:
result := "new Map(["
for i, key := range vdl.SortValuesAsString(v.Keys()) {
if i > 0 {
result += ","
}
result += fmt.Sprintf("\n [%s, %s]",
untypedConst(names, key),
untypedConst(names, v.MapIndex(key)))
}
result += "])"
return result
case vdl.Struct:
result := "{"
t := v.Type()
for ix := 0; ix < t.NumField(); ix++ {
result += "\n '" +
vdlutil.FirstRuneToLower(t.Field(ix).Name) +
"': " +
untypedConst(names, v.StructField(ix)) +
","
}
return result + "\n}"
case vdl.Union:
ix, innerVal := v.UnionField()
return fmt.Sprintf("{ %q: %v }", vdlutil.FirstRuneToLower(v.Type().Field(ix).Name), untypedConst(names, innerVal))
case vdl.TypeObject:
return names.LookupType(v.TypeObject())
default:
panic(fmt.Errorf("vdl: untypedConst unhandled type %v %v", v.Kind(), v.Type()))
}
}
func primitiveWithOptionalName(primitive, name string) string {
if name == "" {
return "types." + primitive
}
return "new vdl.Type({kind: vdl.kind." + primitive + ", name: '" + name + "'})"
}
// typedConst returns a javascript string representing a const that is always
// wrapped with type information
func typedConst(names typeNames, v *vdl.Value) string {
switch v.Kind() {
case vdl.Any, vdl.TypeObject:
return untypedConst(names, v)
default:
// We call canonicalize.reduce so that we convert to native types
// The constructor would have done the reduction of the field values
// but it doesn't convert to native types.
return fmt.Sprintf("canonicalize.reduce(new %s(%s, true), %s)",
names.LookupConstructor(v.Type()),
untypedConst(names, v),
names.LookupType(v.Type()))
}
}
// Returns a Not Implemented stub for the method
func generateMethodStub(method *compile.Method) string {
args := "ctx, serverCall"
for _, arg := range method.InArgs {
args += fmt.Sprintf(", %s", arg.Name)
}
return fmt.Sprintf(`function(%s) {
throw new Error('Method %s not implemented');
}`, args, method.Name)
}
// Returns the JS version of the method signature.
func generateMethodSignature(method *compile.Method, names typeNames) string {
return fmt.Sprintf(`{
name: '%s',
doc: %s,
inArgs: %s,
outArgs: %s,
inStream: %s,
outStream: %s,
tags: %s
}`,
method.Name,
quoteStripDoc(method.Doc),
generateMethodArguments(method.InArgs, names),
generateMethodArguments(method.OutArgs, names),
generateMethodStreaming(method.InStream, names),
generateMethodStreaming(method.OutStream, names),
genMethodTags(names, method))
}
// Returns a slice describing the method's arguments.
func generateMethodArguments(args []*compile.Field, names typeNames) string {
ret := "["
for _, arg := range args {
ret += fmt.Sprintf(
`{
name: '%s',
doc: %s,
type: %s
},
`, arg.Name, quoteStripDoc(arg.Doc), names.LookupType(arg.Type))
}
ret += "]"
return ret
}
// Returns the VOM type of the stream.
func generateMethodStreaming(streaming *vdl.Type, names typeNames) string {
if streaming == nil {
return "null"
}
return fmt.Sprintf(
`{
name: '',
doc: '',
type: %s
}`,
names.LookupType(streaming))
}
// Returns a slice of embeddings with the proper qualified identifiers.
func generateEmbeds(embeds []*compile.Interface) string {
result := "["
for _, embed := range embeds {
result += fmt.Sprintf(`{
name: '%s',
pkgPath: '%s',
doc: %s
},
`, embed.Name, embed.File.Package.Path, quoteStripDoc(embed.Doc))
}
result += "]"
return result
}
func importPath(data data, path string) string {
// We need to prefix all of these paths with a ./ to tell node that the path is relative to
// the current directory. Sadly filepath.Join(".", foo) == foo, so we have to do it
// explicitly.
return "." + string(filepath.Separator) + data.GenerateImport(path)
}
func quoteStripDoc(doc string) string {
// TODO(alexfandrianto): We need to handle '// ' and '\n' in the docstring.
// It would also be nice to single-quote the whole string.
trimmed := strings.Trim(doc, "\n")
return strconv.Quote(trimmed)
}
func hasErrors(pkg *compile.Package) bool {
for _, file := range pkg.Files {
if len(file.ErrorDefs) > 0 {
return true
}
}
return false
}
func hasConsts(pkg *compile.Package) bool {
for _, file := range pkg.Files {
if len(file.ConstDefs) > 0 {
return true
}
}
return false
}
func hasEnums(pkg *compile.Package) bool {
for _, file := range pkg.Files {
for _, def := range file.TypeDefs {
if def.Type.Kind() == vdl.Enum {
return true
}
}
}
return false
}
func hasMethodTags(pkg *compile.Package) bool {
for _, file := range pkg.Files {
for _, iface := range file.Interfaces {
for _, method := range iface.Methods {
if len(method.Tags) > 0 {
return true
}
}
}
}
return false
}
func generateSystemImports(data data) string {
res := "var vdl = require('"
packagePrefix := ""
if data.PathToCoreJS != "" {
packagePrefix = strings.Repeat("../", strings.Count(data.Pkg.Path, "/")+1) + data.PathToCoreJS
}
if data.PathToCoreJS != "" {
res += packagePrefix + "/vdl');"
} else {
res += "vanadium').vdl;"
}
res += "\n"
if hasErrors(data.Pkg) {
if data.PathToCoreJS != "" {
res += "var makeError = require('" + packagePrefix + "/verror/make-errors');\n"
res += "var actions = require('" + packagePrefix + "/verror/actions');\n"
} else {
res += "var makeError = require('vanadium').verror.makeError;\n"
res += "var actions = require('vanadium').verror.actions;\n"
}
}
if hasConsts(data.Pkg) || hasEnums(data.Pkg) || hasMethodTags(data.Pkg) {
if data.PathToCoreJS != "" {
res += "var canonicalize = require('" + packagePrefix + "/vdl/canonicalize');\n"
} else {
res += "var canonicalize = require('vanadium').vdl.canonicalize;\n"
}
}
return res
}
func init() {
funcMap := template.FuncMap{
"firstRuneToLower": vdlutil.FirstRuneToLower,
"genMethodTags": genMethodTags,
"makeTypeDefinitionsString": makeTypeDefinitionsString,
"typedConst": typedConst,
"generateEmbeds": generateEmbeds,
"generateMethodStub": generateMethodStub,
"generateMethodSignature": generateMethodSignature,
"importPath": importPath,
"quoteStripDoc": quoteStripDoc,
"generateErrorConstructor": generateErrorConstructor,
"generateSystemImports": generateSystemImports,
}
javascriptTemplate = template.Must(template.New("genJS").Funcs(funcMap).Parse(genJS))
}
// The template that we execute against a compile.Package instance to generate our
// code. Most of this is fairly straightforward substitution and ranges; more
// complicated logic is delegated to the helper functions above.
//
// We try to generate code that has somewhat reasonable formatting.
const genJS = `{{with $data := .}}{{$data.Pkg.FileDoc}}
// This file was auto-generated by the vanadium vdl tool.
{{generateSystemImports $data}}
{{/* Define additional imported modules. */}}
{{$pkg := $data.Pkg}}
{{if $data.UserImports}}{{range $imp := $data.UserImports}}
var {{$imp.Local}} = require('{{importPath $data $imp.Path}}');{{end}}{{end}}
module.exports = {};
{{/* Define any types introduced by the VDL file. */}}
// Types:
{{makeTypeDefinitionsString $data.TypeNames }}
{{/* Define all constants as typed constants. */}}
// Consts:
{{range $file := $pkg.Files}}{{range $const := $file.ConstDefs}}
module.exports.{{$const.Name}} = {{typedConst $data.TypeNames $const.Value}};
{{end}}{{end}}
{{/* Define all errors. */}}
// Errors:
{{range $file := $pkg.Files}}{{range $error := $file.ErrorDefs}}
{{generateErrorConstructor $data.TypeNames $error}}
{{end}}{{end}}
{{/* Define each of those service interfaces here, including method stubs and
service signature. */}}
// Services:
{{range $file := $pkg.Files}}
{{range $iface := $file.Interfaces}}
{{/* Define the service interface. */}}
function {{$iface.Name}}(){}
module.exports.{{$iface.Name}} = {{$iface.Name}};
{{range $method := $iface.AllMethods}}
{{/* Add each method to the service prototype. */}}
{{$iface.Name}}.prototype.{{firstRuneToLower $method.Name}} = {{generateMethodStub $method}};
{{end}} {{/* end range $iface.AllMethods */}}
{{/* The service signature encodes the same info as signature.Interface.
TODO(alexfandrianto): We want to associate the signature type here, but
it's complicated.
For now, we need to pass the type in manually into encode. */}}
{{$iface.Name}}.prototype._serviceDescription = {
name: '{{$iface.Name}}',
pkgPath: '{{$pkg.Path}}',
doc: {{quoteStripDoc $iface.Doc}},
embeds: {{generateEmbeds $iface.Embeds}},
methods: [
{{range $method := $iface.AllMethods}}
{{/* Each method signature contains the information in rpc.MethodSig. */}}
{{generateMethodSignature $method $data.TypeNames}},
{{end}} {{/*end range $iface.AllMethods*/}}
]
};
{{end}} {{/* end range $files.Interfaces */}}
{{end}} {{/* end range $pkg.Files */}}
{{end}}`