Skip to content

Commit

Permalink
internal/core/export: sort conjuncts in binary expressions
Browse files Browse the repository at this point in the history
This is to help reduce diffs in Unity.

It sorts both the values in adt.Conjunction as well as
adt.BinaryExpr. In the latter case, it first flattens
the three, then sorts, then rebuilds the binary tree.

Signed-off-by: Marcel van Lohuizen <[email protected]>
Change-Id: I9fa6b91816a15c3a6cfa78c25b34851bd791011e
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1205862
Reviewed-by: Daniel Martí <[email protected]>
TryBot-Result: CUEcueckoo <[email protected]>
  • Loading branch information
mpvl committed Dec 20, 2024
1 parent f2775f8 commit 6aa0277
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 74 deletions.
2 changes: 1 addition & 1 deletion cmd/cue/cmd/testdata/script/def_jsonschema.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import "strings"
lastName?: strings.MinRunes(1)

// Age in years which must be equal to or greater than zero.
age?: >=0 & int
age?: int & >=0
...
}
-- schema.json --
Expand Down
99 changes: 99 additions & 0 deletions internal/core/export/adt.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package export

import (
"bytes"
"cmp"
"fmt"
"slices"
"strings"

"cuelang.org/go/cue/ast"
Expand Down Expand Up @@ -230,6 +232,9 @@ func (e *exporter) adt(env *adt.Environment, expr adt.Elem) ast.Expr {
}

case *adt.BinaryExpr:
if x.Op == adt.AndOp || x.Op == adt.OrOp {
return e.sortBinaryTree(env, x)
}
return &ast.BinaryExpr{
Op: x.Op.Token(),
X: e.innerExpr(env, x.X),
Expand Down Expand Up @@ -301,6 +306,100 @@ func (e *exporter) adt(env *adt.Environment, expr adt.Elem) ast.Expr {
}
}

// sortBinaryTree converte x to a binary tree and sorts it's elements
// using sortLeafAdt.
func (e *exporter) sortBinaryTree(env *adt.Environment, x *adt.BinaryExpr) (b ast.Expr) {
var exprs []adt.Node

var flatten func(expr adt.Expr)
flatten = func(expr adt.Expr) {
if y, ok := expr.(*adt.BinaryExpr); ok && x.Op == y.Op {
flatten(y.X)
flatten(y.Y)
} else {
exprs = append(exprs, expr)
}
}
flatten(x)

// Sort the expressions
slices.SortStableFunc(exprs, cmpLeafNodes)

nodes := make([]ast.Expr, 0, len(exprs))
for _, x := range exprs {
switch y := x.(type) {
case *adt.Top:
case *adt.BasicType:
if y.K != adt.TopKind {
nodes = append(nodes, e.expr(env, y))
}
default:
nodes = append(nodes, e.innerExpr(env, y.(adt.Expr)))
}
}

if len(nodes) == 0 {
return e.adt(env, &adt.Top{})
}

return ast.NewBinExpr(x.Op.Token(), nodes...)
}

// cmpConjuncts compares two Conjunct based on their element using cmpLeafNodes.
func cmpConjuncts(a, b adt.Conjunct) int {
return cmpLeafNodes(a.Expr(), b.Expr())
}

// cmpLeafNodes compares two adt.Expr values. The values may not be a binary
// expressions. It returns true if a is less than b.
func cmpLeafNodes[T adt.Node](a, b T) int {
if c := cmp.Compare(typeOrder(a), typeOrder(b)); c != 0 {
return c
}

srcA := a.Source()
srcB := b.Source()

if srcA == nil || srcB == nil {
// TODO: some tie breaker
return 0
}

posA := srcA.Pos()
posB := srcB.Pos()

if c := cmp.Compare(posA.Filename(), posB.Filename()); c != 0 {
return c
}

if c := cmp.Compare(posA.Offset(), posB.Offset()); c != 0 {
return c
}

return 0
}

func typeOrder(x adt.Node) int {
switch x.(type) {
case *adt.Top:
return 0
case *adt.BasicType:
return 1
case *adt.FieldReference:
return 2 // sometimes basic types are represented as field references.
case *adt.Bool, *adt.Null, *adt.Num, *adt.String, *adt.Bytes:
return 10
case *adt.BoundValue:
return 20
case *adt.StructLit, *adt.ListLit:
return 500
case adt.Expr:
return 25
default:
return 100
}
}

var dummyTop = &ast.Ident{Name: "_"}

func (e *exporter) resolve(env *adt.Environment, r adt.Resolver) ast.Expr {
Expand Down
258 changes: 258 additions & 0 deletions internal/core/export/testdata/main/conjunctsort.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
-- in.cue --
import "list"

basicFirst: {
sort: Z & {a: _x} // string first
_x: y + ""
y: string
Z: a: string
}

literalLast: {
p1: {
[string]: list.UniqueItems()
a: [_, _]
}
p2: {
a: [_, _]
[string]: list.UniqueItems()
}
}

posTieBreaker: {
p1: {
a: list.UniqueItems()
b: list.MinItems(3)
c: a & b
}
p2: {
b: list.MinItems(3)
a: list.UniqueItems()
c: a & b
}
}
-- out/definition --
import "list"

basicFirst: {
sort: Z & {
a: _x
}
_x: y + ""
y: string
Z: {
a: string
}
}
literalLast: {
p1: {
[string]: list.UniqueItems()
a: [_, _]
}
p2: {
a: [_, _]
[string]: list.UniqueItems()
}
}
posTieBreaker: {
p1: {
a: list.UniqueItems()
b: list.MinItems(3)
c: a & b
}
p2: {
b: list.MinItems(3)
a: list.UniqueItems()
c: a & b
}
}
-- out/doc --
[]
[basicFirst]
[basicFirst sort]
[basicFirst sort a]
[basicFirst _x]
[basicFirst y]
[basicFirst Z]
[basicFirst Z a]
[literalLast]
[literalLast p1]
[literalLast p1 a]
[literalLast p1 a 0]
[literalLast p1 a 1]
[literalLast p2]
[literalLast p2 a]
[literalLast p2 a 0]
[literalLast p2 a 1]
[posTieBreaker]
[posTieBreaker p1]
[posTieBreaker p1 a]
[posTieBreaker p1 b]
[posTieBreaker p1 c]
[posTieBreaker p2]
[posTieBreaker p2 b]
[posTieBreaker p2 a]
[posTieBreaker p2 c]
-- out/value --
== Simplified
{
basicFirst: {
sort: {
a: string & _x
}
y: string
Z: {
a: string
}
}
literalLast: {
p1: {
a: list.UniqueItems() & [_, _]
}
p2: {
a: list.UniqueItems() & [_, _]
}
}
posTieBreaker: {
p1: {
a: list.UniqueItems()
b: list.MinItems(3)
c: list.UniqueItems() & list.MinItems(3)
}
p2: {
b: list.MinItems(3)
a: list.UniqueItems()
c: list.MinItems(3) & list.UniqueItems()
}
}
}
== Raw
{
basicFirst: {
sort: {
a: string & _x
}
_x: y + ""
y: string
Z: {
a: string
}
}
literalLast: {
p1: {
a: list.UniqueItems() & [_, _]
}
p2: {
a: list.UniqueItems() & [_, _]
}
}
posTieBreaker: {
p1: {
a: list.UniqueItems()
b: list.MinItems(3)
c: list.UniqueItems() & list.MinItems(3)
}
p2: {
b: list.MinItems(3)
a: list.UniqueItems()
c: list.MinItems(3) & list.UniqueItems()
}
}
}
== Final
{
basicFirst: {
sort: {
a: _|_ // basicFirst.sort.a: non-concrete value string in operand to +
}
y: string
Z: {
a: string
}
}
literalLast: {
p1: {
a: _|_ // literalLast.p1.a: invalid value [_,_] (does not satisfy list.UniqueItems): equal values at position 0 and 1
}
p2: {
a: _|_ // literalLast.p2.a: invalid value [_,_] (does not satisfy list.UniqueItems): equal values at position 0 and 1
}
}
posTieBreaker: {
p1: {
a: list.UniqueItems()
b: list.MinItems(3)
c: list.UniqueItems() & list.MinItems(3)
}
p2: {
b: list.MinItems(3)
a: list.UniqueItems()
c: list.MinItems(3) & list.UniqueItems()
}
}
}
== All
{
basicFirst: {
sort: {
a: string & _x
}
_x: y + ""
y: string
Z: {
a: string
}
}
literalLast: {
p1: {
a: list.UniqueItems() & [_, _]
}
p2: {
a: list.UniqueItems() & [_, _]
}
}
posTieBreaker: {
p1: {
a: list.UniqueItems()
b: list.MinItems(3)
c: list.UniqueItems() & list.MinItems(3)
}
p2: {
b: list.MinItems(3)
a: list.UniqueItems()
c: list.MinItems(3) & list.UniqueItems()
}
}
}
== Eval
{
basicFirst: {
sort: {
a: string & _x
}
y: string
Z: {
a: string
}
}
literalLast: {
p1: {
a: list.UniqueItems() & [_, _]
}
p2: {
a: list.UniqueItems() & [_, _]
}
}
posTieBreaker: {
p1: {
a: list.UniqueItems()
b: list.MinItems(3)
c: list.UniqueItems() & list.MinItems(3)
}
p2: {
b: list.MinItems(3)
a: list.UniqueItems()
c: list.MinItems(3) & list.UniqueItems()
}
}
}
Loading

0 comments on commit 6aa0277

Please sign in to comment.