Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func Check(ctx context.Context, spec string, data []byte) (res Result) {
// Last, and after deterministic: this one permutes the input, so a compiler
// that is not even same-input-same-output should be reported as that rather
// than as order-dependence.
if detail, ok := orderInvariant(ctx, spec, data, doc); !ok {
if detail, ok := orderInvariant(ctx, spec, data); !ok {
return Result{Spec: spec, Outcome: OutcomeOrderDependent, Detail: detail}
}
return res
Expand Down
84 changes: 78 additions & 6 deletions internal/harness/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"sort"
"strings"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
Expand Down Expand Up @@ -41,14 +42,29 @@ const maxReverseDepth = 512
//
// And it proves order-independence only for the constructs its input contains,
// which is why it runs over the corpus rather than over one hand-written spec.
func orderInvariant(ctx context.Context, spec string, data []byte, doc *ir.Document) (string, bool) {
reversed, ok := reverseMappings(data)
//
// Both arms are compiled from the encoder's output rather than one from the
// source and one from the rewrite. The encoder does not preserve every spelling:
// a flow-style implicit null comes back with an empty string for its value, so
// comparing against the source would read that rewriting as a lowering that
// depends on order. Passing both sides through it leaves declaration order as
// the only difference, which is the question being asked.
func orderInvariant(ctx context.Context, spec string, data []byte) (string, bool) {
baseline, ok := reencodeMappings(data)
if !ok {
return "", true // not a document whose permutation is meaning-preserving
}
if bytes.Equal(reversed, data) {
reversed, ok := reverseMappings(data)
if !ok {
return "", true // ditto
}
if bytes.Equal(reversed, baseline) {
return "", true // nothing to permute; the oracle has no question to ask
}
doc, _, err := compile(ctx, spec, baseline)
if err != nil || doc == nil {
return "", true // the re-encoding is not this source, so it asks nothing about it
}
other, otherDiags, err := compile(ctx, spec, reversed)
if err != nil {
return "recompile permuted: " + err.Error(), false
Expand Down Expand Up @@ -109,16 +125,46 @@ func diffOrderInvariants(first, second *ir.Document, secondDiags []ir.Diagnostic
// reverse — which is invariant #7's source ordering reaching the message rather
// than a lowering that depends on order. Severity, code and pointer identify the
// finding without that.
//
// A pointer spelled line:col is excluded for the same reason. Provenance.Pointer
// admits either a structural pointer or a source position, and a permutation
// moves a construct to a different line by design. Replacing it rather than
// dropping the field keeps the finding in the multiset, so a permutation that
// changes how many were reported still shows.
func diagnosticSet(diags []ir.Diagnostic) []string {
out := make([]string, 0, len(diags))
for _, d := range diags {
pointer := d.Provenance.Pointer
if isSourcePosition(pointer) {
pointer = "\x01position"
}
out = append(out, fmt.Sprintf("%s\x00%s\x00%s\x00%d",
d.Severity, d.Code, d.Provenance.Pointer, d.Provenance.Source))
d.Severity, d.Code, pointer, d.Provenance.Source))
}
sort.Strings(out)
return out
}

// isSourcePosition reports whether a provenance pointer is a line:col position
// rather than a structural pointer.
func isSourcePosition(pointer string) bool {
line, col, ok := strings.Cut(pointer, ":")
return ok && isDigits(line) && isDigits(col)
}

// isDigits reports whether s is a non-empty run of ASCII digits.
func isDigits(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}

// sourceOrderedCollections orders the collections a mapping's entry order
// decides, so comparing two permutations of one document does not report their
// own permutation.
Expand All @@ -137,10 +183,32 @@ func renderExample(e ir.Example) string {
return fmt.Sprintf("%s\x00%s\x00%v", e.Name, e.ExternalURL, e.Value)
}

// reencodeMappings returns src parsed and re-encoded with its entry order
// intact: the same normalization reverseMappings applies, minus the permutation.
// It is what the permuted source is compared against, so a spelling the encoder
// rewrites changes both sides alike.
func reencodeMappings(src []byte) ([]byte, bool) {
var root yaml.Node
if err := yaml.Unmarshal(src, &root); err != nil {
return nil, false
}
out, err := encodeYAML(&root)
if err != nil {
return nil, false
}
return out, true
}

// reverseMappings returns src with the entry order of every YAML mapping
// reversed, and ok=false for a source the rewrite cannot faithfully permute: one
// that does not parse, one that will not re-encode, or one carrying a duplicate
// mapping key, whose meaning depends on the order being changed.
// that does not parse, one that will not re-encode, one carrying a duplicate
// mapping key, whose meaning depends on the order being changed, or one whose
// permutation no longer parses.
//
// That last case is the rewrite's own doing rather than a fact about the
// compiler: reversing a mapping can carry an alias above the anchor it names,
// which YAML forbids. Re-parsing catches it without enumerating it, and covers
// any later ordering rule of the same kind.
func reverseMappings(src []byte) ([]byte, bool) {
var root yaml.Node
if err := yaml.Unmarshal(src, &root); err != nil {
Expand All @@ -153,6 +221,10 @@ func reverseMappings(src []byte) ([]byte, bool) {
if err != nil {
return nil, false
}
var check yaml.Node
if err := yaml.Unmarshal(out, &check); err != nil {
return nil, false
}
return out, true
}

Expand Down
Loading
Loading