From 276d9408289b9afe6b9ba34f3292a8926fb94349 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Tue, 10 Sep 2024 12:43:11 +0100 Subject: [PATCH] internal/core/compile: add `matchIf` builtin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Change-Id: Id74e40369bf16c7a3d011545890f0a47505b26cb Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1200942 Reviewed-by: Daniel Martí Reviewed-by: Marcel van Lohuizen TryBot-Result: CUEcueckoo Unity-Result: CUE porcuepine --- cue/testdata/builtins/matchif.txtar | 282 +++++++++++++++++++++++++++ internal/core/compile/predeclared.go | 2 + internal/core/compile/validator.go | 38 ++++ 3 files changed, 322 insertions(+) create mode 100644 cue/testdata/builtins/matchif.txtar diff --git a/cue/testdata/builtins/matchif.txtar b/cue/testdata/builtins/matchif.txtar new file mode 100644 index 00000000000..964cab4057b --- /dev/null +++ b/cue/testdata/builtins/matchif.txtar @@ -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 + } + } +} diff --git a/internal/core/compile/predeclared.go b/internal/core/compile/predeclared.go index dd89bf3a5c8..5131eb2096b 100644 --- a/internal/core/compile/predeclared.go +++ b/internal/core/compile/predeclared.go @@ -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": diff --git a/internal/core/compile/validator.go b/internal/core/compile/validator.go index 96408f6c33f..de3633e7d03 100644 --- a/internal/core/compile/validator.go +++ b/internal/core/compile/validator.go @@ -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