Skip to content

Commit

Permalink
internal/core/compile: add matchIf builtin
Browse files Browse the repository at this point in the history
This primitive will make it significantly easier to implement JSON
Schema's `if`, `then`, `else` keywords. It follows a discussion with
Marcel where it became clear that implementing these keywords with
comprehensions would be tricky, and that a builtin along the lines of
`matchN` would be at least a reasonable interim solution.

I've left the testing deliberately light for now until we've decided
that this is actually the correct approach. The implementation is
largely boilerplated from that of `matchN`.

Signed-off-by: Roger Peppe <[email protected]>
Change-Id: Id74e40369bf16c7a3d011545890f0a47505b26cb
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1200942
Reviewed-by: Daniel Martí <[email protected]>
Reviewed-by: Marcel van Lohuizen <[email protected]>
TryBot-Result: CUEcueckoo <[email protected]>
Unity-Result: CUE porcuepine <[email protected]>
  • Loading branch information
rogpeppe committed Sep 10, 2024
1 parent 7a3260c commit 276d940
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 0 deletions.
282 changes: 282 additions & 0 deletions cue/testdata/builtins/matchif.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
-- in.cue --
regularFields: {
[_]: matchIf({x!: >2}, {y!: 5}, {y!: 1})
ok1: {x: 10, y: 5}
ok2: {x: 11, y: 5}
ok3: {x: 2, y: 1}
ok4: {x: 1, y: 1}
err1: {x: 10, y: 6}
err2: {x: 11, y: 6}
err3: {x: 2, y: 5}
err4: {x: 1, y: 2}
}
-- out/eval/stats --
Leaks: 24
Freed: 74
Reused: 69
Allocs: 29
Retain: 24

Unifications: 98
Conjuncts: 154
Disjuncts: 98
-- diff/-out/evalalpha<==>+out/eval --
diff old new
--- old
+++ new
@@ -2,22 +2,18 @@
regularFields.err1: invalid value {x:10,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
./in.cue:2:7
./in.cue:2:30
- ./in.cue:7:8
./in.cue:7:19
regularFields.err2: invalid value {x:11,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
./in.cue:2:7
./in.cue:2:30
- ./in.cue:8:8
./in.cue:8:19
regularFields.err3: invalid value {x:2,y:5} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 5:
./in.cue:2:7
./in.cue:2:39
- ./in.cue:9:8
./in.cue:9:18
regularFields.err4: invalid value {x:1,y:2} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 2:
./in.cue:2:7
./in.cue:2:39
- ./in.cue:10:8
./in.cue:10:18

Result:
@@ -45,7 +41,6 @@
// [eval] regularFields.err1: invalid value {x:10,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
// ./in.cue:2:7
// ./in.cue:2:30
- // ./in.cue:7:8
// ./in.cue:7:19
x: (int){ 10 }
y: (int){ 6 }
@@ -54,7 +49,6 @@
// [eval] regularFields.err2: invalid value {x:11,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
// ./in.cue:2:7
// ./in.cue:2:30
- // ./in.cue:8:8
// ./in.cue:8:19
x: (int){ 11 }
y: (int){ 6 }
@@ -63,7 +57,6 @@
// [eval] regularFields.err3: invalid value {x:2,y:5} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 5:
// ./in.cue:2:7
// ./in.cue:2:39
- // ./in.cue:9:8
// ./in.cue:9:18
x: (int){ 2 }
y: (int){ 5 }
@@ -72,7 +65,6 @@
// [eval] regularFields.err4: invalid value {x:1,y:2} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 2:
// ./in.cue:2:7
// ./in.cue:2:39
- // ./in.cue:10:8
// ./in.cue:10:18
x: (int){ 1 }
y: (int){ 2 }
-- out/eval --
Errors:
regularFields.err1: invalid value {x:10,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
./in.cue:2:7
./in.cue:2:30
./in.cue:7:8
./in.cue:7:19
regularFields.err2: invalid value {x:11,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
./in.cue:2:7
./in.cue:2:30
./in.cue:8:8
./in.cue:8:19
regularFields.err3: invalid value {x:2,y:5} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 5:
./in.cue:2:7
./in.cue:2:39
./in.cue:9:8
./in.cue:9:18
regularFields.err4: invalid value {x:1,y:2} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 2:
./in.cue:2:7
./in.cue:2:39
./in.cue:10:8
./in.cue:10:18

Result:
(_|_){
// [eval]
regularFields: (_|_){
// [eval]
ok1: (struct){
x: (int){ 10 }
y: (int){ 5 }
}
ok2: (struct){
x: (int){ 11 }
y: (int){ 5 }
}
ok3: (struct){
x: (int){ 2 }
y: (int){ 1 }
}
ok4: (struct){
x: (int){ 1 }
y: (int){ 1 }
}
err1: (_|_){
// [eval] regularFields.err1: invalid value {x:10,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
// ./in.cue:2:7
// ./in.cue:2:30
// ./in.cue:7:8
// ./in.cue:7:19
x: (int){ 10 }
y: (int){ 6 }
}
err2: (_|_){
// [eval] regularFields.err2: invalid value {x:11,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
// ./in.cue:2:7
// ./in.cue:2:30
// ./in.cue:8:8
// ./in.cue:8:19
x: (int){ 11 }
y: (int){ 6 }
}
err3: (_|_){
// [eval] regularFields.err3: invalid value {x:2,y:5} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 5:
// ./in.cue:2:7
// ./in.cue:2:39
// ./in.cue:9:8
// ./in.cue:9:18
x: (int){ 2 }
y: (int){ 5 }
}
err4: (_|_){
// [eval] regularFields.err4: invalid value {x:1,y:2} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 2:
// ./in.cue:2:7
// ./in.cue:2:39
// ./in.cue:10:8
// ./in.cue:10:18
x: (int){ 1 }
y: (int){ 2 }
}
}
}
-- out/evalalpha --
Errors:
regularFields.err1: invalid value {x:10,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
./in.cue:2:7
./in.cue:2:30
./in.cue:7:19
regularFields.err2: invalid value {x:11,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
./in.cue:2:7
./in.cue:2:30
./in.cue:8:19
regularFields.err3: invalid value {x:2,y:5} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 5:
./in.cue:2:7
./in.cue:2:39
./in.cue:9:18
regularFields.err4: invalid value {x:1,y:2} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 2:
./in.cue:2:7
./in.cue:2:39
./in.cue:10:18

Result:
(_|_){
// [eval]
regularFields: (_|_){
// [eval]
ok1: (struct){
x: (int){ 10 }
y: (int){ 5 }
}
ok2: (struct){
x: (int){ 11 }
y: (int){ 5 }
}
ok3: (struct){
x: (int){ 2 }
y: (int){ 1 }
}
ok4: (struct){
x: (int){ 1 }
y: (int){ 1 }
}
err1: (_|_){
// [eval] regularFields.err1: invalid value {x:10,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
// ./in.cue:2:7
// ./in.cue:2:30
// ./in.cue:7:19
x: (int){ 10 }
y: (int){ 6 }
}
err2: (_|_){
// [eval] regularFields.err2: invalid value {x:11,y:6} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 5 and 6:
// ./in.cue:2:7
// ./in.cue:2:30
// ./in.cue:8:19
x: (int){ 11 }
y: (int){ 6 }
}
err3: (_|_){
// [eval] regularFields.err3: invalid value {x:2,y:5} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 5:
// ./in.cue:2:7
// ./in.cue:2:39
// ./in.cue:9:18
x: (int){ 2 }
y: (int){ 5 }
}
err4: (_|_){
// [eval] regularFields.err4: invalid value {x:1,y:2} (does not satisfy matchIf({x!:>2}, {y!:5}, {y!:1})): conflicting values 1 and 2:
// ./in.cue:2:7
// ./in.cue:2:39
// ./in.cue:10:18
x: (int){ 1 }
y: (int){ 2 }
}
}
}
-- out/compile --
--- in.cue
{
regularFields: {
[_]: matchIf({
x!: >2
}, {
y!: 5
}, {
y!: 1
})
ok1: {
x: 10
y: 5
}
ok2: {
x: 11
y: 5
}
ok3: {
x: 2
y: 1
}
ok4: {
x: 1
y: 1
}
err1: {
x: 10
y: 6
}
err2: {
x: 11
y: 6
}
err3: {
x: 2
y: 5
}
err4: {
x: 1
y: 2
}
}
}
2 changes: 2 additions & 0 deletions internal/core/compile/predeclared.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func predeclared(n *ast.Ident) adt.Expr {
return lenBuiltin
case "close", "__close":
return closeBuiltin
case "matchIf", "__matchIf":
return matchIfBuiltin
case "matchN", "__matchN":
return matchNBuiltin
case "and", "__and":
Expand Down
38 changes: 38 additions & 0 deletions internal/core/compile/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,44 @@ var matchNBuiltin = &adt.Builtin{
},
}

// matchIf is a validator that checks that if the first argument unifies with
// self, the second argument also unifies with self, otherwise the third
// argument unifies with self.
// The same finalization heuristics are applied to self as are applied
// in matchN.
var matchIfBuiltin = &adt.Builtin{
Name: "matchIf",
Params: []adt.Param{topParam, topParam, topParam, topParam},
Result: adt.BoolKind,
NonConcrete: true,
Func: func(c *adt.OpContext, args []adt.Value) adt.Expr {
if !c.IsValidator {
return c.NewErrf("matchIf is a validator and should not be used as a function")
}

self := finalizeSelf(c, args[0])
if err := bottom(c, self); err != nil {
return &adt.Bool{B: false}
}
ifSchema, thenSchema, elseSchema := args[1], args[2], args[3]
v := unifyValidator(c, self, ifSchema)
var chosenSchema adt.Value
if err := validate.Validate(c, v, finalCfg); err == nil {
chosenSchema = thenSchema
} else {
chosenSchema = elseSchema
}
v = unifyValidator(c, self, chosenSchema)
err := validate.Validate(c, v, finalCfg)
if err == nil {
return &adt.Bool{B: true}
}
// TODO should we also include in the error something about the fact that
// the if condition passed or failed?
return err
},
}

var finalCfg = &validate.Config{Final: true}

// finalizeSelf ensures a value is fully evaluated and then strips it of any
Expand Down

0 comments on commit 276d940

Please sign in to comment.