Skip to content

Commit

Permalink
Add array.reverse(array) and strings.reverse(string) built-in fun…
Browse files Browse the repository at this point in the history
…ctions. (open-policy-agent#4161)

The function `array.reverse` takes an array as an argument, and returns an array with a reversed order of elements.
The function `strings.reverse` takes a string as an argument, and returns a string with a reversed order of unicode code points.
WASM support is included for both built-ins.

Fixes open-policy-agent#3736

Signed-off-by: Kristian Svalland <kristian.svalland@gmail.com>
  • Loading branch information
kristiansvalland committed Dec 27, 2021
1 parent 328ffcd commit 6f81c4a
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 0 deletions.
24 changes: 24 additions & 0 deletions ast/builtins.go
Expand Up @@ -80,6 +80,7 @@ var DefaultBuiltins = [...]*Builtin{
// Arrays
ArrayConcat,
ArraySlice,
ArrayReverse,

// Conversions
ToNumber,
Expand Down Expand Up @@ -127,6 +128,7 @@ var DefaultBuiltins = [...]*Builtin{
TrimSuffix,
TrimSpace,
Sprintf,
StringReverse,

// Numbers
NumbersRange,
Expand Down Expand Up @@ -717,6 +719,17 @@ var ArraySlice = &Builtin{
),
}

// ArrayReverse returns a given array, reversed
var ArrayReverse = &Builtin{
Name: "array.reverse",
Decl: types.NewFunction(
types.Args(
types.NewArray(nil, types.A),
),
types.NewArray(nil, types.A),
),
}

/**
* Conversions
*/
Expand Down Expand Up @@ -1080,6 +1093,17 @@ var Sprintf = &Builtin{
),
}

// StringReverse returns the given string, reversed.
var StringReverse = &Builtin{
Name: "strings.reverse",
Decl: types.NewFunction(
types.Args(
types.S,
),
types.S,
),
}

/**
* Numbers
*/
Expand Down
34 changes: 34 additions & 0 deletions capabilities.json
Expand Up @@ -123,6 +123,26 @@
"type": "function"
}
},
{
"name": "array.reverse",
"decl": {
"args": [
{
"dynamic": {
"type": "any"
},
"type": "array"
}
],
"result": {
"dynamic": {
"type": "any"
},
"type": "array"
},
"type": "function"
}
},
{
"name": "array.slice",
"decl": {
Expand Down Expand Up @@ -2979,6 +2999,20 @@
"type": "function"
}
},
{
"name": "strings.reverse",
"decl": {
"args": [
{
"type": "string"
}
],
"result": {
"type": "string"
},
"type": "function"
}
},
{
"name": "substring",
"decl": {
Expand Down
2 changes: 2 additions & 0 deletions docs/content/policy-reference.md
Expand Up @@ -324,6 +324,7 @@ complex types.
| Built-in | Description | Wasm Support |
| ------- |-------------|---------------|
| <span class="opa-keep-it-together">``output := array.concat(array, array)``</span> | ``output`` is the result of concatenating the two input arrays together. ||
| <span class="opa-keep-it-together">``output := array.reverse(array)``</span> | ``output`` is the result of reversing the order of the elements in ``array``. ||
<span class="opa-keep-it-together">``output := array.slice(array, startIndex, stopIndex)``</span> | ``output`` is the part of the ``array`` from ``startIndex`` to ``stopIndex`` including the first but excluding the last. If `startIndex >= stopIndex` then `output == []`. If both `startIndex` and `stopIndex` are less than zero, `output == []`. Otherwise, `startIndex` and `stopIndex` are clamped to 0 and `count(array)` respectively. | ✅ |

### Sets
Expand Down Expand Up @@ -379,6 +380,7 @@ complex types.
| <span class="opa-keep-it-together">``output := indexof(string, search)``</span> | ``output`` is the index inside ``string`` where ``search`` first occurs, or -1 if ``search`` does not exist ||
| <span class="opa-keep-it-together">``output := lower(string)``</span> | ``output`` is ``string`` after converting to lower case ||
| <span class="opa-keep-it-together">``output := replace(string, old, new)``</span> | ``output`` is a ``string`` representing ``string`` with all instances of ``old`` replaced by ``new`` ||
| <span class="opa-keep-it-together">``output := strings.reverse(string)``</span> | ``output`` is ``string`` reversed ||
| <span class="opa-keep-it-together">``output := strings.replace_n(patterns, string)``</span> | ``patterns`` is an object with old, new string key value pairs (e.g. ``{"old1": "new1", "old2": "new2", ...}``). ``output`` is a ``string`` with all old strings inside ``patterns`` replaced by the new strings ||
| <span class="opa-keep-it-together">``output := split(string, delimiter)``</span> | ``output`` is ``array[string]`` representing elements of ``string`` separated by ``delimiter`` ||
| <span class="opa-keep-it-together">``output := sprintf(string, values)``</span> | ``output`` is a ``string`` representing ``string`` formatted by the values in the ``array`` ``values``. | ``SDK-dependent`` |
Expand Down
2 changes: 2 additions & 0 deletions internal/compiler/wasm/wasm.go
Expand Up @@ -91,6 +91,7 @@ var builtinsFunctions = map[string]string{
ast.Floor.Name: "opa_arith_floor",
ast.Rem.Name: "opa_arith_rem",
ast.ArrayConcat.Name: "opa_array_concat",
ast.ArrayReverse.Name: "opa_array_reverse",
ast.ArraySlice.Name: "opa_array_slice",
ast.SetDiff.Name: "opa_set_diff",
ast.And.Name: "opa_set_intersection",
Expand Down Expand Up @@ -149,6 +150,7 @@ var builtinsFunctions = map[string]string{
ast.Contains.Name: "opa_strings_contains",
ast.StartsWith.Name: "opa_strings_startswith",
ast.EndsWith.Name: "opa_strings_endswith",
ast.StringReverse.Name: "opa_strings_reverse",
ast.Split.Name: "opa_strings_split",
ast.Replace.Name: "opa_strings_replace",
ast.ReplaceN.Name: "opa_strings_replace_n",
Expand Down
43 changes: 43 additions & 0 deletions test/cases/testdata/array/test-array-0052.yaml
@@ -0,0 +1,43 @@
cases:
- note: 'array/reverse_123'
query: data.test.p = x
data:
foo:
- 1
- 2
- 3
modules:
- |
package test
p := array.reverse(data.foo)
want_result:
- x:
- 3
- 2
- 1
- note: 'array/reverse_empty'
query: data.test.p = x
data:
foo: []
modules:
- |
package test
p := array.reverse(data.foo)
want_result:
- x: []
- note: 'array/reverse_object_error'
query: data.test.p = x
data:
foo:
bar: baz
baz: bar
modules:
- |
package test
p := array.reverse(data.foo)
want_error: "array.reverse: operand 1 must be array but got object"
want_error_code: eval_type_error
strict_error: true
73 changes: 73 additions & 0 deletions test/cases/testdata/strings/test-strings-0924.yaml
@@ -0,0 +1,73 @@
cases:
- note: 'strings/reverse_bar'
query: data.test.p = x
data:
foo: "bar"
modules:
- |
package test
p := strings.reverse(data.foo)
want_result:
- x: "rab"
- note: 'strings/reverse_unicode_multi_char_emojii'
query: data.test.p = x
data:
# The "Keycap Digit Two" consists of three codepoints: [2 U+32, U+FE0F, U+20E3]
# Other examples of such emojies are smileys with defined skin-color
# In the current implementation, we reverse strings at the level of codepoints, not Glyphs
foo: "2️⃣"
modules:
- |
package test
p := strings.reverse(data.foo)
want_result:
- x: "⃣️2"
- note: 'strings/reverse_unicode'
query: data.test.p = x
data:
foo: "1😀𝛾"
modules:
- |
package test
p := strings.reverse(data.foo)
want_result:
- x: "𝛾😀1"
- note: 'strings/reverse_empty'
query: data.test.p = x
data:
foo: ""
modules:
- |
package test
p := strings.reverse(data.foo)
want_result:
- x: ""
- note: 'strings/reverse_number_error'
query: data.test.p = x
data:
foo: 123
modules:
- |
package test
p := strings.reverse(data.foo)
want_error: "reverse: operand 1 must be string but got number"
want_error_code: eval_type_error
strict_error: true
- note: 'strings/reverse_object_error'
query: data.test.p = x
data:
foo:
bar: baz
modules:
- |
package test
p := strings.reverse(data.foo)
want_error: "reverse: operand 1 must be string but got object"
want_error_code: eval_type_error
strict_error: true
17 changes: 17 additions & 0 deletions topdown/array.go
Expand Up @@ -71,7 +71,24 @@ func builtinArraySlice(a, i, j ast.Value) (ast.Value, error) {
return arr.Slice(startIndex, stopIndex), nil
}

func builtinArrayReverse(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
arr, err := builtins.ArrayOperand(operands[0].Value, 1)
if err != nil {
return err
}

length := arr.Len()
reversedArr := make([]*ast.Term, length)

for index := 0; index < length; index++ {
reversedArr[index] = arr.Elem(length - index - 1)
}

return iter(ast.ArrayTerm(reversedArr...))
}

func init() {
RegisterFunctionalBuiltin2(ast.ArrayConcat.Name, builtinArrayConcat)
RegisterFunctionalBuiltin3(ast.ArraySlice.Name, builtinArraySlice)
RegisterBuiltinFunc(ast.ArrayReverse.Name, builtinArrayReverse)
}
20 changes: 20 additions & 0 deletions topdown/strings.go
Expand Up @@ -412,6 +412,25 @@ func builtinSprintf(a, b ast.Value) (ast.Value, error) {
return ast.String(fmt.Sprintf(string(s), args...)), nil
}

func builtinReverse(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
s, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
return err
}

sRunes := []rune(string(s))
length := len(sRunes)
reversedRunes := make([]rune, length)

for index, r := range sRunes {
reversedRunes[length-index-1] = r
}

reversedString := string(reversedRunes)

return iter(ast.StringTerm(reversedString))
}

func init() {
RegisterFunctionalBuiltin2(ast.FormatInt.Name, builtinFormatInt)
RegisterFunctionalBuiltin2(ast.Concat.Name, builtinConcat)
Expand All @@ -432,4 +451,5 @@ func init() {
RegisterFunctionalBuiltin2(ast.TrimSuffix.Name, builtinTrimSuffix)
RegisterFunctionalBuiltin1(ast.TrimSpace.Name, builtinTrimSpace)
RegisterFunctionalBuiltin2(ast.Sprintf.Name, builtinSprintf)
RegisterBuiltinFunc(ast.StringReverse.Name, builtinReverse)
}
21 changes: 21 additions & 0 deletions wasm/src/array.c
Expand Up @@ -66,3 +66,24 @@ opa_value *opa_array_slice(opa_value *a, opa_value *i, opa_value *j)

return &r->hdr;
}

OPA_BUILTIN
opa_value *opa_array_reverse(opa_value *a)
{
if (opa_value_type(a) != OPA_ARRAY)
{
return NULL;
}

opa_array_t *arr = opa_cast_array(a);

opa_array_t *reversed = opa_cast_array(opa_array_with_cap(arr->len));

int n = arr->len;

for (int i = 0; i < n; i++) {
opa_array_append(reversed, arr->elems[n - 1 - i].v);
}

return &reversed->hdr;
}
1 change: 1 addition & 0 deletions wasm/src/array.h
Expand Up @@ -3,5 +3,6 @@

opa_value *opa_array_concat(opa_value *a, opa_value *b);
opa_value *opa_array_slice(opa_value *a, opa_value *i, opa_value *j);
opa_value *opa_array_reverse(opa_value *a);

#endif
27 changes: 27 additions & 0 deletions wasm/src/strings.c
Expand Up @@ -351,6 +351,33 @@ opa_value *opa_strings_replace_n(opa_value *a, opa_value *b)
return result;
}

OPA_BUILTIN
opa_value *opa_strings_reverse(opa_value *a)
{
if (opa_value_type(a) != OPA_STRING)
{
return NULL;
}

opa_string_t *s = opa_cast_string(a);

char *reversed = opa_malloc(s->len + 1);

for (int i = 0; i < s->len; )
{
int len = 0;
if (opa_unicode_decode_utf8(s->v, i, s->len, &len) == -1)
{
opa_abort("string: invalid unicode");
}
memcpy(&reversed[s->len - i - len], &s->v[i], len);
i += len;
}
reversed[s->len] = '\0';

return opa_string_allocated(reversed, s->len);
}

OPA_BUILTIN
opa_value *opa_strings_split(opa_value *a, opa_value *b)
{
Expand Down
1 change: 1 addition & 0 deletions wasm/src/strings.h
Expand Up @@ -11,6 +11,7 @@ opa_value *opa_strings_indexof(opa_value *a, opa_value *b);
opa_value *opa_strings_lower(opa_value *a);
opa_value *opa_strings_replace(opa_value *a, opa_value *b, opa_value *c);
opa_value *opa_strings_replace_n(opa_value *a, opa_value *b);
opa_value *opa_strings_reverse(opa_value *a);
opa_value *opa_strings_split(opa_value *a, opa_value *b);
opa_value *opa_strings_startswith(opa_value *a, opa_value *b);
opa_value *opa_strings_substring(opa_value *a, opa_value *b, opa_value *c);
Expand Down

0 comments on commit 6f81c4a

Please sign in to comment.