From 07bdb0c2abb4e835e5b3e1698340d8e887d1b62f Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Fri, 28 Aug 2026 11:16:53 -0500 Subject: [PATCH 1/2] feat(builtins): add array.flatten Implements array.flatten, which performs one level of nesting removal on an array: nested arrays at the top level are inlined, while deeper nesting is left intact (matching OPA semantics). - Register array.flatten with arity 1 in builtins::arrays - Iterate input array; spread nested arrays one level, copy scalars as-is - Call enforce_limit() on each element to bound memory growth from adversarial inputs - Add YAML regression test covering: shallow, mixed-depth, empty, all-scalars, undefined element propagation, wrong type, and arg count Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/builtins.md | 1 + src/builtins/arrays.rs | 25 ++++++- .../cases/builtins/arrays/flatten.yaml | 67 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/interpreter/cases/builtins/arrays/flatten.yaml diff --git a/docs/builtins.md b/docs/builtins.md index dddf124b9..e5f1fd81b 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -52,6 +52,7 @@ In future, each builtin will be associated with a feature (many builtins could b | Builtin | Feature | |-----------------------------------------------------------------------------------------------------------|---------| | [array.concat](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayconcat) | _ | + | [array.flatten](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayflatten) | _ | | [array.reverse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayreverse) | _ | | [array.slice](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayslice) | _ | diff --git a/src/builtins/arrays.rs b/src/builtins/arrays.rs index 3245f2c87..d0f9dfbea 100644 --- a/src/builtins/arrays.rs +++ b/src/builtins/arrays.rs @@ -4,15 +4,17 @@ use crate::ast::{Expr, Ref}; use crate::builtins; -use crate::builtins::utils::{ensure_args_count, ensure_array, ensure_numeric}; +use crate::builtins::utils::{enforce_limit, ensure_args_count, ensure_array, ensure_numeric}; use crate::lexer::Span; use crate::Rc; use crate::Value; +use crate::Vec; use anyhow::Result; pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) { m.insert("array.concat", (concat, 2)); + m.insert("array.flatten", (flatten, 1)); m.insert("array.reverse", (reverse, 1)); m.insert("array.slice", (slice, 3)); } @@ -68,3 +70,24 @@ fn slice(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re let slice = array.as_slice().get(start..stop).unwrap_or_default(); Ok(Value::from(slice.to_vec())) } + +fn flatten(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "array.flatten"; + ensure_args_count(span, name, params, args, 1)?; + let array = ensure_array(name, ¶ms[0], args[0].clone())?; + let mut flattened = Vec::new(); + + for value in array.iter() { + if let Value::Array(nested) = value { + for nested_value in nested.iter() { + flattened.push(nested_value.clone()); + enforce_limit()?; + } + } else { + flattened.push(value.clone()); + enforce_limit()?; + } + } + + Ok(Value::from_array(flattened)) +} diff --git a/tests/interpreter/cases/builtins/arrays/flatten.yaml b/tests/interpreter/cases/builtins/arrays/flatten.yaml new file mode 100644 index 000000000..c22cdb102 --- /dev/null +++ b/tests/interpreter/cases/builtins/arrays/flatten.yaml @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: shallow + data: {} + modules: + - | + package test + x = array.flatten([[1, 2], [3, 4]]) + query: data.test.x + want_result: [1, 2, 3, 4] + + - note: mixed-depth + data: {} + modules: + - | + package test + x = array.flatten([[1, [2, 3]], 4, [5]]) + query: data.test.x + want_result: [1, [2, 3], 4, 5] + + - note: empty + data: {} + modules: + - | + package test + x = array.flatten([]) + query: data.test.x + want_result: [] + + - note: all-scalars + data: {} + modules: + - | + package test + x = array.flatten([1, 2, 3]) + query: data.test.x + want_result: [1, 2, 3] + + - note: undefined-element-propagates + data: {} + modules: + - | + package test + r { false } + x = array.flatten([r]) + query: data.test.x + no_result: true + + - note: wrong-type + data: {} + modules: + - | + package test + x = array.flatten("not-an-array") + query: data.test.x + error: "`array.flatten` expects array argument." + + - note: too-many-args + data: {} + modules: + - | + package test + x = array.flatten([1], [2]) + query: data.test.x + error: "`array.flatten` expects 1 argument" From 7457c2f7d7e6705326d4e961d7dc2f134cc2e659 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Fri, 28 Aug 2026 16:07:00 -0500 Subject: [PATCH 2/2] fix(builtins): resolve clippy pattern_type_mismatch and add RVM flatten coverage - Silence the pattern_type_mismatch/needless_borrowed_reference clippy conflict in array.flatten by matching on an explicit &Value::Array(ref nested) pattern, mirroring the existing convention in template_functions_collection.rs. This fixes the failing rust-clippy CI check (cargo xtask clippy, pinned to rustc 1.92.0). - Add tests/rvm/rego/cases/array_flatten.yaml, mirroring the interpreter fixture, to close the RVM coverage gap flagged in review. Type-check and arity 'bail!' errors are asserted as Undefined with allow_interpreter_incorrect_behavior: true, documenting RVM's existing non-strict builtin-error default (interpreter defaults to strict) rather than papering over the divergence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/builtins/arrays.rs | 6 +- tests/rvm/rego/cases/array_flatten.yaml | 84 +++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/rvm/rego/cases/array_flatten.yaml diff --git a/src/builtins/arrays.rs b/src/builtins/arrays.rs index d0f9dfbea..a3ca58538 100644 --- a/src/builtins/arrays.rs +++ b/src/builtins/arrays.rs @@ -78,7 +78,11 @@ fn flatten(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> let mut flattened = Vec::new(); for value in array.iter() { - if let Value::Array(nested) = value { + // `pattern_type_mismatch` requires the explicit `&`/`ref` here, which in + // turn triggers `needless_borrowed_reference`; the two lints conflict for + // this shape, so silence the latter (see template_functions_collection.rs). + #[allow(clippy::needless_borrowed_reference)] + if let &Value::Array(ref nested) = value { for nested_value in nested.iter() { flattened.push(nested_value.clone()); enforce_limit()?; diff --git a/tests/rvm/rego/cases/array_flatten.yaml b/tests/rvm/rego/cases/array_flatten.yaml new file mode 100644 index 000000000..636e4a9fe --- /dev/null +++ b/tests/rvm/rego/cases/array_flatten.yaml @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# RVM coverage for `array.flatten`, mirroring +# tests/interpreter/cases/builtins/arrays/flatten.yaml so the interpreter and +# RVM execution paths stay in parity for this builtin. + +cases: + - note: shallow + data: {} + modules: + - | + package test + x = array.flatten([[1, 2], [3, 4]]) + query: data.test.x + want_result: [1, 2, 3, 4] + + - note: mixed-depth + data: {} + modules: + - | + package test + x = array.flatten([[1, [2, 3]], 4, [5]]) + query: data.test.x + want_result: [1, [2, 3], 4, 5] + + - note: empty + data: {} + modules: + - | + package test + x = array.flatten([]) + query: data.test.x + want_result: [] + + - note: all-scalars + data: {} + modules: + - | + package test + x = array.flatten([1, 2, 3]) + query: data.test.x + want_result: [1, 2, 3] + + - note: undefined-element-propagates + data: {} + modules: + - | + package test + r if { false } + x = array.flatten([r]) + query: data.test.x + want_result: "#undefined" + + - note: wrong-type + data: {} + modules: + - | + package test + x = array.flatten("not-an-array") + query: data.test.x + want_result: "#undefined" + # RVM defaults `strict_builtin_errors` to false (the interpreter defaults + # to true), so the type-check `bail!` raised by `ensure_array` is + # swallowed to Undefined here rather than surfacing as an error. This is + # a pre-existing, general divergence in default settings that applies to + # every builtin's error path, not something specific to `array.flatten`. + allow_interpreter_incorrect_behavior: true + + - note: too-many-args + data: {} + modules: + - | + package test + x = array.flatten([1], [2], [3]) + query: data.test.x + want_result: "#undefined" + # Same non-strict-builtin-errors divergence as `wrong-type` above: the + # `ensure_args_count` arity-check `bail!` is swallowed to Undefined under + # RVM's default settings instead of surfacing as an error. (Two args are + # deliberately avoided here because `array.flatten(a, b)` with exactly + # one extra argument is valid Rego out-param call syntax equivalent to + # `b := array.flatten(a)`; three args unambiguously exceeds that.) + allow_interpreter_incorrect_behavior: true