blob: 207352673d81b446cc6c5423cee6b8bde5035c38 [file] [log] [blame]
// Copyright 2023 The Shac 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 engine
import (
"context"
"errors"
"fmt"
"strings"
"go.chromium.org/luci/starlark/builtins"
"go.starlark.net/lib/json"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)
// getPredeclared returns the predeclared starlark symbols in the runtime.
//
// The upstream starlark interpreter includes all the symbols described at
// https://github.com/google/starlark-go/blob/HEAD/doc/spec.md#built-in-constants-and-functions
// See https://pkg.go.dev/go.starlark.net/starlark#Universe for the default list.
func getPredeclared() starlark.StringDict {
return starlark.StringDict{
"shac": toValue("shac", getShac()),
// Add https://bazel.build/rules/lib/json so it feels more natural to bazel
// users.
"json": json.Module,
// Override fail to include additional functionality.
//
// Do not use newBuiltinNone() because it needs access to the thread to
// capture the stack trace.
"fail": starlark.NewBuiltin("fail", fail),
// struct is an helper function that enables users to create seamless
// object instances.
"struct": builtins.Struct,
}
}
// fail aborts execution. When run within a check, associates the check with an "abnormal failure".
//
// Unlike builtins.Fail(), it doesn't allow user specified stack traces.
func fail(th *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
sep := " "
// Do not exit early if the arguments are wrong.
err := starlark.UnpackArgs(fn.Name(), nil, kwargs, "sep?", &sep)
buf := strings.Builder{}
for i, v := range args {
if i > 0 {
buf.WriteString(sep)
}
if s, ok := starlark.AsString(v); ok {
buf.WriteString(s)
} else {
buf.WriteString(v.String())
}
}
if err != nil {
buf.WriteString("\n")
buf.WriteString(err.Error())
}
msg := buf.String()
failErr := &failure{
Message: msg,
Stack: th.CallStack(),
}
ctx := getContext(th)
if c := ctxCheck(ctx); c != nil {
// Running inside a check, annotate it.
c.failErr = failErr
} else {
// Save the error in the shacState object since we are in the first phase.
s := ctxShacState(ctx)
s.failErr = failErr
}
return nil, errors.New(fn.Name() + ": " + msg)
}
// toValue converts a StringDict to a Value.
func toValue(name string, d starlark.StringDict) starlark.Value {
return starlarkstruct.FromStringDict(starlark.String(name), d)
}
type builtin func(ctx context.Context, s *shacState, name string, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error)
type boundBuiltin func(ctx context.Context, s *shacState, name string, self starlark.Value, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error)
// newBuiltin registers a go function as a Starlark builtin.
//
// It's identical to `starlark.NewBuiltin()`, but prepends the function name to
// the text of any returned errors as a usability improvement.
func newBuiltin(name string, impl builtin) *starlark.Builtin {
return starlark.NewBuiltin(name, func(th *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return builtinWrapper(th, name, func(ctx context.Context, s *shacState) (starlark.Value, error) {
return impl(ctx, s, name, args, kwargs)
})
})
}
// newBoundBuiltin registers a go function as a Starlark builtin that's bound to
// an object.
//
// The method receiver is passed to `impl` as an argument.
func newBoundBuiltin(name string, impl boundBuiltin) *starlark.Builtin {
return starlark.NewBuiltin(name, func(th *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return builtinWrapper(th, name, func(ctx context.Context, s *shacState) (starlark.Value, error) {
self := fn.Receiver()
return impl(ctx, s, name, self, args, kwargs)
})
})
}
func builtinWrapper(th *starlark.Thread, name string, f func(ctx context.Context, s *shacState) (starlark.Value, error)) (starlark.Value, error) {
ctx := getContext(th)
s := ctxShacState(ctx)
val, err := f(ctx, s)
if err != nil {
// starlark.UnpackArgs already adds the function name prefix to errors
// it returns, so make sure not to duplicate the prefix if it's already
// there.
if !strings.HasPrefix(err.Error(), name+": ") {
err = fmt.Errorf("%s: %w", name, err)
}
return nil, err
}
// All values returned by builtins are immutable. This is not a hard
// requirement, and can be relaxed if there's a use case for mutable
// return values, but it's still a sensible default.
val.Freeze()
return val, nil
}
func newBuiltinNone(name string, f func(ctx context.Context, s *shacState, name string, args starlark.Tuple, kwargs []starlark.Tuple) error) *starlark.Builtin {
return newBuiltin(
name,
func(ctx context.Context, s *shacState, name string, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return starlark.None, f(ctx, s, name, args, kwargs)
})
}