Repository: pulumi/esc
Pull request #432 (2025-01-15 03:48:27)

provider rotation
Introduces a new provider type and verb to support rotating static credentials.

Rotator providers have an additional `state` input which is used as a write-back target. The result of invoking Rotate is persisted back into the environment to this key.  This provides a stable location to write to without accidentally clobbering interpolations that might be used for other inputs.

The new `fn::rotate` function type is used to invoke this new type of provider. It behaves similarly to existing providers during Open, but during rotation the evaluator will invoke the Rotate methods and collect their outputs, which are returned to the caller. The caller is expected to persist these updates back into the environment as a new revision.

```
# long form
fn::rotate:
  rotator: provider
  inputs:
    # any config the provider needs to use during rotation
  state:
    # persisted state

# short form
fn::rotate::provider:
  inputs:
    # any config the provider needs to use during rotation
  state:
    # persisted state
```
  

Closes https://github.com/pulumi/pulumi-service/issues/24986

dschaller: > I would not drop the inputs. It might be that the provider still needs creds to fetch secrets (e.g. if the secrets aren't embedded as literals in the state, and are instead references).

That's a good point - it is possible state isn't stored here so we might need credentials to open the rotated credentials.
pgavlin: I don't love the short form intermixing `state` and inputs. I would prefer that the short form just lifts `provider` from a property to part of the function name and retains both `inputs` and `state` as properties. This allows users to write things like:

```yaml
  commonInputs:
    ...
  foo:
    fn::rotate::provider:
      inputs: ${commonInputs}
      state: ...
```

I think this is kind of a fundamental issue, FWIW. Even if we store state out-of-band, unless we reference it by path (which kills the ability to refactor via cut-and-paste), we'd need something in the provider inputs that serves as an identifier for the state.


nyobe: > I don't love the short form intermixing `state` and inputs. I would prefer that the short form just lifts `provider` from a property to part of the function name and retains both `inputs` and `state` as properties. 

Yeah, hoisting the state out of inputs for the short form is a bit cheeky. I've removed it: https://github.com/pulumi/esc/pull/432/commits/53dc66cb807b43ffb7bc797dff6944e6959251bd

Patch:
diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md
index ed1d4f8..e9e5b0a 100644
--- a/CHANGELOG_PENDING.md
+++ b/CHANGELOG_PENDING.md
@@ -2,8 +2,8 @@
 
 - Fix diagnostic messages when updating environment with invalid definition
   [#422](https://github.com/pulumi/esc/pull/422)
+- Introduce support for rotating static credentials via `fn::rotate` providers [432](https://github.com/pulumi/esc/pull/432)
 
 ### Bug Fixes
 
 ### Breaking changes
-
diff --git a/ast/expr.go b/ast/expr.go
index 71b7c32..62a8dee 100644
--- a/ast/expr.go
+++ b/ast/expr.go
@@ -434,6 +434,41 @@ func Open(provider string, inputs *ObjectExpr) *OpenExpr {
 	}
 }
 
+// RotateExpr is a type of OpenExpr that supports a rotate operation.
+type RotateExpr struct {
+	builtinNode
+
+	Provider *StringExpr
+	Inputs   Expr
+	State    *ObjectExpr
+}
+
+func RotateSyntax(node *syntax.ObjectNode, name *StringExpr, args Expr, provider *StringExpr, inputs Expr, state *ObjectExpr) *RotateExpr {
+	return &RotateExpr{
+		builtinNode: builtin(node, name, args),
+		Provider:    provider,
+		Inputs:      inputs,
+		State:       state,
+	}
+}
+
+func Rotate(provider string, inputs, state *ObjectExpr) *RotateExpr {
+	name, providerX := String("fn::rotate"), String(provider)
+
+	entries := []ObjectProperty{
+		{Key: String("provider"), Value: providerX},
+		{Key: String("inputs"), Value: inputs},
+		{Key: String("state"), Value: state},
+	}
+
+	return &RotateExpr{
+		builtinNode: builtin(nil, name, Object(entries...)),
+		Provider:    providerX,
+		Inputs:      inputs,
+		State:       state,
+	}
+}
+
 // ToJSON returns the underlying structure as a json string.
 type ToJSONExpr struct {
 	builtinNode
@@ -607,6 +642,8 @@ func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool)
 		parse = parseJoin
 	case "fn::open":
 		parse = parseOpen
+	case "fn::rotate":
+		parse = parseRotate
 	case "fn::secret":
 		parse = parseSecret
 	case "fn::toBase64":
@@ -620,6 +657,10 @@ func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool)
 			parse = parseShortOpen
 			break
 		}
+		if strings.HasPrefix(kvp.Key.Value(), "fn::rotate::") {
+			parse = parseShortRotate
+			break
+		}
 
 		if strings.HasPrefix(strings.ToLower(kvp.Key.Value()), "fn::") {
 			diags = append(diags, syntax.Error(kvp.Key.Syntax().Range(),
@@ -696,6 +737,86 @@ func parseShortOpen(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr,
 	return OpenSyntax(node, name, args, provider, args), nil
 }
 
+func parseRotate(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
+	obj, ok := args.(*ObjectExpr)
+	if !ok {
+		diags := syntax.Diagnostics{ExprError(args, "the argument to fn::rotate must be an object containing 'provider', 'inputs' and 'state'")}
+		return RotateSyntax(node, name, args, nil, nil, nil), diags
+	}
+
+	var providerExpr, inputs, stateExpr Expr
+	var diags syntax.Diagnostics
+
+	for i := 0; i < len(obj.Entries); i++ {
+		kvp := obj.Entries[i]
+		key := kvp.Key
+		switch key.GetValue() {
+		case "provider":
+			providerExpr = kvp.Value
+		case "inputs":
+			inputs = kvp.Value
+		case "state":
+			stateExpr = kvp.Value
+		}
+	}
+
+	provider, ok := providerExpr.(*StringExpr)
+	if !ok {
+		if providerExpr == nil {
+			diags.Extend(ExprError(obj, "missing provider name ('provider')"))
+		} else {
+			diags.Extend(ExprError(providerExpr, "provider name must be a string literal"))
+		}
+	}
+
+	if inputs == nil {
+		diags.Extend(ExprError(obj, "missing provider inputs ('inputs')"))
+	}
+
+	state, ok := stateExpr.(*ObjectExpr)
+	if !ok && state != nil {
+		diags.Extend(ExprError(stateExpr, "rotation state must be an object literal"))
+	}
+
+	return RotateSyntax(node, name, obj, provider, inputs, state), diags
+}
+
+func parseShortRotate(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
+	kvp := node.Index(0)
+	provider := StringSyntaxValue(name.Syntax().(*syntax.StringNode), strings.TrimPrefix(kvp.Key.Value(), "fn::rotate::"))
+
+	obj, ok := args.(*ObjectExpr)
+	if !ok {
+		diags := syntax.Diagnostics{ExprError(args, "the argument to fn::rotate must be an object containing 'inputs' and 'state'")}
+		return RotateSyntax(node, name, args, nil, nil, nil), diags
+	}
+
+	var inputs, stateExpr Expr
+	var diags syntax.Diagnostics
+
+	for i := 0; i < len(obj.Entries); i++ {
+		kvp := obj.Entries[i]
+		key := kvp.Key
+		switch key.GetValue() {
+		case "inputs":
+			inputs = kvp.Value
+		case "state":
+			stateExpr = kvp.Value
+		}
+	}
+
+	if inputs == nil {
+		diags.Extend(ExprError(obj, "missing provider inputs ('inputs')"))
+	}
+
+	state, ok := stateExpr.(*ObjectExpr)
+	if !ok && state != nil {
+		diags.Extend(ExprError(stateExpr, "rotation state must be an object literal"))
+	}
+
+	return RotateSyntax(node, name, args, provider, inputs, state), nil
+}
+
 func parseJoin(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
 	list, ok := args.(*ArrayExpr)
 	if !ok || len(list.Elements) != 2 {
diff --git a/eval/eval.go b/eval/eval.go
index 31945a0..b95c88e 100644
--- a/eval/eval.go
+++ b/eval/eval.go
@@ -37,6 +37,8 @@ import (
 type ProviderLoader interface {
 	// LoadProvider loads the provider with the given name.
 	LoadProvider(ctx context.Context, name string) (esc.Provider, error)
+	// LoadRotator loads the rotator with the given name.
+	LoadRotator(ctx context.Context, name string) (esc.Rotator, error)
 }
 
 // An EnvironmentLoader provides the environment evaluator the capability to load imported environment definitions.
@@ -83,7 +85,8 @@ func EvalEnvironment(
 	environments EnvironmentLoader,
 	execContext *esc.ExecContext,
 ) (*esc.Environment, syntax.Diagnostics) {
-	return evalEnvironment(ctx, false, name, env, decrypter, providers, environments, execContext, true)
+	opened, _, diags := evalEnvironment(ctx, false, false, name, env, decrypter, providers, environments, execContext, true)
+	return opened, diags
 }
 
 // CheckEnvironment symbolically evaluates the given environment. Calls to fn::open are not invoked, and instead
@@ -98,13 +101,29 @@ func CheckEnvironment(
 	execContext *esc.ExecContext,
 	showSecrets bool,
 ) (*esc.Environment, syntax.Diagnostics) {
-	return evalEnvironment(ctx, true, name, env, decrypter, providers, environments, execContext, showSecrets)
+	checked, _, diags := evalEnvironment(ctx, true, false, name, env, decrypter, providers, environments, execContext, showSecrets)
+	return checked, diags
+}
+
+// RotateEnvironment evaluates the given environment and invokes provider rotate methods.
+// The updated rotation state is returned with a set of patches to be written back to the environment.
+func RotateEnvironment(
+	ctx context.Context,
+	name string,
+	env *ast.EnvironmentDecl,
+	decrypter Decrypter,
+	providers ProviderLoader,
+	environments EnvironmentLoader,
+	execContext *esc.ExecContext,
+) (*esc.Environment, []*Patch, syntax.Diagnostics) {
+	return evalEnvironment(ctx, false, true, name, env, decrypter, providers, environments, execContext, true)
 }
 
 // evalEnvironment evaluates an environment and exports the result of evaluation.
 func evalEnvironment(
 	ctx context.Context,
 	validating bool,
+	rotating bool,
 	name string,
 	env *ast.EnvironmentDecl,
 	decrypter Decrypter,
@@ -112,12 +131,12 @@ func evalEnvironment(
 	envs EnvironmentLoader,
 	execContext *esc.ExecContext,
 	showSecrets bool,
-) (*esc.Environment, syntax.Diagnostics) {
+) (*esc.Environment, []*Patch, syntax.Diagnostics) {
 	if env == nil || (len(env.Values.GetEntries()) == 0 && len(env.Imports.GetElements()) == 0) {
-		return nil, nil
+		return nil, nil, nil
 	}
 
-	ec := newEvalContext(ctx, validating, name, env, decrypter, providers, envs, map[string]*imported{}, execContext, showSecrets)
+	ec := newEvalContext(ctx, validating, rotating, name, env, decrypter, providers, envs, map[string]*imported{}, execContext, showSecrets)
 	v, diags := ec.evaluate()
 
 	s := schema.Never().Schema()
@@ -139,7 +158,7 @@ func evalEnvironment(
 		Properties:       v.export(name).Value.(map[string]esc.Value),
 		Schema:           s,
 		ExecutionContext: executionContext,
-	}, diags
+	}, ec.patchOutputs, diags
 }
 
 type imported struct {
@@ -151,6 +170,7 @@ type imported struct {
 type evalContext struct {
 	ctx          context.Context      // the cancellation context for evaluation
 	validating   bool                 // true if we are only checking the environment
+	rotating     bool                 // true if we are invoking rotators
 	showSecrets  bool                 // true if secrets should be decrypted during validation
 	name         string               // the name of the environment
 	env          *ast.EnvironmentDecl // the root of the environment AST
@@ -165,12 +185,15 @@ type evalContext struct {
 	root      *expr  // the root expression
 	base      *value // the base value
 
+	patchOutputs []*Patch // updated rotation state generated during evaluation, to be written back to the environment definition
+
 	diags syntax.Diagnostics // diagnostics generated during evaluation
 }
 
 func newEvalContext(
 	ctx context.Context,
 	validating bool,
+	rotating bool,
 	name string,
 	env *ast.EnvironmentDecl,
 	decrypter Decrypter,
@@ -183,6 +206,7 @@ func newEvalContext(
 	return &evalContext{
 		ctx:          ctx,
 		validating:   validating,
+		rotating:     rotating,
 		showSecrets:  showSecrets,
 		name:         name,
 		env:          env,
@@ -302,6 +326,16 @@ func declare[Expr exprNode](e *evalContext, path string, x Expr, base *value) *e
 			inputSchema: schema.Always().Schema(),
 		}
 		return newExpr(path, repr, schema.Always().Schema(), base)
+	case *ast.RotateExpr:
+		repr := &rotateExpr{
+			node:        x,
+			provider:    declare(e, "", x.Provider, nil),
+			inputs:      declare(e, "", x.Inputs, nil),
+			state:       declare(e, "", x.State, nil),
+			inputSchema: schema.Always().Schema(),
+			stateSchema: schema.Always().Schema(),
+		}
+		return newExpr(path, repr, schema.Always().Schema(), base)
 	case *ast.SecretExpr:
 		if x.Plaintext != nil {
 			repr := &secretExpr{node: x, plaintext: declare(e, "", x.Plaintext, nil)}
@@ -462,7 +496,8 @@ func (e *evalContext) evaluateImport(myImports map[string]*value, decl *ast.Impo
 			return
 		}
 
-		imp := newEvalContext(e.ctx, e.validating, name, env, dec, e.providers, e.environments, e.imports, e.execContext, e.showSecrets)
+		// we only want to rotate the root environment, so set rotating flag to false when evaluating imports
+		imp := newEvalContext(e.ctx, e.validating, false, name, env, dec, e.providers, e.environments, e.imports, e.execContext, e.showSecrets)
 		v, diags := imp.evaluate()
 		e.diags.Extend(diags...)
 
@@ -526,6 +561,8 @@ func (e *evalContext) evaluateExpr(x *expr) *value {
 		val = e.evaluateBuiltinJoin(x, repr)
 	case *openExpr:
 		val = e.evaluateBuiltinOpen(x, repr)
+	case *rotateExpr:
+		val = e.evaluateBuiltinRotate(x, repr)
 	case *secretExpr:
 		val = e.evaluateBuiltinSecret(x, repr)
 	case *toBase64Expr:
@@ -932,6 +969,87 @@ func (e *evalContext) evaluateBuiltinOpen(x *expr, repr *openExpr) *value {
 	return unexport(output, x)
 }
 
+// evaluateBuiltinOpen evaluates a call to the fn::rotate builtin.
+func (e *evalContext) evaluateBuiltinRotate(x *expr, repr *rotateExpr) *value {
+	v := &value{def: x}
+
+	// Can happen if there are parse errors.
+	if repr.node.Provider == nil {
+		v.schema = schema.Always()
+		v.unknown = true
+		return v
+	}
+
+	rotator, err := e.providers.LoadRotator(e.ctx, repr.node.Provider.GetValue())
+	if err != nil {
+		e.errorf(repr.syntax(), "%v", err)
+	} else {
+		inputSchema, stateSchema, outputSchema := rotator.Schema()
+		if err := inputSchema.Compile(); err != nil {
+			e.errorf(repr.syntax(), "internal error: invalid input schema (%v)", err)
+		} else {
+			repr.inputSchema = inputSchema
+		}
+		if err := stateSchema.Compile(); err != nil {
+			e.errorf(repr.syntax(), "internal error: invalid state schema (%v)", err)
+		} else {
+			repr.stateSchema = stateSchema
+		}
+		if err := outputSchema.Compile(); err != nil {
+			e.errorf(repr.syntax(), "internal error: invalid schema (%v)", err)
+		} else {
+			x.schema = outputSchema
+		}
+	}
+	v.schema = x.schema
+
+	inputs, inputsOK := e.evaluateTypedExpr(repr.inputs, repr.inputSchema)
+	state, stateOK := e.evaluateTypedExpr(repr.state, repr.stateSchema)
+	if !inputsOK || inputs.containsUnknowns() || !stateOK || state.containsUnknowns() || e.validating || err != nil {
+		v.unknown = true
+		return v
+	}
+
+	// if rotating, invoke prior to open
+	if e.rotating {
+		newState, err := rotator.Rotate(
+			e.ctx,
+			inputs.export("").Value.(map[string]esc.Value),
+			state.export("").Value.(map[string]esc.Value),
+			e.execContext,
+		)
+		if err != nil {
+			e.errorf(repr.syntax(), "rotate: %s", err.Error())
+			v.unknown = true
+			return v
+		}
+
+		// todo: validate newState conforms to state schema
+
+		e.patchOutputs = append(e.patchOutputs, &Patch{
+			// rotation output is written back to the fn's `state` input
+			DocPath:     util.JoinKey(x.path, repr.node.Name().GetValue()) + ".state",
+			Replacement: newState,
+		})
+
+		// pass the updated state to open, as if it were already persisted
+		state = unexport(newState, x)
+	}
+
+	output, err := rotator.Open(
+		e.ctx,
+		inputs.export("").Value.(map[string]esc.Value),
+		state.export("").Value.(map[string]esc.Value),
+		e.execContext,
+	)
+	if err != nil {
+		e.errorf(repr.syntax(), "%s", err.Error())
+		v.unknown = true
+		return v
+	}
+	return unexport(output, x)
+}
+
 // evaluateBuiltinJoin evaluates a call to the fn::join builtin.
 func (e *evalContext) evaluateBuiltinJoin(x *expr, repr *joinExpr) *value {
 	v := &value{def: x, schema: x.schema}
diff --git a/eval/expr.go b/eval/expr.go
index f13b114..e54feee 100644
--- a/eval/expr.go
+++ b/eval/expr.go
@@ -16,7 +16,6 @@ package eval
 
 import (
 	"fmt"
-
 	"github.com/hashicorp/hcl/v2"
 	"github.com/pulumi/esc"
 	"github.com/pulumi/esc/ast"
@@ -204,6 +203,41 @@ func (x *expr) export(environment string) esc.Expr {
 				Arg:       repr.inputs.export(environment),
 			}
 		}
+	case *rotateExpr:
+		name := repr.node.Name().Value
+		if name == "fn::rotate" {
+			ex.Builtin = &esc.BuiltinExpr{
+				Name:      name,
+				NameRange: convertRange(repr.node.Name().Syntax().Syntax().Range(), environment),
+				ArgSchema: schema.Record(schema.SchemaMap{
+					"provider": schema.String().Schema(),
+					"inputs":   repr.inputSchema,
+					"state":    repr.stateSchema,
+				}).Schema(),
+				Arg: esc.Expr{
+					Object: map[string]esc.Expr{
+						"provider": repr.provider.export(environment),
+						"inputs":   repr.inputs.export(environment),
+						"state":    repr.state.export(environment),
+					},
+				},
+			}
+		} else {
+			ex.Builtin = &esc.BuiltinExpr{
+				Name:      name,
+				NameRange: convertRange(repr.node.Name().Syntax().Syntax().Range(), environment),
+				ArgSchema: schema.Record(schema.SchemaMap{
+					"inputs": repr.inputSchema,
+					"state":  repr.stateSchema,
+				}).Schema(),
+				Arg: esc.Expr{
+					Object: map[string]esc.Expr{
+						"inputs": repr.inputs.export(environment),
+						"state":  repr.state.export(environment),
+					},
+				},
+			}
+		}
 	case *secretExpr:
 		var arg esc.Expr
 		if repr.plaintext != nil {
@@ -371,6 +405,22 @@ func (x *openExpr) syntax() ast.Expr {
 	return x.node
 }
 
+// rotateExpr represents a call to the fn::rotate builtin.
+type rotateExpr struct {
+	node *ast.RotateExpr
+
+	provider *expr
+	inputs   *expr
+	state    *expr
+
+	inputSchema *schema.Schema
+	stateSchema *schema.Schema
+}
+
+func (x *rotateExpr) syntax() ast.Expr {
+	return x.node
+}
+
 // toJSONExpr represents a call to the fn::toJSON builtin.
 type toJSONExpr struct {
 	node *ast.ToJSONExpr
diff --git a/eval/patch.go b/eval/patch.go
new file mode 100644
index 0000000..59f6464
--- /dev/null
+++ b/eval/patch.go
@@ -0,0 +1,92 @@
+// Copyright 2025, Pulumi Corporation.
+//
+// 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 eval
+
+import (
+	"github.com/pulumi/esc"
+	"github.com/pulumi/esc/syntax/encoding"
+	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
+	"gopkg.in/yaml.v3"
+)
+
+// Patch represents a value that should be written back to the environment at the given path.
+type Patch struct {
+	DocPath     string
+	Replacement esc.Value
+}
+
+// ApplyValuePatches applies a set of patches values to an environment definition.
+// If patch values contain secret values, they will be wrapped with fn::secret.
+func ApplyValuePatches(source []byte, patches []*Patch) ([]byte, error) {
+	var doc yaml.Node
+	if err := yaml.Unmarshal(source, &doc); err != nil {
+		return nil, err
+	}
+
+	for _, patch := range patches {
+		path, err := resource.ParsePropertyPath("values." + patch.DocPath)
+		if err != nil {
+			return nil, err
+		}
+
+		// convert the esc.Value into a yaml node that can be set on the environment
+		replacement := valueToSecretJSON(patch.Replacement)
+		bytes, err := yaml.Marshal(replacement)
+		if err != nil {
+			return nil, err
+		}
+		var yamlValue yaml.Node
+		if err := yaml.Unmarshal(bytes, &yamlValue); err != nil {
+			return nil, err
+		}
+		yamlValue = *yamlValue.Content[0]
+
+		_, err = encoding.YAMLSyntax{Node: &doc}.Set(nil, path, yamlValue)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return yaml.Marshal(doc.Content[0])
+}
+
+// valueToSecretJSON converts a Value into a plain-old-JSON value, but secret values are wrapped with fn::secret
+func valueToSecretJSON(v esc.Value) any {
+	ret := func() any {
+		switch pv := v.Value.(type) {
+		case []esc.Value:
+			a := make([]any, len(pv))
+			for i, v := range pv {
+				a[i] = valueToSecretJSON(v)
+			}
+			return a
+		case map[string]esc.Value:
+			m := make(map[string]any, len(pv))
+			for k, v := range pv {
+				m[k] = valueToSecretJSON(v)
+			}
+			return m
+		default:
+			return pv
+		}
+	}()
+	// wrap secret values
+	if v.Secret {
+		return map[string]any{
+			"fn::secret": ret,
+		}
+	}
+	return ret
+}
diff --git a/provider.go b/provider.go
index 153cbaa..7caf062 100644
--- a/provider.go
+++ b/provider.go
@@ -29,3 +29,16 @@ type Provider interface {
 	// Open retrieves the provider's secrets.
 	Open(ctx context.Context, inputs map[string]Value, executionContext EnvExecContext) (Value, error)
 }
+
+// A Rotator enables environments to rotate a secret.
+// It is the responsibility of the caller to appropriately persist rotation state (e.g. by writing it back to the environment definition).
+type Rotator interface {
+	// Schema returns the rotator's input, state, and output schemata.
+	Schema() (inputs, state, outputs *schema.Schema)
+
+	// Open retrieves the rotator's secrets, using persisted state.
+	Open(ctx context.Context, inputs, state map[string]Value, executionContext EnvExecContext) (Value, error)
+
+	// Rotate rotates the provider's secret, and returns the rotator's new state to be persisted.
+	Rotate(ctx context.Context, inputs, state map[string]Value, executionContext EnvExecContext) (Value, error)
+}


Test patch:
diff --git a/analysis/common_test.go b/analysis/common_test.go
index c21d8c1..3e6dfd2 100644
--- a/analysis/common_test.go
+++ b/analysis/common_test.go
@@ -71,6 +71,10 @@ func (testProviders) LoadProvider(ctx context.Context, name string) (esc.Provide
 	return nil, fmt.Errorf("unknown provider %q", name)
 }
 
+func (testProviders) LoadRotator(ctx context.Context, name string) (esc.Rotator, error) {
+	return nil, fmt.Errorf("unknown rotator %q", name)
+}
+
 type testEnvironments struct{}
 
 func (testEnvironments) LoadEnvironment(ctx context.Context, name string) ([]byte, eval.Decrypter, error) {
diff --git a/cmd/esc/cli/cli_test.go b/cmd/esc/cli/cli_test.go
index 87674f7..53ec1a4 100644
--- a/cmd/esc/cli/cli_test.go
+++ b/cmd/esc/cli/cli_test.go
@@ -215,6 +215,10 @@ func (testProviders) LoadProvider(ctx context.Context, name string) (esc.Provide
 	return nil, fmt.Errorf("unknown provider %q", name)
 }
 
+func (testProviders) LoadRotator(ctx context.Context, name string) (esc.Rotator, error) {
+	return nil, fmt.Errorf("unknown rotator %q", name)
+}
+
 type rot128 struct{}
 
 func (rot128) Encrypt(_ context.Context, plaintext []byte) ([]byte, error) {
diff --git a/eval/eval_test.go b/eval/eval_test.go
index 22dc803..5bab313 100644
--- a/eval/eval_test.go
+++ b/eval/eval_test.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"slices"
 	"sort"
 	"strings"
 	"testing"
@@ -136,6 +137,33 @@ func (testProvider) Open(ctx context.Context, inputs map[string]esc.Value, conte
 	return esc.NewValue(inputs), nil
 }
 
+type swapRotator struct{}
+
+func (swapRotator) Schema() (*schema.Schema, *schema.Schema, *schema.Schema) {
+	inputSchema := schema.Always()
+	stateSchema := schema.Record(schema.BuilderMap{
+		"a": schema.String(),
+		"b": schema.String(),
+	}).Schema()
+	outputSchema := schema.Record(schema.BuilderMap{
+		"a": schema.String(),
+		"b": schema.String(),
+	}).Schema()
+	return inputSchema, stateSchema, outputSchema
+}
+
+func (swapRotator) Open(ctx context.Context, inputs, state map[string]esc.Value, context esc.EnvExecContext) (esc.Value, error) {
+	return esc.NewValue(state), nil
+}
+
+func (swapRotator) Rotate(ctx context.Context, inputs, state map[string]esc.Value, context esc.EnvExecContext) (esc.Value, error) {
+	newState := esc.NewValue(map[string]esc.Value{
+		"a": state["b"],
+		"b": state["a"],
+	})
+	return newState, nil
+}
+
 type testProviders struct {
 	benchDelay time.Duration
 }
@@ -154,6 +182,14 @@ func (tp testProviders) LoadProvider(ctx context.Context, name string) (esc.Prov
 	return nil, fmt.Errorf("unknown provider %q", name)
 }
 
+func (testProviders) LoadRotator(ctx context.Context, name string) (esc.Rotator, error) {
+	switch name {
+	case "swap":
+		return swapRotator{}, nil
+	}
+	return nil, fmt.Errorf("unknown rotator %q", name)
+}
+
 type testEnvironments struct {
 	root string
 }
@@ -245,6 +281,7 @@ func TestEval(t *testing.T) {
 	type testOverrides struct {
 		ShowSecrets     bool   `json:"showSecrets,omitempty"`
 		RootEnvironment string `json:"rootEnvironment,omitempty"`
+		Rotate          bool   `json:"rotate,omitempty"`
 	}
 
 	type expectedData struct {
@@ -256,6 +293,10 @@ func TestEval(t *testing.T) {
 		Eval             *esc.Environment   `json:"eval,omitempty"`
 		EvalJSONRedacted any                `json:"evalJsonRedacted,omitempty"`
 		EvalJSONRevealed any                `json:"evalJSONRevealed,omitempty"`
+		RotateDiags      syntax.Diagnostics `json:"rotateDiags,omitempty"`
+		Rotate           *esc.Environment   `json:"rotate,omitempty"`
+		RotateJSON       any                `json:"rotateJson,omitempty"`
+		RotatePatches    []*Patch           `json:"rotatePatches,omitempty"`
 	}
 
 	path := filepath.Join("testdata", "eval")
@@ -295,6 +336,7 @@ func TestEval(t *testing.T) {
 				environmentName = overrides.RootEnvironment
 			}
 			showSecrets := overrides.ShowSecrets
+			doRotate := overrides.Rotate
 
 			if accept() {
 				env, loadDiags, err := LoadYAMLBytes(environmentName, envBytes)
@@ -309,9 +351,18 @@ func TestEval(t *testing.T) {
 					&testEnvironments{basePath}, execContext)
 				sortEnvironmentDiagnostics(evalDiags)
 
+				var rotated *esc.Environment
+				var patches []*Patch
+				var rotateDiags syntax.Diagnostics
+				if doRotate {
+					rotated, patches, rotateDiags = RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
+						&testEnvironments{basePath}, execContext)
+				}
+
 				var checkJSON any
 				var evalJSONRedacted any
 				var evalJSONRevealed any
+				var rotateJSON any
 				if check != nil {
 					check = normalize(t, check)
 					checkJSON = esc.NewValue(check.Properties).ToJSON(true)
@@ -321,6 +372,10 @@ func TestEval(t *testing.T) {
 					evalJSONRedacted = esc.NewValue(actual.Properties).ToJSON(true)
 					evalJSONRevealed = esc.NewValue(actual.Properties).ToJSON(false)
 				}
+				if rotated != nil {
+					rotated = normalize(t, rotated)
+					rotateJSON = esc.NewValue(rotated.Properties).ToJSON(true)
+				}
 
 				bytes, err := json.MarshalIndent(expectedData{
 					LoadDiags:        loadDiags,
@@ -331,6 +386,10 @@ func TestEval(t *testing.T) {
 					EvalJSONRedacted: evalJSONRedacted,
 					EvalJSONRevealed: evalJSONRevealed,
 					CheckJSON:        checkJSON,
+					RotateDiags:      rotateDiags,
+					Rotate:           rotated,
+					RotateJSON:       rotateJSON,
+					RotatePatches:    patches,
 				}, "", "    ")
 				bytes = append(bytes, '\n')
 				require.NoError(t, err)
@@ -364,9 +423,26 @@ func TestEval(t *testing.T) {
 			sortEnvironmentDiagnostics(diags)
 			require.Equal(t, expected.EvalDiags, diags)
 
+			var rotated *esc.Environment
+			if doRotate {
+				rotated_, patches, diags := RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
+					&testEnvironments{basePath}, execContext)
+
+				sortEnvironmentDiagnostics(diags)
+				require.Equal(t, expected.RotateDiags, diags)
+
+				slices.SortFunc(patches, func(a, b *Patch) int {
+					return strings.Compare(a.DocPath, b.DocPath)
+				})
+				require.Equal(t, expected.RotatePatches, patches)
+
+				rotated = rotated_
+			}
+
 			// work around a schema comparison issue due to the 'compiled' field by roundtripping through JSON
 			check = normalize(t, check)
 			actual = normalize(t, actual)
+			rotated = normalize(t, rotated)
 
 			// work around a comparison issue when comparing nil slices/maps against zero-length slices/maps
 			if actual != nil {
@@ -389,8 +465,18 @@ func TestEval(t *testing.T) {
 				t.Logf("check: %v", string(bytes))
 			}
 
+			if rotated != nil {
+				rotateJSON := esc.NewValue(check.Properties).ToJSON(true)
+				assert.Equal(t, expected.CheckJSON, rotateJSON)
+
+				bytes, err := json.MarshalIndent(rotateJSON, "", "  ")
+				require.NoError(t, err)
+				t.Logf("rotate: %v", string(bytes))
+			}
+
 			assert.Equal(t, expected.Check, check)
 			assert.Equal(t, expected.Eval, actual)
+			assert.Equal(t, expected.Rotate, rotated)
 		})
 	}
 }
diff --git a/eval/patch_test.go b/eval/patch_test.go
new file mode 100644
index 0000000..f94567c
--- /dev/null
+++ b/eval/patch_test.go
@@ -0,0 +1,39 @@
+// Copyright 2025, Pulumi Corporation.
+//
+// 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 eval
+
+import (
+	"github.com/pulumi/esc"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestValueToSecretJSON(t *testing.T) {
+	t.Run("nested secrets", func(t *testing.T) {
+		actual := valueToSecretJSON(esc.NewValue(map[string]esc.Value{
+			"foo": esc.NewValue(map[string]esc.Value{
+				"bar": esc.NewSecret("secret"),
+			}),
+		}))
+		expected := map[string]any{
+			"foo": map[string]any{
+				"bar": map[string]any{
+					"fn::secret": "secret",
+				},
+			},
+		}
+		require.Equal(t, expected, actual)
+	})
+}
diff --git a/eval/rotate_example_test.go b/eval/rotate_example_test.go
new file mode 100644
index 0000000..4be47b0
--- /dev/null
+++ b/eval/rotate_example_test.go
@@ -0,0 +1,79 @@
+// Copyright 2025, Pulumi Corporation.
+//
+// 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 eval
+
+import (
+	"context"
+	"fmt"
+	"github.com/pulumi/esc"
+)
+
+func ExampleRotate() {
+	const def = `
+values:
+  a:
+    a:
+      fn::rotate:
+        provider: swap
+        inputs:
+          foo: bar
+        state:
+          a: a
+          b: b
+    b:
+    - c:
+        fn::rotate::swap:
+          inputs:
+            foo: bar
+          state:
+            a: 
+              fn::secret: a
+            b: b
+`
+	env, _, _ := LoadYAMLBytes("<stdin>", []byte(def))
+
+	// rotate the environment
+	execContext, _ := esc.NewExecContext(nil)
+	_, patches, _ := RotateEnvironment(context.Background(), "<stdin>", env, rot128{}, testProviders{}, &testEnvironments{}, execContext)
+
+	// writeback state patches
+	updated, _ := ApplyValuePatches([]byte(def), patches)
+
+	// encrypt secret values
+	encryptedYaml, _ := EncryptSecrets(context.Background(), "<stdin>", updated, rot128{})
+
+	fmt.Println(string(encryptedYaml))
+	// Output:
+	// values:
+	//   a:
+	//     a:
+	//       fn::rotate:
+	//         provider: swap
+	//         inputs:
+	//           foo: bar
+	//         state:
+	//           a: b
+	//           b: a
+	//     b:
+	//       - c:
+	//           fn::rotate::swap:
+	//             inputs:
+	//               foo: bar
+	//             state:
+	//               a: b
+	//               b:
+	//                 fn::secret:
+	//                   ciphertext: ZXNjeAAAAAHhQRt8TQ==
+}
diff --git a/eval/testdata/eval/rotate/env.yaml b/eval/testdata/eval/rotate/env.yaml
new file mode 100644
index 0000000..cb8231e
--- /dev/null
+++ b/eval/testdata/eval/rotate/env.yaml
@@ -0,0 +1,17 @@
+values:
+  full:
+    fn::rotate:
+      provider: swap
+      inputs: {}
+      state:
+        a: bar1
+        b: bar2
+  short:
+    fn::rotate::swap:
+      inputs: {}
+      state:
+        a: bar1
+        b: bar2
+
+  reference:
+    test: ${full.a} ${short.b}
\ No newline at end of file
diff --git a/eval/testdata/eval/rotate/expected.json b/eval/testdata/eval/rotate/expected.json
new file mode 100644
index 0000000..2a1ee0e
--- /dev/null
+++ b/eval/testdata/eval/rotate/expected.json
@@ -0,0 +1,3161 @@
+{
+    "check": {
+        "exprs": {
+            "full": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 3,
+                        "column": 5,
+                        "byte": 20
+                    },
+                    "end": {
+                        "line": 8,
+                        "column": 16,
+                        "byte": 114
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "a": {
+                            "type": "string"
+                        },
+                        "b": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "builtin": {
+                    "name": "fn::rotate",
+                    "nameRange": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 3,
+                            "column": 5,
+                            "byte": 20
+                        },
+                        "end": {
+                            "line": 3,
+                            "column": 15,
+                            "byte": 30
+                        }
+                    },
+                    "argSchema": {
+                        "properties": {
+                            "inputs": true,
+                            "provider": {
+                                "type": "string"
+                            },
+                            "state": {
+                                "properties": {
+                                    "a": {
+                                        "type": "string"
+                                    },
+                                    "b": {
+                                        "type": "string"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "a",
+                                    "b"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "inputs",
+                            "provider",
+                            "state"
+                        ]
+                    },
+                    "arg": {
+                        "range": {
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        },
+                        "object": {
+                            "inputs": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 5,
+                                        "column": 15,
+                                        "byte": 67
+                                    },
+                                    "end": {
+                                        "line": 5,
+                                        "column": 15,
+                                        "byte": 67
+                                    }
+                                },
+                                "schema": {
+                                    "type": "object"
+                                }
+                            },
+                            "provider": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 4,
+                                        "column": 17,
+                                        "byte": 48
+                                    },
+                                    "end": {
+                                        "line": 4,
+                                        "column": 21,
+                                        "byte": 52
+                                    }
+                                },
+                                "schema": {
+                                    "type": "string",
+                                    "const": "swap"
+                                },
+                                "literal": "swap"
+                            },
+                            "state": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 7,
+                                        "column": 9,
+                                        "byte": 91
+                                    },
+                                    "end": {
+                                        "line": 8,
+                                        "column": 16,
+                                        "byte": 114
+                                    }
+                                },
+                                "schema": {
+                                    "properties": {
+                                        "a": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "b": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        }
+                                    },
+                                    "type": "object",
+                                    "required": [
+                                        "a",
+                                        "b"
+                                    ]
+                                },
+                                "keyRanges": {
+                                    "a": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 7,
+                                            "column": 9,
+                                            "byte": 91
+                                        },
+                                        "end": {
+                                            "line": 7,
+                                            "column": 10,
+                                            "byte": 92
+                                        }
+                                    },
+                                    "b": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 8,
+                                            "column": 9,
+                                            "byte": 107
+                                        },
+                                        "end": {
+                                            "line": 8,
+                                            "column": 10,
+                                            "byte": 108
+                                        }
+                                    }
+                                },
+                                "object": {
+                                    "a": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 7,
+                                                "column": 12,
+                                                "byte": 94
+                                            },
+                                            "end": {
+                                                "line": 7,
+                                                "column": 16,
+                                                "byte": 98
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "literal": "bar1"
+                                    },
+                                    "b": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 8,
+                                                "column": 12,
+                                                "byte": 110
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        },
+                                        "literal": "bar2"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            "reference": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 17,
+                        "column": 5,
+                        "byte": 0
+                    },
+                    "end": {
+                        "line": 17,
+                        "column": 31,
+                        "byte": 0
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "test": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "test"
+                    ]
+                },
+                "keyRanges": {
+                    "test": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 17,
+                            "column": 5,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 17,
+                            "column": 9,
+                            "byte": 0
+                        }
+                    }
+                },
+                "object": {
+                    "test": {
+                        "range": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 17,
+                                "column": 11,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 17,
+                                "column": 31,
+                                "byte": 0
+                            }
+                        },
+                        "schema": {
+                            "type": "string"
+                        },
+                        "interpolate": [
+                            {
+                                "value": [
+                                    {
+                                        "key": "full",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 13,
+                                                "byte": 2
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 17,
+                                                "byte": 6
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 3,
+                                                "column": 5,
+                                                "byte": 20
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        }
+                                    },
+                                    {
+                                        "key": "a",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 17,
+                                                "byte": 6
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 19,
+                                                "byte": 8
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 11,
+                                                "byte": 0
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 31,
+                                                "byte": 0
+                                            }
+                                        }
+                                    }
+                                ]
+                            },
+                            {
+                                "text": " ",
+                                "value": [
+                                    {
+                                        "key": "short",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 23,
+                                                "byte": 12
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 28,
+                                                "byte": 17
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 10,
+                                                "column": 5,
+                                                "byte": 128
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        }
+                                    },
+                                    {
+                                        "key": "b",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 28,
+                                                "byte": 17
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 30,
+                                                "byte": 19
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 11,
+                                                "byte": 0
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 31,
+                                                "byte": 0
+                                            }
+                                        }
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            },
+            "short": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 10,
+                        "column": 5,
+                        "byte": 128
+                    },
+                    "end": {
+                        "line": 14,
+                        "column": 16,
+                        "byte": 207
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "a": {
+                            "type": "string"
+                        },
+                        "b": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "builtin": {
+                    "name": "fn::rotate::swap",
+                    "nameRange": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 10,
+                            "column": 5,
+                            "byte": 128
+                        },
+                        "end": {
+                            "line": 10,
+                            "column": 21,
+                            "byte": 144
+                        }
+                    },
+                    "argSchema": {
+                        "properties": {
+                            "inputs": true,
+                            "state": {
+                                "properties": {
+                                    "a": {
+                                        "type": "string"
+                                    },
+                                    "b": {
+                                        "type": "string"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "a",
+                                    "b"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "inputs",
+                            "state"
+                        ]
+                    },
+                    "arg": {
+                        "range": {
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        },
+                        "object": {
+                            "inputs": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 11,
+                                        "column": 15,
+                                        "byte": 160
+                                    },
+                                    "end": {
+                                        "line": 11,
+                                        "column": 15,
+                                        "byte": 160
+                                    }
+                                },
+                                "schema": {
+                                    "type": "object"
+                                }
+                            },
+                            "state": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 13,
+                                        "column": 9,
+                                        "byte": 184
+                                    },
+                                    "end": {
+                                        "line": 14,
+                                        "column": 16,
+                                        "byte": 207
+                                    }
+                                },
+                                "schema": {
+                                    "properties": {
+                                        "a": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "b": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        }
+                                    },
+                                    "type": "object",
+                                    "required": [
+                                        "a",
+                                        "b"
+                                    ]
+                                },
+                                "keyRanges": {
+                                    "a": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 13,
+                                            "column": 9,
+                                            "byte": 184
+                                        },
+                                        "end": {
+                                            "line": 13,
+                                            "column": 10,
+                                            "byte": 185
+                                        }
+                                    },
+                                    "b": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 14,
+                                            "column": 9,
+                                            "byte": 200
+                                        },
+                                        "end": {
+                                            "line": 14,
+                                            "column": 10,
+                                            "byte": 201
+                                        }
+                                    }
+                                },
+                                "object": {
+                                    "a": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 13,
+                                                "column": 12,
+                                                "byte": 187
+                                            },
+                                            "end": {
+                                                "line": 13,
+                                                "column": 16,
+                                                "byte": 191
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "literal": "bar1"
+                                    },
+                                    "b": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 14,
+                                                "column": 12,
+                                                "byte": 203
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        },
+                                        "literal": "bar2"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "properties": {
+            "full": {
+                "unknown": true,
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 3,
+                            "column": 5,
+                            "byte": 20
+                        },
+                        "end": {
+                            "line": 8,
+                            "column": 16,
+                            "byte": 114
+                        }
+                    }
+                }
+            },
+            "reference": {
+                "value": {
+                    "test": {
+                        "value": "[unknown]",
+                        "unknown": true,
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 17,
+                                    "column": 11,
+                                    "byte": 0
+                                },
+                                "end": {
+                                    "line": 17,
+                                    "column": 31,
+                                    "byte": 0
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 17,
+                            "column": 5,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 17,
+                            "column": 31,
+                            "byte": 0
+                        }
+                    }
+                }
+            },
+            "short": {
+                "unknown": true,
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 10,
+                            "column": 5,
+                            "byte": 128
+                        },
+                        "end": {
+                            "line": 14,
+                            "column": 16,
+                            "byte": 207
+                        }
+                    }
+                }
+            }
+        },
+        "schema": {
+            "properties": {
+                "full": {
+                    "properties": {
+                        "a": {
+                            "type": "string"
+                        },
+                        "b": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "reference": {
+                    "properties": {
+                        "test": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "test"
+                    ]
+                },
+                "short": {
+                    "properties": {
+                        "a": {
+                            "type": "string"
+                        },
+                        "b": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                }
+            },
+            "type": "object",
+            "required": [
+                "full",
+                "reference",
+                "short"
+            ]
+        },
+        "executionContext": {
+            "properties": {
+                "currentEnvironment": {
+                    "value": {
+                        "name": {
+                            "value": "rotate",
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                },
+                "pulumi": {
+                    "value": {
+                        "user": {
+                            "value": {
+                                "id": {
+                                    "value": "USER_123",
+                                    "trace": {
+                                        "def": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 0,
+                                                "column": 0,
+                                                "byte": 0
+                                            },
+                                            "end": {
+                                                "line": 0,
+                                                "column": 0,
+                                                "byte": 0
+                                            }
+                                        }
+                                    }
+                                }
+                            },
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                },
+                "rootEnvironment": {
+                    "value": {
+                        "name": {
+                            "value": "rotate",
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                }
+            },
+            "schema": {
+                "properties": {
+                    "currentEnvironment": {
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "const": "rotate"
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "name"
+                        ]
+                    },
+                    "pulumi": {
+                        "properties": {
+                            "user": {
+                                "properties": {
+                                    "id": {
+                                        "type": "string",
+                                        "const": "USER_123"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "id"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "user"
+                        ]
+                    },
+                    "rootEnvironment": {
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "const": "rotate"
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "name"
+                        ]
+                    }
+                },
+                "type": "object",
+                "required": [
+                    "currentEnvironment",
+                    "pulumi",
+                    "rootEnvironment"
+                ]
+            }
+        }
+    },
+    "checkJson": {
+        "full": "[unknown]",
+        "reference": {
+            "test": "[unknown]"
+        },
+        "short": "[unknown]"
+    },
+    "eval": {
+        "exprs": {
+            "full": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 3,
+                        "column": 5,
+                        "byte": 20
+                    },
+                    "end": {
+                        "line": 8,
+                        "column": 16,
+                        "byte": 114
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar1"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar2"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "builtin": {
+                    "name": "fn::rotate",
+                    "nameRange": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 3,
+                            "column": 5,
+                            "byte": 20
+                        },
+                        "end": {
+                            "line": 3,
+                            "column": 15,
+                            "byte": 30
+                        }
+                    },
+                    "argSchema": {
+                        "properties": {
+                            "inputs": true,
+                            "provider": {
+                                "type": "string"
+                            },
+                            "state": {
+                                "properties": {
+                                    "a": {
+                                        "type": "string"
+                                    },
+                                    "b": {
+                                        "type": "string"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "a",
+                                    "b"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "inputs",
+                            "provider",
+                            "state"
+                        ]
+                    },
+                    "arg": {
+                        "range": {
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        },
+                        "object": {
+                            "inputs": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 5,
+                                        "column": 15,
+                                        "byte": 67
+                                    },
+                                    "end": {
+                                        "line": 5,
+                                        "column": 15,
+                                        "byte": 67
+                                    }
+                                },
+                                "schema": {
+                                    "type": "object"
+                                }
+                            },
+                            "provider": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 4,
+                                        "column": 17,
+                                        "byte": 48
+                                    },
+                                    "end": {
+                                        "line": 4,
+                                        "column": 21,
+                                        "byte": 52
+                                    }
+                                },
+                                "schema": {
+                                    "type": "string",
+                                    "const": "swap"
+                                },
+                                "literal": "swap"
+                            },
+                            "state": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 7,
+                                        "column": 9,
+                                        "byte": 91
+                                    },
+                                    "end": {
+                                        "line": 8,
+                                        "column": 16,
+                                        "byte": 114
+                                    }
+                                },
+                                "schema": {
+                                    "properties": {
+                                        "a": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "b": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        }
+                                    },
+                                    "type": "object",
+                                    "required": [
+                                        "a",
+                                        "b"
+                                    ]
+                                },
+                                "keyRanges": {
+                                    "a": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 7,
+                                            "column": 9,
+                                            "byte": 91
+                                        },
+                                        "end": {
+                                            "line": 7,
+                                            "column": 10,
+                                            "byte": 92
+                                        }
+                                    },
+                                    "b": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 8,
+                                            "column": 9,
+                                            "byte": 107
+                                        },
+                                        "end": {
+                                            "line": 8,
+                                            "column": 10,
+                                            "byte": 108
+                                        }
+                                    }
+                                },
+                                "object": {
+                                    "a": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 7,
+                                                "column": 12,
+                                                "byte": 94
+                                            },
+                                            "end": {
+                                                "line": 7,
+                                                "column": 16,
+                                                "byte": 98
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "literal": "bar1"
+                                    },
+                                    "b": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 8,
+                                                "column": 12,
+                                                "byte": 110
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        },
+                                        "literal": "bar2"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            "reference": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 17,
+                        "column": 5,
+                        "byte": 0
+                    },
+                    "end": {
+                        "line": 17,
+                        "column": 31,
+                        "byte": 0
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "test": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "test"
+                    ]
+                },
+                "keyRanges": {
+                    "test": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 17,
+                            "column": 5,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 17,
+                            "column": 9,
+                            "byte": 0
+                        }
+                    }
+                },
+                "object": {
+                    "test": {
+                        "range": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 17,
+                                "column": 11,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 17,
+                                "column": 31,
+                                "byte": 0
+                            }
+                        },
+                        "schema": {
+                            "type": "string"
+                        },
+                        "interpolate": [
+                            {
+                                "value": [
+                                    {
+                                        "key": "full",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 13,
+                                                "byte": 2
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 17,
+                                                "byte": 6
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 3,
+                                                "column": 5,
+                                                "byte": 20
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        }
+                                    },
+                                    {
+                                        "key": "a",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 17,
+                                                "byte": 6
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 19,
+                                                "byte": 8
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 3,
+                                                "column": 5,
+                                                "byte": 20
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        }
+                                    }
+                                ]
+                            },
+                            {
+                                "text": " ",
+                                "value": [
+                                    {
+                                        "key": "short",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 23,
+                                                "byte": 12
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 28,
+                                                "byte": 17
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 10,
+                                                "column": 5,
+                                                "byte": 128
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        }
+                                    },
+                                    {
+                                        "key": "b",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 28,
+                                                "byte": 17
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 30,
+                                                "byte": 19
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 10,
+                                                "column": 5,
+                                                "byte": 128
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        }
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            },
+            "short": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 10,
+                        "column": 5,
+                        "byte": 128
+                    },
+                    "end": {
+                        "line": 14,
+                        "column": 16,
+                        "byte": 207
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar1"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar2"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "builtin": {
+                    "name": "fn::rotate::swap",
+                    "nameRange": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 10,
+                            "column": 5,
+                            "byte": 128
+                        },
+                        "end": {
+                            "line": 10,
+                            "column": 21,
+                            "byte": 144
+                        }
+                    },
+                    "argSchema": {
+                        "properties": {
+                            "inputs": true,
+                            "state": {
+                                "properties": {
+                                    "a": {
+                                        "type": "string"
+                                    },
+                                    "b": {
+                                        "type": "string"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "a",
+                                    "b"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "inputs",
+                            "state"
+                        ]
+                    },
+                    "arg": {
+                        "range": {
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        },
+                        "object": {
+                            "inputs": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 11,
+                                        "column": 15,
+                                        "byte": 160
+                                    },
+                                    "end": {
+                                        "line": 11,
+                                        "column": 15,
+                                        "byte": 160
+                                    }
+                                },
+                                "schema": {
+                                    "type": "object"
+                                }
+                            },
+                            "state": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 13,
+                                        "column": 9,
+                                        "byte": 184
+                                    },
+                                    "end": {
+                                        "line": 14,
+                                        "column": 16,
+                                        "byte": 207
+                                    }
+                                },
+                                "schema": {
+                                    "properties": {
+                                        "a": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "b": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        }
+                                    },
+                                    "type": "object",
+                                    "required": [
+                                        "a",
+                                        "b"
+                                    ]
+                                },
+                                "keyRanges": {
+                                    "a": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 13,
+                                            "column": 9,
+                                            "byte": 184
+                                        },
+                                        "end": {
+                                            "line": 13,
+                                            "column": 10,
+                                            "byte": 185
+                                        }
+                                    },
+                                    "b": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 14,
+                                            "column": 9,
+                                            "byte": 200
+                                        },
+                                        "end": {
+                                            "line": 14,
+                                            "column": 10,
+                                            "byte": 201
+                                        }
+                                    }
+                                },
+                                "object": {
+                                    "a": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 13,
+                                                "column": 12,
+                                                "byte": 187
+                                            },
+                                            "end": {
+                                                "line": 13,
+                                                "column": 16,
+                                                "byte": 191
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "literal": "bar1"
+                                    },
+                                    "b": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 14,
+                                                "column": 12,
+                                                "byte": 203
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        },
+                                        "literal": "bar2"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "properties": {
+            "full": {
+                "value": {
+                    "a": {
+                        "value": "bar1",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 3,
+                                    "column": 5,
+                                    "byte": 20
+                                },
+                                "end": {
+                                    "line": 8,
+                                    "column": 16,
+                                    "byte": 114
+                                }
+                            }
+                        }
+                    },
+                    "b": {
+                        "value": "bar2",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 3,
+                                    "column": 5,
+                                    "byte": 20
+                                },
+                                "end": {
+                                    "line": 8,
+                                    "column": 16,
+                                    "byte": 114
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 3,
+                            "column": 5,
+                            "byte": 20
+                        },
+                        "end": {
+                            "line": 8,
+                            "column": 16,
+                            "byte": 114
+                        }
+                    }
+                }
+            },
+            "reference": {
+                "value": {
+                    "test": {
+                        "value": "bar1 bar2",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 17,
+                                    "column": 11,
+                                    "byte": 0
+                                },
+                                "end": {
+                                    "line": 17,
+                                    "column": 31,
+                                    "byte": 0
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 17,
+                            "column": 5,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 17,
+                            "column": 31,
+                            "byte": 0
+                        }
+                    }
+                }
+            },
+            "short": {
+                "value": {
+                    "a": {
+                        "value": "bar1",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 10,
+                                    "column": 5,
+                                    "byte": 128
+                                },
+                                "end": {
+                                    "line": 14,
+                                    "column": 16,
+                                    "byte": 207
+                                }
+                            }
+                        }
+                    },
+                    "b": {
+                        "value": "bar2",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 10,
+                                    "column": 5,
+                                    "byte": 128
+                                },
+                                "end": {
+                                    "line": 14,
+                                    "column": 16,
+                                    "byte": 207
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 10,
+                            "column": 5,
+                            "byte": 128
+                        },
+                        "end": {
+                            "line": 14,
+                            "column": 16,
+                            "byte": 207
+                        }
+                    }
+                }
+            }
+        },
+        "schema": {
+            "properties": {
+                "full": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar1"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar2"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "reference": {
+                    "properties": {
+                        "test": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "test"
+                    ]
+                },
+                "short": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar1"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar2"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                }
+            },
+            "type": "object",
+            "required": [
+                "full",
+                "reference",
+                "short"
+            ]
+        },
+        "executionContext": {
+            "properties": {
+                "currentEnvironment": {
+                    "value": {
+                        "name": {
+                            "value": "rotate",
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                },
+                "pulumi": {
+                    "value": {
+                        "user": {
+                            "value": {
+                                "id": {
+                                    "value": "USER_123",
+                                    "trace": {
+                                        "def": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 0,
+                                                "column": 0,
+                                                "byte": 0
+                                            },
+                                            "end": {
+                                                "line": 0,
+                                                "column": 0,
+                                                "byte": 0
+                                            }
+                                        }
+                                    }
+                                }
+                            },
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                },
+                "rootEnvironment": {
+                    "value": {
+                        "name": {
+                            "value": "rotate",
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                }
+            },
+            "schema": {
+                "properties": {
+                    "currentEnvironment": {
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "const": "rotate"
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "name"
+                        ]
+                    },
+                    "pulumi": {
+                        "properties": {
+                            "user": {
+                                "properties": {
+                                    "id": {
+                                        "type": "string",
+                                        "const": "USER_123"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "id"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "user"
+                        ]
+                    },
+                    "rootEnvironment": {
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "const": "rotate"
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "name"
+                        ]
+                    }
+                },
+                "type": "object",
+                "required": [
+                    "currentEnvironment",
+                    "pulumi",
+                    "rootEnvironment"
+                ]
+            }
+        }
+    },
+    "evalJsonRedacted": {
+        "full": {
+            "a": "bar1",
+            "b": "bar2"
+        },
+        "reference": {
+            "test": "bar1 bar2"
+        },
+        "short": {
+            "a": "bar1",
+            "b": "bar2"
+        }
+    },
+    "evalJSONRevealed": {
+        "full": {
+            "a": "bar1",
+            "b": "bar2"
+        },
+        "reference": {
+            "test": "bar1 bar2"
+        },
+        "short": {
+            "a": "bar1",
+            "b": "bar2"
+        }
+    },
+    "rotate": {
+        "exprs": {
+            "full": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 3,
+                        "column": 5,
+                        "byte": 20
+                    },
+                    "end": {
+                        "line": 8,
+                        "column": 16,
+                        "byte": 114
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar2"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar1"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "builtin": {
+                    "name": "fn::rotate",
+                    "nameRange": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 3,
+                            "column": 5,
+                            "byte": 20
+                        },
+                        "end": {
+                            "line": 3,
+                            "column": 15,
+                            "byte": 30
+                        }
+                    },
+                    "argSchema": {
+                        "properties": {
+                            "inputs": true,
+                            "provider": {
+                                "type": "string"
+                            },
+                            "state": {
+                                "properties": {
+                                    "a": {
+                                        "type": "string"
+                                    },
+                                    "b": {
+                                        "type": "string"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "a",
+                                    "b"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "inputs",
+                            "provider",
+                            "state"
+                        ]
+                    },
+                    "arg": {
+                        "range": {
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        },
+                        "object": {
+                            "inputs": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 5,
+                                        "column": 15,
+                                        "byte": 67
+                                    },
+                                    "end": {
+                                        "line": 5,
+                                        "column": 15,
+                                        "byte": 67
+                                    }
+                                },
+                                "schema": {
+                                    "type": "object"
+                                }
+                            },
+                            "provider": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 4,
+                                        "column": 17,
+                                        "byte": 48
+                                    },
+                                    "end": {
+                                        "line": 4,
+                                        "column": 21,
+                                        "byte": 52
+                                    }
+                                },
+                                "schema": {
+                                    "type": "string",
+                                    "const": "swap"
+                                },
+                                "literal": "swap"
+                            },
+                            "state": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 7,
+                                        "column": 9,
+                                        "byte": 91
+                                    },
+                                    "end": {
+                                        "line": 8,
+                                        "column": 16,
+                                        "byte": 114
+                                    }
+                                },
+                                "schema": {
+                                    "properties": {
+                                        "a": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "b": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        }
+                                    },
+                                    "type": "object",
+                                    "required": [
+                                        "a",
+                                        "b"
+                                    ]
+                                },
+                                "keyRanges": {
+                                    "a": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 7,
+                                            "column": 9,
+                                            "byte": 91
+                                        },
+                                        "end": {
+                                            "line": 7,
+                                            "column": 10,
+                                            "byte": 92
+                                        }
+                                    },
+                                    "b": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 8,
+                                            "column": 9,
+                                            "byte": 107
+                                        },
+                                        "end": {
+                                            "line": 8,
+                                            "column": 10,
+                                            "byte": 108
+                                        }
+                                    }
+                                },
+                                "object": {
+                                    "a": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 7,
+                                                "column": 12,
+                                                "byte": 94
+                                            },
+                                            "end": {
+                                                "line": 7,
+                                                "column": 16,
+                                                "byte": 98
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "literal": "bar1"
+                                    },
+                                    "b": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 8,
+                                                "column": 12,
+                                                "byte": 110
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        },
+                                        "literal": "bar2"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            "reference": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 17,
+                        "column": 5,
+                        "byte": 0
+                    },
+                    "end": {
+                        "line": 17,
+                        "column": 31,
+                        "byte": 0
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "test": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "test"
+                    ]
+                },
+                "keyRanges": {
+                    "test": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 17,
+                            "column": 5,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 17,
+                            "column": 9,
+                            "byte": 0
+                        }
+                    }
+                },
+                "object": {
+                    "test": {
+                        "range": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 17,
+                                "column": 11,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 17,
+                                "column": 31,
+                                "byte": 0
+                            }
+                        },
+                        "schema": {
+                            "type": "string"
+                        },
+                        "interpolate": [
+                            {
+                                "value": [
+                                    {
+                                        "key": "full",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 13,
+                                                "byte": 2
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 17,
+                                                "byte": 6
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 3,
+                                                "column": 5,
+                                                "byte": 20
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        }
+                                    },
+                                    {
+                                        "key": "a",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 17,
+                                                "byte": 6
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 19,
+                                                "byte": 8
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 3,
+                                                "column": 5,
+                                                "byte": 20
+                                            },
+                                            "end": {
+                                                "line": 8,
+                                                "column": 16,
+                                                "byte": 114
+                                            }
+                                        }
+                                    }
+                                ]
+                            },
+                            {
+                                "text": " ",
+                                "value": [
+                                    {
+                                        "key": "short",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 23,
+                                                "byte": 12
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 28,
+                                                "byte": 17
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 10,
+                                                "column": 5,
+                                                "byte": 128
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        }
+                                    },
+                                    {
+                                        "key": "b",
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 17,
+                                                "column": 28,
+                                                "byte": 17
+                                            },
+                                            "end": {
+                                                "line": 17,
+                                                "column": 30,
+                                                "byte": 19
+                                            }
+                                        },
+                                        "value": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 10,
+                                                "column": 5,
+                                                "byte": 128
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        }
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            },
+            "short": {
+                "range": {
+                    "environment": "rotate",
+                    "begin": {
+                        "line": 10,
+                        "column": 5,
+                        "byte": 128
+                    },
+                    "end": {
+                        "line": 14,
+                        "column": 16,
+                        "byte": 207
+                    }
+                },
+                "schema": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar2"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar1"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "builtin": {
+                    "name": "fn::rotate::swap",
+                    "nameRange": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 10,
+                            "column": 5,
+                            "byte": 128
+                        },
+                        "end": {
+                            "line": 10,
+                            "column": 21,
+                            "byte": 144
+                        }
+                    },
+                    "argSchema": {
+                        "properties": {
+                            "inputs": true,
+                            "state": {
+                                "properties": {
+                                    "a": {
+                                        "type": "string"
+                                    },
+                                    "b": {
+                                        "type": "string"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "a",
+                                    "b"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "inputs",
+                            "state"
+                        ]
+                    },
+                    "arg": {
+                        "range": {
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        },
+                        "object": {
+                            "inputs": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 11,
+                                        "column": 15,
+                                        "byte": 160
+                                    },
+                                    "end": {
+                                        "line": 11,
+                                        "column": 15,
+                                        "byte": 160
+                                    }
+                                },
+                                "schema": {
+                                    "type": "object"
+                                }
+                            },
+                            "state": {
+                                "range": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 13,
+                                        "column": 9,
+                                        "byte": 184
+                                    },
+                                    "end": {
+                                        "line": 14,
+                                        "column": 16,
+                                        "byte": 207
+                                    }
+                                },
+                                "schema": {
+                                    "properties": {
+                                        "a": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "b": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        }
+                                    },
+                                    "type": "object",
+                                    "required": [
+                                        "a",
+                                        "b"
+                                    ]
+                                },
+                                "keyRanges": {
+                                    "a": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 13,
+                                            "column": 9,
+                                            "byte": 184
+                                        },
+                                        "end": {
+                                            "line": 13,
+                                            "column": 10,
+                                            "byte": 185
+                                        }
+                                    },
+                                    "b": {
+                                        "environment": "rotate",
+                                        "begin": {
+                                            "line": 14,
+                                            "column": 9,
+                                            "byte": 200
+                                        },
+                                        "end": {
+                                            "line": 14,
+                                            "column": 10,
+                                            "byte": 201
+                                        }
+                                    }
+                                },
+                                "object": {
+                                    "a": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 13,
+                                                "column": 12,
+                                                "byte": 187
+                                            },
+                                            "end": {
+                                                "line": 13,
+                                                "column": 16,
+                                                "byte": 191
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar1"
+                                        },
+                                        "literal": "bar1"
+                                    },
+                                    "b": {
+                                        "range": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 14,
+                                                "column": 12,
+                                                "byte": 203
+                                            },
+                                            "end": {
+                                                "line": 14,
+                                                "column": 16,
+                                                "byte": 207
+                                            }
+                                        },
+                                        "schema": {
+                                            "type": "string",
+                                            "const": "bar2"
+                                        },
+                                        "literal": "bar2"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "properties": {
+            "full": {
+                "value": {
+                    "a": {
+                        "value": "bar2",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 3,
+                                    "column": 5,
+                                    "byte": 20
+                                },
+                                "end": {
+                                    "line": 8,
+                                    "column": 16,
+                                    "byte": 114
+                                }
+                            }
+                        }
+                    },
+                    "b": {
+                        "value": "bar1",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 3,
+                                    "column": 5,
+                                    "byte": 20
+                                },
+                                "end": {
+                                    "line": 8,
+                                    "column": 16,
+                                    "byte": 114
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 3,
+                            "column": 5,
+                            "byte": 20
+                        },
+                        "end": {
+                            "line": 8,
+                            "column": 16,
+                            "byte": 114
+                        }
+                    }
+                }
+            },
+            "reference": {
+                "value": {
+                    "test": {
+                        "value": "bar2 bar1",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 17,
+                                    "column": 11,
+                                    "byte": 0
+                                },
+                                "end": {
+                                    "line": 17,
+                                    "column": 31,
+                                    "byte": 0
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 17,
+                            "column": 5,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 17,
+                            "column": 31,
+                            "byte": 0
+                        }
+                    }
+                }
+            },
+            "short": {
+                "value": {
+                    "a": {
+                        "value": "bar2",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 10,
+                                    "column": 5,
+                                    "byte": 128
+                                },
+                                "end": {
+                                    "line": 14,
+                                    "column": 16,
+                                    "byte": 207
+                                }
+                            }
+                        }
+                    },
+                    "b": {
+                        "value": "bar1",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 10,
+                                    "column": 5,
+                                    "byte": 128
+                                },
+                                "end": {
+                                    "line": 14,
+                                    "column": 16,
+                                    "byte": 207
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "environment": "rotate",
+                        "begin": {
+                            "line": 10,
+                            "column": 5,
+                            "byte": 128
+                        },
+                        "end": {
+                            "line": 14,
+                            "column": 16,
+                            "byte": 207
+                        }
+                    }
+                }
+            }
+        },
+        "schema": {
+            "properties": {
+                "full": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar2"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar1"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                },
+                "reference": {
+                    "properties": {
+                        "test": {
+                            "type": "string"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "test"
+                    ]
+                },
+                "short": {
+                    "properties": {
+                        "a": {
+                            "type": "string",
+                            "const": "bar2"
+                        },
+                        "b": {
+                            "type": "string",
+                            "const": "bar1"
+                        }
+                    },
+                    "type": "object",
+                    "required": [
+                        "a",
+                        "b"
+                    ]
+                }
+            },
+            "type": "object",
+            "required": [
+                "full",
+                "reference",
+                "short"
+            ]
+        },
+        "executionContext": {
+            "properties": {
+                "currentEnvironment": {
+                    "value": {
+                        "name": {
+                            "value": "rotate",
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                },
+                "pulumi": {
+                    "value": {
+                        "user": {
+                            "value": {
+                                "id": {
+                                    "value": "USER_123",
+                                    "trace": {
+                                        "def": {
+                                            "environment": "rotate",
+                                            "begin": {
+                                                "line": 0,
+                                                "column": 0,
+                                                "byte": 0
+                                            },
+                                            "end": {
+                                                "line": 0,
+                                                "column": 0,
+                                                "byte": 0
+                                            }
+                                        }
+                                    }
+                                }
+                            },
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                },
+                "rootEnvironment": {
+                    "value": {
+                        "name": {
+                            "value": "rotate",
+                            "trace": {
+                                "def": {
+                                    "environment": "rotate",
+                                    "begin": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    },
+                                    "end": {
+                                        "line": 0,
+                                        "column": 0,
+                                        "byte": 0
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "trace": {
+                        "def": {
+                            "environment": "rotate",
+                            "begin": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            },
+                            "end": {
+                                "line": 0,
+                                "column": 0,
+                                "byte": 0
+                            }
+                        }
+                    }
+                }
+            },
+            "schema": {
+                "properties": {
+                    "currentEnvironment": {
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "const": "rotate"
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "name"
+                        ]
+                    },
+                    "pulumi": {
+                        "properties": {
+                            "user": {
+                                "properties": {
+                                    "id": {
+                                        "type": "string",
+                                        "const": "USER_123"
+                                    }
+                                },
+                                "type": "object",
+                                "required": [
+                                    "id"
+                                ]
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "user"
+                        ]
+                    },
+                    "rootEnvironment": {
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "const": "rotate"
+                            }
+                        },
+                        "type": "object",
+                        "required": [
+                            "name"
+                        ]
+                    }
+                },
+                "type": "object",
+                "required": [
+                    "currentEnvironment",
+                    "pulumi",
+                    "rootEnvironment"
+                ]
+            }
+        }
+    },
+    "rotateJson": {
+        "full": {
+            "a": "bar2",
+            "b": "bar1"
+        },
+        "reference": {
+            "test": "bar2 bar1"
+        },
+        "short": {
+            "a": "bar2",
+            "b": "bar1"
+        }
+    },
+    "rotatePatches": [
+        {
+            "DocPath": "full[\"fn::rotate\"].state",
+            "Replacement": {
+                "value": {
+                    "a": {
+                        "value": "bar2",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 8,
+                                    "column": 12,
+                                    "byte": 110
+                                },
+                                "end": {
+                                    "line": 8,
+                                    "column": 16,
+                                    "byte": 114
+                                }
+                            }
+                        }
+                    },
+                    "b": {
+                        "value": "bar1",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 7,
+                                    "column": 12,
+                                    "byte": 94
+                                },
+                                "end": {
+                                    "line": 7,
+                                    "column": 16,
+                                    "byte": 98
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "begin": {
+                            "line": 0,
+                            "column": 0,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 0,
+                            "column": 0,
+                            "byte": 0
+                        }
+                    }
+                }
+            }
+        },
+        {
+            "DocPath": "short[\"fn::rotate::swap\"].state",
+            "Replacement": {
+                "value": {
+                    "a": {
+                        "value": "bar2",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 14,
+                                    "column": 12,
+                                    "byte": 203
+                                },
+                                "end": {
+                                    "line": 14,
+                                    "column": 16,
+                                    "byte": 207
+                                }
+                            }
+                        }
+                    },
+                    "b": {
+                        "value": "bar1",
+                        "trace": {
+                            "def": {
+                                "environment": "rotate",
+                                "begin": {
+                                    "line": 13,
+                                    "column": 12,
+                                    "byte": 187
+                                },
+                                "end": {
+                                    "line": 13,
+                                    "column": 16,
+                                    "byte": 191
+                                }
+                            }
+                        }
+                    }
+                },
+                "trace": {
+                    "def": {
+                        "begin": {
+                            "line": 0,
+                            "column": 0,
+                            "byte": 0
+                        },
+                        "end": {
+                            "line": 0,
+                            "column": 0,
+                            "byte": 0
+                        }
+                    }
+                }
+            }
+        }
+    ]
+}
diff --git a/eval/testdata/eval/rotate/overrides.json b/eval/testdata/eval/rotate/overrides.json
new file mode 100644
index 0000000..23e284f
--- /dev/null
+++ b/eval/testdata/eval/rotate/overrides.json
@@ -0,0 +1,1 @@
+{"rotate": true}