blob: f7e17fae9bfdb06d9f41f977faf619f5675550e3 [file] [log] [blame]
// Copyright 2018 The CUE Authors
//
// 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 cue
import (
"path"
"strconv"
"sync"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
)
// A Runtime is used for creating CUE interpretations.
//
// Any operation that involves two Values or Instances should originate from
// the same Runtime.
//
// The zero value of a Runtime is ready to use.
type Runtime struct {
ctx *build.Context // TODO: remove
idx *index
}
func init() {
internal.GetRuntime = func(instance interface{}) interface{} {
switch x := instance.(type) {
case Value:
return &Runtime{idx: x.idx}
case *Instance:
return &Runtime{idx: x.index}
default:
panic("argument must be Value or *Instance")
}
}
internal.CheckAndForkRuntime = func(runtime, value interface{}) interface{} {
r := runtime.(*Runtime)
idx := value.(Value).ctx().index
if idx != r.idx {
panic("value not from same runtime")
}
return &Runtime{idx: newIndex(idx)}
}
}
func dummyLoad(token.Pos, string) *build.Instance { return nil }
func (r *Runtime) index() *index {
if r.idx == nil {
r.idx = newIndex(sharedIndex)
}
return r.idx
}
func (r *Runtime) buildContext() *build.Context {
ctx := r.ctx
if r.ctx == nil {
ctx = build.NewContext()
}
return ctx
}
func (r *Runtime) complete(p *build.Instance) (*Instance, error) {
idx := r.index()
if err := p.Complete(); err != nil {
return nil, err
}
inst := idx.loadInstance(p)
inst.ImportPath = p.ImportPath
if inst.Err != nil {
return nil, inst.Err
}
return inst, nil
}
// Compile compiles the given source into an Instance. The source code may be
// provided as a string, byte slice, io.Reader. The name is used as the file
// name in position information. The source may import builtin packages. Use
// Build to allow importing non-builtin packages.
func (r *Runtime) Compile(filename string, source interface{}) (*Instance, error) {
ctx := r.buildContext()
p := ctx.NewInstance(filename, dummyLoad)
if err := p.AddFile(filename, source); err != nil {
return nil, p.Err
}
return r.complete(p)
}
// CompileFile compiles the given source file into an Instance. The source may
// import builtin packages. Use Build to allow importing non-builtin packages.
func (r *Runtime) CompileFile(file *ast.File) (*Instance, error) {
ctx := r.buildContext()
p := ctx.NewInstance(file.Filename, dummyLoad)
err := p.AddSyntax(file)
if err != nil {
return nil, err
}
_, p.PkgName, _ = internal.PackageInfo(file)
return r.complete(p)
}
// CompileExpr compiles the given source expression into an Instance. The source
// may import builtin packages. Use Build to allow importing non-builtin
// packages.
func (r *Runtime) CompileExpr(expr ast.Expr) (*Instance, error) {
ctx := r.buildContext()
p := ctx.NewInstance("", dummyLoad)
switch x := expr.(type) {
case *ast.StructLit:
_ = p.AddSyntax(&ast.File{Decls: x.Elts})
default:
_ = p.AddSyntax(&ast.File{
Decls: []ast.Decl{&ast.EmbedDecl{Expr: expr}},
})
}
if p.Err != nil {
return nil, p.Err
}
return r.complete(p)
}
// Parse parses a CUE source value into a CUE Instance. The source code may
// be provided as a string, byte slice, or io.Reader. The name is used as the
// file name in position information. The source may import builtin packages.
//
// Deprecated: use Compile
func (r *Runtime) Parse(name string, source interface{}) (*Instance, error) {
return r.Compile(name, source)
}
// Build creates an Instance from the given build.Instance. A returned Instance
// may be incomplete, in which case its Err field is set.
func (r *Runtime) Build(instance *build.Instance) (*Instance, error) {
return r.complete(instance)
}
// Build creates one Instance for each build.Instance. A returned Instance
// may be incomplete, in which case its Err field is set.
//
// Example:
// inst := cue.Build(load.Instances(args))
//
func Build(instances []*build.Instance) []*Instance {
if len(instances) == 0 {
panic("cue: list of instances must not be empty")
}
var r Runtime
a, _ := r.build(instances)
return a
}
func (r *Runtime) build(instances []*build.Instance) ([]*Instance, error) {
index := r.index()
loaded := []*Instance{}
var errs errors.Error
for _, p := range instances {
_ = p.Complete()
errs = errors.Append(errs, p.Err)
i := index.loadInstance(p)
errs = errors.Append(errs, i.Err)
loaded = append(loaded, i)
}
// TODO: insert imports
return loaded, errs
}
// FromExpr creates an instance from an expression.
// Any references must be resolved beforehand.
//
// Deprecated: use CompileExpr
func (r *Runtime) FromExpr(expr ast.Expr) (*Instance, error) {
i := r.index().newInstance(nil)
err := i.insertFile(&ast.File{
Decls: []ast.Decl{&ast.EmbedDecl{Expr: expr}},
})
if err != nil {
return nil, err
}
return i, nil
}
// index maps conversions from label names to internal codes.
//
// All instances belonging to the same package should share this index.
type index struct {
labelMap map[string]label
labels []string
loaded map[*build.Instance]*Instance
imports map[value]*Instance // key is always a *structLit
importsByPath map[string]*Instance
offset label
parent *index
mutex sync.Mutex
typeCache sync.Map // map[reflect.Type]evaluated
}
// work around golang-ci linter bug: fields are used.
func init() {
var i index
i.mutex.Lock()
i.mutex.Unlock()
i.typeCache.Load(1)
}
const sharedOffset = 0x40000000
// sharedIndex is used for indexing builtins and any other labels common to
// all instances.
var sharedIndex = newSharedIndex()
func newSharedIndex() *index {
// TODO: nasty hack to indicate FileSet of shared index. Remove the whole
// FileSet idea from the API. Just take the hit of the extra pointers for
// positions in the ast, and then optimize the storage in an abstract
// machine implementation for storing graphs.
token.NewFile("dummy", sharedOffset, 0)
i := &index{
labelMap: map[string]label{"": 0},
labels: []string{""},
imports: map[value]*Instance{},
importsByPath: map[string]*Instance{},
}
return i
}
// newIndex creates a new index.
func newIndex(parent *index) *index {
i := &index{
labelMap: map[string]label{},
loaded: map[*build.Instance]*Instance{},
imports: map[value]*Instance{},
importsByPath: map[string]*Instance{},
offset: label(len(parent.labels)) + parent.offset,
parent: parent,
}
return i
}
func (idx *index) strLabel(str string) label {
return idx.label(str, false)
}
func (idx *index) nodeLabel(n ast.Node) (f label, ok bool) {
switch x := n.(type) {
case *ast.BasicLit:
name, _, err := ast.LabelName(x)
return idx.label(name, false), err == nil
case *ast.Ident:
name, err := ast.ParseIdent(x)
return idx.label(name, true), err == nil
}
return 0, false
}
func (idx *index) findLabel(s string) (f label, ok bool) {
for x := idx; x != nil; x = x.parent {
f, ok = x.labelMap[s]
if ok {
break
}
}
return f, ok
}
func (idx *index) label(s string, isIdent bool) label {
f, ok := idx.findLabel(s)
if !ok {
f = label(len(idx.labelMap)) + idx.offset
idx.labelMap[s] = f
idx.labels = append(idx.labels, s)
}
f <<= 1
if isIdent && s != "" && s[0] == '_' {
f |= 1
}
return f
}
func (idx *index) labelStr(l label) string {
l >>= 1
for ; l < idx.offset; idx = idx.parent {
}
return idx.labels[l-idx.offset]
}
func (idx *index) loadInstance(p *build.Instance) *Instance {
if inst := idx.loaded[p]; inst != nil {
if !inst.complete {
// cycles should be detected by the builder and it should not be
// possible to construct a build.Instance that has them.
panic("cue: cycle")
}
return inst
}
files := p.Files
inst := idx.newInstance(p)
if inst.Err == nil {
// inst.instance.index.state = s
// inst.instance.inst = p
inst.Err = resolveFiles(idx, p)
for _, f := range files {
err := inst.insertFile(f)
inst.Err = errors.Append(inst.Err, err)
}
}
inst.ImportPath = p.ImportPath
inst.complete = true
return inst
}
func lineStr(idx *index, n ast.Node) string {
return n.Pos().String()
}
func resolveFiles(idx *index, p *build.Instance) errors.Error {
// Link top-level declarations. As top-level entries get unified, an entry
// may be linked to any top-level entry of any of the files.
allFields := map[string]ast.Node{}
for _, file := range p.Files {
for _, d := range file.Decls {
if f, ok := d.(*ast.Field); ok && f.Value != nil {
if ident, ok := f.Label.(*ast.Ident); ok {
allFields[ident.Name] = f.Value
}
}
}
}
for _, f := range p.Files {
if err := resolveFile(idx, f, p, allFields); err != nil {
return err
}
}
return nil
}
func resolveFile(idx *index, f *ast.File, p *build.Instance, allFields map[string]ast.Node) errors.Error {
unresolved := map[string][]*ast.Ident{}
for _, u := range f.Unresolved {
unresolved[u.Name] = append(unresolved[u.Name], u)
}
fields := map[string]ast.Node{}
for _, d := range f.Decls {
if f, ok := d.(*ast.Field); ok && f.Value != nil {
if ident, ok := f.Label.(*ast.Ident); ok {
fields[ident.Name] = d
}
}
}
var errs errors.Error
specs := []*ast.ImportSpec{}
for _, spec := range f.Imports {
id, err := strconv.Unquote(spec.Path.Value)
if err != nil {
continue // quietly ignore the error
}
name := path.Base(id)
if imp := p.LookupImport(id); imp != nil {
name = imp.PkgName
} else if _, ok := builtins[id]; !ok {
errs = errors.Append(errs,
nodeErrorf(spec, "package %q not found", id))
continue
}
if spec.Name != nil {
name = spec.Name.Name
}
if n, ok := fields[name]; ok {
errs = errors.Append(errs, nodeErrorf(spec,
"%s redeclared as imported package name\n"+
"\tprevious declaration at %v", name, lineStr(idx, n)))
continue
}
fields[name] = spec
used := false
for _, u := range unresolved[name] {
used = true
u.Node = spec
}
if !used {
specs = append(specs, spec)
}
}
// Verify each import is used.
if len(specs) > 0 {
// Find references to imports. This assumes that identifiers in labels
// are not resolved or that such errors are caught elsewhere.
ast.Walk(f, nil, func(n ast.Node) {
if x, ok := n.(*ast.Ident); ok {
// As we also visit labels, most nodes will be nil.
if x.Node == nil {
return
}
for i, s := range specs {
if s == x.Node {
specs[i] = nil
return
}
}
}
})
// Add errors for unused imports.
for _, spec := range specs {
if spec == nil {
continue
}
if spec.Name == nil {
errs = errors.Append(errs, nodeErrorf(spec,
"imported and not used: %s", spec.Path.Value))
} else {
errs = errors.Append(errs, nodeErrorf(spec,
"imported and not used: %s as %s", spec.Path.Value, spec.Name))
}
}
}
k := 0
for _, u := range f.Unresolved {
if u.Node != nil {
continue
}
if n, ok := allFields[u.Name]; ok {
u.Node = n
u.Scope = f
continue
}
f.Unresolved[k] = u
k++
}
f.Unresolved = f.Unresolved[:k]
// TODO: also need to resolve types.
// if len(f.Unresolved) > 0 {
// n := f.Unresolved[0]
// return ctx.mkErr(newBase(n), "unresolved reference %s", n.Name)
// }
return errs
}