Skip to content

Commit

Permalink
cuecontext: add options to set version and debug flags
Browse files Browse the repository at this point in the history
This changes Runtime.SetSettings back to SetVersion and
adds AddDebugOptions. SetSettings has now served its
purpose of identifying all call sites where these options
need to be set. :)

Note that we enable the API before it is fully working.
But it probably works well enough for many applications.

For open options search for the following across the repo:
 - '-- diff(/.*)?todo/p? --' in txtar files
 - todo_* fields in table-driven tests
 - TODO_(V3|Sharing|NoSharing) function calls in table-driven tests
 - TODO(evalv3) around matrix tests (like in trim_test.go)

Removed use of "stderr 'str'" in txtar, as it does not
support CUE_UPDATE=1.

Closes #3060

Signed-off-by: Marcel van Lohuizen <mpvl@gmail.com>
Change-Id: Ib3970d867363711f461c8c303abfdb0402706fff
  • Loading branch information
mpvl committed May 7, 2024
1 parent 0bf573f commit 8f028c7
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 24 deletions.
3 changes: 2 additions & 1 deletion cmd/cue/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ func mkRunE(c *Command, f runFunction) func(*cobra.Command, []string) error {
// in a non-tooling context.
if cueexperiment.Flags.EvalV3 {
const dev = internal.DevVersion
(*cueruntime.Runtime)(c.ctx).SetSettings(internal.EvaluatorVersion(dev), cuedebug.Flags)
(*cueruntime.Runtime)(c.ctx).SetVersion(internal.EvaluatorVersion(dev))
(*cueruntime.Runtime)(c.ctx).SetDebugOptions(&cuedebug.Flags)
}

err := f(c, args)
Expand Down
4 changes: 3 additions & 1 deletion cmd/cue/cmd/testdata/script/experiment_unknown.txtar
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
env CUE_EXPERIMENT=xxx
! exec cue eval something
stderr 'unknown CUE_EXPERIMENT xxx'
cmp stderr errout
-- errout --
cannot parse CUE_EXPERIMENT: unknown xxx
59 changes: 59 additions & 0 deletions cue/cuecontext/cuecontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
package cuecontext

import (
"fmt"

"cuelang.org/go/cue"
"cuelang.org/go/internal"
"cuelang.org/go/internal/core/runtime"
"cuelang.org/go/internal/cuedebug"
"cuelang.org/go/internal/envflag"

_ "cuelang.org/go/pkg"
)
Expand All @@ -26,9 +31,20 @@ type Option struct {
apply func(r *runtime.Runtime)
}

// defaultFlags defines the debug flags that are set by default.
var defaultFlags cuedebug.Config

func init() {
if err := envflag.Parse(&defaultFlags, ""); err != nil {
panic(err)
}
}

// New creates a new Context.
func New(options ...Option) *cue.Context {
r := runtime.New()
// Ensure default behavior if the flags are not set explicitly.
r.SetDebugOptions(&defaultFlags)
for _, o := range options {
o.apply(r)
}
Expand All @@ -46,3 +62,46 @@ func Interpreter(i ExternInterpreter) Option {
r.SetInterpreter(i)
}}
}

type EvalVersion = internal.EvaluatorVersion

const (
// EvalDefault is the latest stable version of the evaluator.
EvalDefault EvalVersion = EvalV2

// EvalExperiment refers to the latest unstable version of the evaluator.
// Note that this version may change without notice.
EvalExperiment EvalVersion = EvalV3

// EvalV2 is the currently latest stable version of the evaluator.
// It was introduced in CUE version 0.3 and is being maintained until 2024.
EvalV2 EvalVersion = internal.DefaultVersion

// EvalV3 is the currently experimental version of the evaluator.
// It was introduced in 2024 and brought a new disjunction algorithm,
// a new closedness algorithm, a new core scheduler, and adds performance
// enhancements like structure sharing.
EvalV3 EvalVersion = internal.DevVersion
)

// EvaluatorVersion indicates which version of the evaluator to use. Currently
// only experimental versions can be selected as an alternative.
func EvaluatorVersion(v EvalVersion) Option {
return Option{func(r *runtime.Runtime) {
r.SetVersion(v)
}}
}

// CUE_DEBUG takes a string with the same contents as CUE_DEBUG and configures
// the context with the relevant debug options. It panics for unknown or
// malformed options.
func CUE_DEBUG(s string) Option {
var c cuedebug.Config
if err := envflag.Parse(&c, s); err != nil {
panic(fmt.Errorf("cuecontext.CUE_DEBUG: %v", err))
}

return Option{func(r *runtime.Runtime) {
r.SetDebugOptions(&c)
}}
}
3 changes: 2 additions & 1 deletion cue/matrix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ func (c *evalConfig) runtime() *Runtime {
}

func (c *evalConfig) updateRuntime(r *runtime.Runtime) {
r.SetSettings(c.version, c.flags)
r.SetVersion(c.version)
r.SetDebugOptions(&c.flags)
}

func runMatrix(t *testing.T, name string, f func(t *testing.T, c *evalConfig)) {
Expand Down
11 changes: 8 additions & 3 deletions internal/core/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,16 @@ func NewWithSettings(v internal.EvaluatorVersion, flags cuedebug.Config) *Runtim
return r
}

// SetSettings sets the version to use for the Runtime. This should only be set
// SetVersion sets the version to use for the Runtime. This should only be set
// before first use.
func (r *Runtime) SetSettings(v internal.EvaluatorVersion, flags cuedebug.Config) {
func (r *Runtime) SetVersion(v internal.EvaluatorVersion) {
r.version = v
r.flags = flags
}

// SetDebugOptions sets the debug flags to use for the Runtime. This should only
// be set before first use.
func (r *Runtime) SetDebugOptions(flags *cuedebug.Config) {
r.flags = *flags
}

// IsInitialized reports whether the runtime has been initialized.
Expand Down
3 changes: 2 additions & 1 deletion internal/cuetdtest/matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ func (t *M) Runtime() *runtime.Runtime {
}

func (t *M) UpdateRuntime(r *runtime.Runtime) {
r.SetSettings(t.version, t.flags)
r.SetVersion(t.version)
r.SetDebugOptions(&t.flags)
}

const DefaultVersion = "v2"
Expand Down
53 changes: 39 additions & 14 deletions internal/envflag/flag.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
package envflag

import (
"errors"
"fmt"
"os"
"reflect"
"strconv"
"strings"
)

// Init initializes the fields in flags from the attached struct field tags
// as well as the contents of the given environment variable.
// Init uses Parse with the contents of the given environment variable as input.
func Init[T any](flags *T, envVar string) error {
err := Parse(flags, os.Getenv(envVar))
if err != nil {
return fmt.Errorf("cannot parse %s: %w", envVar, err)
}
return nil
}

// Parse initializes the fields in flags from the attached struct field tags as
// well as the contents of the given string.
//
// The struct field tag may contain a default value other than the zero value,
// such as `envflag:"default:true"` to set a boolean field to true by default.
//
// The environment variable may contain a comma-separated list of name=value
// pairs values representing the boolean fields in the struct type T.
// If the value is omitted entirely, the value is assumed to be name=true.
// The string may contain a comma-separated list of name=value pairs values
// representing the boolean fields in the struct type T. If the value is omitted
// entirely, the value is assumed to be name=true.
//
// Names are treated case insensitively.
// Value strings are parsed as Go booleans via [strconv.ParseBool],
// meaning that they accept "true" and "false" but also the shorter "1" and "0".
func Init[T any](flags *T, envVar string) error {
// Names are treated case insensitively. Value strings are parsed as Go booleans
// via [strconv.ParseBool], meaning that they accept "true" and "false" but also
// the shorter "1" and "0".
func Parse[T any](flags *T, env string) error {
// Collect the field indices and set the default values.
indexByName := make(map[string]int)
fv := reflect.ValueOf(flags).Elem()
Expand All @@ -31,6 +41,7 @@ func Init[T any](flags *T, envVar string) error {
defaultValue := false
if tagStr, ok := field.Tag.Lookup("envflag"); ok {
defaultStr, ok := strings.CutPrefix(tagStr, "default:")
// TODO: consider panicking for these error types.
if !ok {
return fmt.Errorf("expected tag like `envflag:\"default:true\"`: %s", tagStr)
}
Expand All @@ -44,27 +55,41 @@ func Init[T any](flags *T, envVar string) error {
indexByName[strings.ToLower(field.Name)] = i
}

// Parse the env value to set the fields.
env := os.Getenv(envVar)
if env == "" {
return nil
}
var errs []error
for _, elem := range strings.Split(env, ",") {
name, valueStr, ok := strings.Cut(elem, "=")
// "somename" is short for "somename=true" or "somename=1".
value := true
if ok {
v, err := strconv.ParseBool(valueStr)
if err != nil {
return fmt.Errorf("invalid bool value for %s: %v", name, err)
// Invalid format, return an error immediately.
return invalidError{
fmt.Errorf("invalid bool value for %s: %v", name, err),
}
}
value = v
}
index, ok := indexByName[name]
if !ok {
return fmt.Errorf("unknown %s %s", envVar, elem)
// Unknown option, proceed processing options as long as the format
// is valid.
errs = append(errs, fmt.Errorf("unknown %s", elem))
continue
}
fv.Field(index).SetBool(value)
}
return nil
return errors.Join(errs...)
}

// An InvalidError indicates a malformed input string.
var InvalidError = errors.New("invalid value")

type invalidError struct{ error }

func (invalidError) Is(err error) bool {
return err == InvalidError
}
31 changes: 28 additions & 3 deletions internal/envflag/flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,21 @@ func success[T comparable](want T) func(t *testing.T) {
}
}

func failure[T comparable](wantError string) func(t *testing.T) {
func failure[T comparable](want T, wantError string) func(t *testing.T) {
return func(t *testing.T) {
var x T
err := Init(&x, "TEST_VAR")
qt.Assert(t, qt.ErrorMatches(err, wantError))
qt.Assert(t, qt.Equals(x, want))
}
}

func invalid[T comparable](want T) func(t *testing.T) {
return func(t *testing.T) {
var x T
err := Init(&x, "TEST_VAR")
qt.Assert(t, qt.ErrorIs(err, InvalidError))
qt.Assert(t, qt.Equals(x, want))
}
}

Expand All @@ -44,7 +54,8 @@ var tests = []struct {
}, {
testName: "Unknown",
envVal: "ratchet",
test: failure[testFlags]("unknown TEST_VAR ratchet"),
test: failure[testFlags](testFlags{DefaultTrue: true},
"cannot parse TEST_VAR: unknown ratchet"),
}, {
testName: "Set",
envVal: "foo",
Expand All @@ -62,7 +73,10 @@ var tests = []struct {
}, {
testName: "SetWithUnknown",
envVal: "foo,other",
test: failure[testFlags]("unknown TEST_VAR other"),
test: failure[testFlags](testFlags{
Foo: true,
DefaultTrue: true,
}, "cannot parse TEST_VAR: unknown other"),
}, {
testName: "TwoFlags",
envVal: "barbaz,foo",
Expand All @@ -83,6 +97,17 @@ var tests = []struct {
test: success(testFlags{
DefaultFalse: true,
}),
}, {
testName: "MultipleUnknown",
envVal: "other1,other2,foo",
test: failure(testFlags{
Foo: true,
DefaultTrue: true,
}, "cannot parse TEST_VAR: unknown other1\nunknown other2"),
}, {
testName: "Invalid",
envVal: "foo=2,BarBaz=true",
test: invalid(testFlags{DefaultTrue: true}),
}}

func TestInit(t *testing.T) {
Expand Down

0 comments on commit 8f028c7

Please sign in to comment.