@@ -795,6 +811,16 @@ namespace Regorus.Internal
public UIntPtr max_lines;
}
+ ///
+ /// FFI representation of the cache configuration.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct RegorusCacheConfig
+ {
+ public UIntPtr regex;
+ public UIntPtr glob;
+ }
+
///
/// Byte buffer returned from FFI.
///
diff --git a/bindings/csharp/TestApp/Program.cs b/bindings/csharp/TestApp/Program.cs
index 1cfb42f..ebd0b47 100644
--- a/bindings/csharp/TestApp/Program.cs
+++ b/bindings/csharp/TestApp/Program.cs
@@ -18,6 +18,9 @@ var w = new Stopwatch();
w.Restart();
+// Configure the global pattern caches.
+Regorus.Engine.SetCacheConfig(new Regorus.CacheConfig(regex: 256, glob: 128));
+
var engine = new Regorus.Engine();
engine.SetRegoV0(true);
// Raise the default col limit to 2000
diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock
index dec168c..698a6bc 100644
--- a/bindings/ffi/Cargo.lock
+++ b/bindings/ffi/Cargo.lock
@@ -657,6 +657,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+
[[package]]
name = "memchr"
version = "2.7.6"
@@ -985,9 +991,11 @@ dependencies = [
"ipnet",
"jsonschema",
"lazy_static",
+ "lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
+ "parking_lot",
"postcard",
"rand",
"regex",
diff --git a/bindings/ffi/Cargo.toml b/bindings/ffi/Cargo.toml
index a787910..f20c3f7 100644
--- a/bindings/ffi/Cargo.toml
+++ b/bindings/ffi/Cargo.toml
@@ -35,6 +35,7 @@ default = [
"rbac",
"regorus/arc",
"regorus/full-opa",
+ "cache",
"contention_checks",
]
ast = ["regorus/ast"]
@@ -45,6 +46,7 @@ allocator-memory-limits = ["regorus/allocator-memory-limits"]
contention_checks = ["parking_lot"]
rvm = ["regorus/rvm"]
rbac = ["regorus/azure-rbac"]
+cache = ["regorus/cache"]
custom_allocator = []
[build-dependencies]
diff --git a/bindings/ffi/src/limits.rs b/bindings/ffi/src/limits.rs
index 18b74ff..dc98a4e 100644
--- a/bindings/ffi/src/limits.rs
+++ b/bindings/ffi/src/limits.rs
@@ -199,6 +199,40 @@ pub extern "C" fn regorus_clear_fallback_execution_timer_config() -> RegorusResu
RegorusResult::ok_void()
}
+// ---------------------------------------------------------------------------
+// Cache configuration (global)
+// ---------------------------------------------------------------------------
+
+/// FFI representation of [`regorus::cache::Config`].
+#[cfg(feature = "cache")]
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+pub struct RegorusCacheConfig {
+ /// Maximum compiled regex patterns (default 256, 0 = disabled).
+ pub regex: usize,
+ /// Maximum compiled glob matchers (default 128, 0 = disabled).
+ pub glob: usize,
+}
+
+/// Configure the global pattern caches used by `regex.*` and `glob.*` builtins.
+#[cfg(feature = "cache")]
+#[no_mangle]
+pub extern "C" fn regorus_set_cache_config(config: RegorusCacheConfig) -> RegorusResult {
+ regorus::cache::configure(regorus::cache::Config {
+ regex: config.regex,
+ glob: config.glob,
+ });
+ RegorusResult::ok_void()
+}
+
+/// Clear all entries from every pattern cache.
+#[cfg(feature = "cache")]
+#[no_mangle]
+pub extern "C" fn regorus_clear_cache() -> RegorusResult {
+ regorus::cache::clear();
+ RegorusResult::ok_void()
+}
+
#[cfg(test)]
mod tests {
use super::{
diff --git a/bindings/go/main.go b/bindings/go/main.go
index e59de0d..a5d4ed3 100644
--- a/bindings/go/main.go
+++ b/bindings/go/main.go
@@ -17,6 +17,12 @@ func main() {
engine := regorus.NewEngine()
defer engine.Close()
+ // Configure the global pattern caches.
+ if err = regorus.SetCacheConfig(regorus.CacheConfig{Regex: 256, Glob: 128}); err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
+
engine.SetRegoV0(true)
// Raise the default col limit to 2000
engine.SetPolicyLengthConfig(regorus.PolicyLengthConfig{MaxCol: 2000, MaxFileBytes: 1048576, MaxLines: 20000})
diff --git a/bindings/go/pkg/regorus/mod.go b/bindings/go/pkg/regorus/mod.go
index 98aaf49..042f2b3 100644
--- a/bindings/go/pkg/regorus/mod.go
+++ b/bindings/go/pkg/regorus/mod.go
@@ -243,3 +243,30 @@ func (e *Engine) ClearPolicyLengthConfig() error {
}
return nil
}
+
+type CacheConfig struct {
+ Regex uint
+ Glob uint
+}
+
+func SetCacheConfig(config CacheConfig) error {
+ c := C.RegorusCacheConfig{
+ regex: C.size_t(config.Regex),
+ glob: C.size_t(config.Glob),
+ }
+ result := C.regorus_set_cache_config(c)
+ defer C.regorus_result_drop(result)
+ if result.status != C.Ok {
+ return fmt.Errorf("%s", C.GoString(result.error_message))
+ }
+ return nil
+}
+
+func ClearCache() error {
+ result := C.regorus_clear_cache()
+ defer C.regorus_result_drop(result)
+ if result.status != C.Ok {
+ return fmt.Errorf("%s", C.GoString(result.error_message))
+ }
+ return nil
+}
diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock
index 280df93..91ae70e 100644
--- a/bindings/java/Cargo.lock
+++ b/bindings/java/Cargo.lock
@@ -539,6 +539,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+
[[package]]
name = "memchr"
version = "2.7.6"
@@ -860,9 +866,11 @@ dependencies = [
"ipnet",
"jsonschema",
"lazy_static",
+ "lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
+ "parking_lot",
"postcard",
"rand",
"regex",
diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml
index dc6d44a..0710448 100644
--- a/bindings/java/Cargo.toml
+++ b/bindings/java/Cargo.toml
@@ -14,9 +14,10 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
crate-type = ["cdylib"]
[features]
-default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
+default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa"]
coverage = ["regorus/coverage"]
ast = ["regorus/ast"]
+cache = ["regorus/cache"]
[dependencies]
anyhow = "1.0"
diff --git a/bindings/java/Test.java b/bindings/java/Test.java
index 08f2371..5f802a9 100644
--- a/bindings/java/Test.java
+++ b/bindings/java/Test.java
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+import com.microsoft.regorus.CacheConfig;
import com.microsoft.regorus.Engine;
import com.microsoft.regorus.PolicyLengthConfig;
import com.microsoft.regorus.PolicyModule;
@@ -10,6 +11,9 @@ import com.microsoft.regorus.Rvm;
public class Test {
public static void main(String[] args) {
+ // Configure the global pattern caches.
+ CacheConfig.configure(new CacheConfig(256, 128));
+
try (Engine engine = new Engine()) {
String pkg = engine.addPolicy(
"hello.rego",
diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs
index 6732c39..7f6a1a5 100644
--- a/bindings/java/src/lib.rs
+++ b/bindings/java/src/lib.rs
@@ -399,6 +399,37 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearPolicyLength
engine.clear_policy_length_config();
}
+#[cfg(feature = "cache")]
+#[no_mangle]
+pub extern "system" fn Java_com_microsoft_regorus_CacheConfig_nativeSetCacheConfig(
+ _env: JNIEnv,
+ _class: JClass,
+ regex: jlong,
+ glob: jlong,
+) {
+ regorus::cache::configure(regorus::cache::Config {
+ regex: if regex < 0 {
+ 0
+ } else {
+ usize::try_from(regex).unwrap_or(usize::MAX)
+ },
+ glob: if glob < 0 {
+ 0
+ } else {
+ usize::try_from(glob).unwrap_or(usize::MAX)
+ },
+ });
+}
+
+#[cfg(feature = "cache")]
+#[no_mangle]
+pub extern "system" fn Java_com_microsoft_regorus_CacheConfig_nativeClearCache(
+ _env: JNIEnv,
+ _class: JClass,
+) {
+ regorus::cache::clear();
+}
+
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
_env: JNIEnv,
diff --git a/bindings/java/src/main/java/com/microsoft/regorus/CacheConfig.java b/bindings/java/src/main/java/com/microsoft/regorus/CacheConfig.java
new file mode 100644
index 0000000..06354d2
--- /dev/null
+++ b/bindings/java/src/main/java/com/microsoft/regorus/CacheConfig.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ **/
+
+package com.microsoft.regorus;
+
+/**
+ * Global configuration for compiled pattern caches used by regex and glob builtins.
+ *
+ * Capacity of 0 disables the corresponding cache.
+ */
+public final class CacheConfig {
+
+ static {
+ System.loadLibrary("regorus_java");
+ }
+
+ private static native void nativeSetCacheConfig(long regex, long glob);
+ private static native void nativeClearCache();
+
+ /**
+ * Maximum cached compiled regex patterns (default 256).
+ */
+ public final long regex;
+
+ /**
+ * Maximum cached compiled glob matchers (default 128).
+ */
+ public final long glob;
+
+ /**
+ * Create a new cache configuration.
+ *
+ * @param regex Maximum cached compiled regex patterns (0 = disabled).
+ * @param glob Maximum cached compiled glob matchers (0 = disabled).
+ */
+ public CacheConfig(long regex, long glob) {
+ if (regex < 0) {
+ throw new IllegalArgumentException("regex must be non-negative");
+ }
+ if (glob < 0) {
+ throw new IllegalArgumentException("glob must be non-negative");
+ }
+ this.regex = regex;
+ this.glob = glob;
+ }
+
+ /**
+ * Apply this cache configuration globally.
+ */
+ public static void configure(CacheConfig config) {
+ nativeSetCacheConfig(config.regex, config.glob);
+ }
+
+ /**
+ * Clear all entries from every pattern cache.
+ */
+ public static void clear() {
+ nativeClearCache();
+ }
+}
diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock
index d4c819f..4e5798b 100644
--- a/bindings/python/Cargo.lock
+++ b/bindings/python/Cargo.lock
@@ -510,6 +510,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+
[[package]]
name = "memchr"
version = "2.7.6"
@@ -919,9 +925,11 @@ dependencies = [
"ipnet",
"jsonschema",
"lazy_static",
+ "lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
+ "parking_lot",
"postcard",
"rand",
"regex",
diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml
index 1d721e9..87f2c94 100644
--- a/bindings/python/Cargo.toml
+++ b/bindings/python/Cargo.toml
@@ -15,8 +15,9 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
crate-type = ["cdylib"]
[features]
-default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
+default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa"]
ast = ["regorus/ast"]
+cache = ["regorus/cache"]
coverage = ["regorus/coverage"]
[dependencies]
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index e97362b..bff5061 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -622,10 +622,33 @@ impl Rvm {
}
}
+/// Configure the global pattern caches used by `regex.*` and `glob.*` builtins.
+///
+/// * `regex`: Maximum cached compiled regex patterns (default 256, 0 = disabled).
+/// * `glob`: Maximum cached compiled glob matchers (default 128, 0 = disabled).
+#[cfg(feature = "cache")]
+#[pyfunction]
+#[pyo3(signature = (*, regex = 256, glob = 128))]
+fn set_cache_config(regex: usize, glob: usize) {
+ ::regorus::cache::configure(::regorus::cache::Config { regex, glob });
+}
+
+/// Clear all entries from every pattern cache.
+#[cfg(feature = "cache")]
+#[pyfunction]
+fn clear_cache() {
+ ::regorus::cache::clear();
+}
+
#[pymodule]
pub fn regorus(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
+ #[cfg(feature = "cache")]
+ {
+ m.add_function(wrap_pyfunction!(set_cache_config, m)?)?;
+ m.add_function(wrap_pyfunction!(clear_cache, m)?)?;
+ }
Ok(())
}
diff --git a/bindings/python/test.py b/bindings/python/test.py
index 2f6262e..5ef1bef 100644
--- a/bindings/python/test.py
+++ b/bindings/python/test.py
@@ -6,6 +6,9 @@ import sys
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
+# Configure the global pattern caches.
+regorus.set_cache_config(regex=256, glob=128)
+
# Create engine
engine = regorus.Engine()
diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/Cargo.lock
index 92d4882..b46e74b 100644
--- a/bindings/ruby/Cargo.lock
+++ b/bindings/ruby/Cargo.lock
@@ -549,6 +549,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+
[[package]]
name = "magnus"
version = "0.8.2"
@@ -926,6 +932,7 @@ dependencies = [
"ipnet",
"jsonschema",
"lazy_static",
+ "lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
diff --git a/bindings/ruby/ext/regorusrb/Cargo.toml b/bindings/ruby/ext/regorusrb/Cargo.toml
index 7d7de82..b2f1851 100644
--- a/bindings/ruby/ext/regorusrb/Cargo.toml
+++ b/bindings/ruby/ext/regorusrb/Cargo.toml
@@ -11,8 +11,9 @@ crate-type = ["cdylib"]
path = "src/lib.rs"
[features]
-default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
+default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa"]
ast = ["regorus/ast"]
+cache = ["regorus/cache"]
coverage = ["regorus/coverage"]
[dependencies]
diff --git a/bindings/ruby/ext/regorusrb/src/lib.rs b/bindings/ruby/ext/regorusrb/src/lib.rs
index c4142a4..c1746e4 100644
--- a/bindings/ruby/ext/regorusrb/src/lib.rs
+++ b/bindings/ruby/ext/regorusrb/src/lib.rs
@@ -14,6 +14,13 @@ struct PolicyLengthSpec {
max_lines: usize,
}
+#[cfg(feature = "cache")]
+#[derive(Deserialize)]
+struct CacheConfigSpec {
+ regex: usize,
+ glob: usize,
+}
+
#[derive(Default)]
#[magnus::wrap(class = "Regorus::Engine")]
pub struct Engine {
@@ -417,5 +424,35 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
// ast
engine_class.define_method("get_ast_as_json", method!(Engine::get_ast_as_json, 0))?;
+
+ // cache configuration (module-level)
+ #[cfg(feature = "cache")]
+ {
+ regorus_module
+ .define_module_function("set_cache_config", magnus::function!(set_cache_config, 1))?;
+ regorus_module.define_module_function("clear_cache", magnus::function!(clear_cache, 0))?;
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "cache")]
+fn set_cache_config(ruby: &Ruby, hash: magnus::RHash) -> Result<(), Error> {
+ let spec: CacheConfigSpec = serde_magnus::deserialize(ruby, hash).map_err(|e| {
+ Error::new(
+ runtime_error(),
+ format!("Failed to deserialize cache config: {e}"),
+ )
+ })?;
+ regorus::cache::configure(regorus::cache::Config {
+ regex: spec.regex,
+ glob: spec.glob,
+ });
+ Ok(())
+}
+
+#[cfg(feature = "cache")]
+fn clear_cache() -> Result<(), Error> {
+ regorus::cache::clear();
Ok(())
}
diff --git a/bindings/ruby/test/test_regorus.rb b/bindings/ruby/test/test_regorus.rb
index c06c7cc..b3ea418 100644
--- a/bindings/ruby/test/test_regorus.rb
+++ b/bindings/ruby/test/test_regorus.rb
@@ -188,6 +188,11 @@ class TestRegorus < Minitest::Test
@engine.clear_policy_length_config
end
+ def test_set_cache_config
+ ::Regorus.set_cache_config({ regex: 256, glob: 128 })
+ ::Regorus.clear_cache
+ end
+
def alice_results
{
result: [
diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock
index c286431..03a092a 100644
--- a/bindings/wasm/Cargo.lock
+++ b/bindings/wasm/Cargo.lock
@@ -558,6 +558,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+
[[package]]
name = "memchr"
version = "2.7.6"
@@ -917,9 +923,11 @@ dependencies = [
"ipnet",
"jsonschema",
"lazy_static",
+ "lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
+ "parking_lot",
"postcard",
"rand",
"regex",
diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml
index 42dd89a..edec881 100644
--- a/bindings/wasm/Cargo.toml
+++ b/bindings/wasm/Cargo.toml
@@ -32,9 +32,11 @@ default = [
"regorus/time",
"regorus/uuid",
"regorus/urlquery",
- "regorus/yaml"
+ "regorus/yaml",
+ "cache"
]
ast = ["regorus/ast"]
+cache = ["regorus/cache"]
coverage = ["regorus/coverage"]
[dependencies]
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 6807535..0346017 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -35,6 +35,34 @@ struct PolicyLengthSpec {
max_lines: usize,
}
+#[cfg(feature = "cache")]
+#[derive(Deserialize)]
+struct CacheConfigSpec {
+ regex: usize,
+ glob: usize,
+}
+
+/// Configure the global pattern caches used by regex and glob builtins.
+///
+/// Accepts a JS object: `{ regex, glob }`.
+#[cfg(feature = "cache")]
+#[wasm_bindgen(js_name = "setCacheConfig")]
+pub fn set_cache_config(config: JsValue) -> Result<(), JsValue> {
+ let spec: CacheConfigSpec = serde_wasm_bindgen::from_value(config).map_err(error_to_jsvalue)?;
+ regorus::cache::configure(regorus::cache::Config {
+ regex: spec.regex,
+ glob: spec.glob,
+ });
+ Ok(())
+}
+
+/// Clear all entries from every pattern cache.
+#[cfg(feature = "cache")]
+#[wasm_bindgen(js_name = "clearCache")]
+pub fn clear_cache() {
+ regorus::cache::clear();
+}
+
#[wasm_bindgen]
pub struct Program {
program: Arc,
diff --git a/bindings/wasm/test.js b/bindings/wasm/test.js
index bbd46b3..04d59bc 100644
--- a/bindings/wasm/test.js
+++ b/bindings/wasm/test.js
@@ -3,6 +3,9 @@
var regorus = require('./pkg/regorusjs');
+// Configure the global pattern caches.
+regorus.setCacheConfig({ regex: 256, glob: 128 });
+
// Create an engine.
var engine = new regorus.Engine();
diff --git a/src/builtins/glob.rs b/src/builtins/glob.rs
index 820e126..a2c2749 100644
--- a/src/builtins/glob.rs
+++ b/src/builtins/glob.rs
@@ -52,11 +52,33 @@ fn make_delimiters_unix_style(s: &str, delimiters: &[char]) -> Result {
}
fn make_glob(pattern: &str, span: &Span) -> Result {
- Ok(GlobBuilder::new(pattern)
- .literal_separator(true)
- .build()
- .or_else(|_| bail!(span.error("invalid glob")))?
- .compile_matcher())
+ #[cfg(feature = "cache")]
+ {
+ {
+ let mut cache = crate::cache::GLOB_CACHE.lock();
+ if let Some(matcher) = cache.get(pattern) {
+ return Ok(matcher.clone());
+ }
+ }
+ let matcher = GlobBuilder::new(pattern)
+ .literal_separator(true)
+ .build()
+ .or_else(|_| bail!(span.error("invalid glob")))?
+ .compile_matcher();
+ {
+ let mut cache = crate::cache::GLOB_CACHE.lock();
+ cache.put(alloc::string::String::from(pattern), matcher.clone());
+ }
+ Ok(matcher)
+ }
+ #[cfg(not(feature = "cache"))]
+ {
+ Ok(GlobBuilder::new(pattern)
+ .literal_separator(true)
+ .build()
+ .or_else(|_| bail!(span.error("invalid glob")))?
+ .compile_matcher())
+ }
}
fn glob_match(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result {
diff --git a/src/builtins/regex.rs b/src/builtins/regex.rs
index 36828f4..46f8d7b 100644
--- a/src/builtins/regex.rs
+++ b/src/builtins/regex.rs
@@ -12,6 +12,39 @@ use crate::*;
use anyhow::{bail, Result};
use regex::Regex;
+// ---------------------------------------------------------------------------
+// Compiled-regex cache (feature = "cache")
+//
+// When enabled, compiled Regex objects are stored in a bounded LRU cache
+// protected by a Mutex (parking_lot when std, spin when no_std).
+// The capacity is configurable at runtime
+// via regorus::cache::configure().
+// ---------------------------------------------------------------------------
+
+/// Compile a regex pattern, using the cache when the `cache` feature
+/// is enabled and falling back to direct compilation otherwise.
+fn get_or_compile_regex(pattern: &str) -> core::result::Result {
+ #[cfg(feature = "cache")]
+ {
+ {
+ let mut cache = crate::cache::REGEX_CACHE.lock();
+ if let Some(re) = cache.get(pattern) {
+ return Ok(re.clone());
+ }
+ }
+ let re = Regex::new(pattern)?;
+ {
+ let mut cache = crate::cache::REGEX_CACHE.lock();
+ cache.put(alloc::string::String::from(pattern), re.clone());
+ Ok(re)
+ }
+ }
+ #[cfg(not(feature = "cache"))]
+ {
+ Regex::new(pattern)
+ }
+}
+
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert(
"regex.find_all_string_submatch_n",
@@ -39,8 +72,8 @@ fn find_all_string_submatch_n(
let value = ensure_string(name, ¶ms[1], &args[1])?;
let n = ensure_numeric(name, ¶ms[2], &args[2])?;
- let pattern =
- Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
+ let re = get_or_compile_regex(&pattern)
+ .or_else(|_| bail!(params[0].span().error("invalid regex")))?;
if !n.is_integer() {
bail!(params[2].span().error("n must be an integer"));
@@ -53,8 +86,7 @@ fn find_all_string_submatch_n(
};
Ok(Value::from_array(
- pattern
- .captures_iter(&value)
+ re.captures_iter(&value)
.map(|capture| {
let groups = capture
.iter()
@@ -86,8 +118,8 @@ fn find_n(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> R
let value = ensure_string(name, ¶ms[1], &args[1])?;
let n = ensure_numeric(name, ¶ms[2], &args[2])?;
- let pattern =
- Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
+ let re = get_or_compile_regex(&pattern)
+ .or_else(|_| bail!(params[0].span().error("invalid regex")))?;
if !n.is_integer() {
bail!(params[2].span().error("n must be an integer"));
@@ -100,8 +132,7 @@ fn find_n(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> R
};
Ok(Value::from_array(
- pattern
- .find_iter(&value)
+ re.find_iter(&value)
.map(|m| {
let value = Value::String(m.as_str().into());
// Guard match accumulation while pushing each substring.
@@ -116,8 +147,11 @@ fn find_n(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> R
fn is_valid(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result {
let name = "regex.is_valid";
ensure_args_count(span, name, params, args, 1)?;
- Ok(ensure_string(name, ¶ms[0], &args[0])
- .map_or(Value::Bool(false), |p| Value::Bool(Regex::new(&p).is_ok())))
+ Ok(
+ ensure_string(name, ¶ms[0], &args[0]).map_or(Value::Bool(false), |p| {
+ Value::Bool(get_or_compile_regex(&p).is_ok())
+ }),
+ )
}
pub fn regex_match(
@@ -131,9 +165,9 @@ pub fn regex_match(
let pattern = ensure_string(name, ¶ms[0], &args[0])?;
let value = ensure_string(name, ¶ms[1], &args[1])?;
- let pattern =
- Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
- Ok(Value::Bool(pattern.is_match(&value)))
+ let re = get_or_compile_regex(&pattern)
+ .or_else(|_| bail!(params[0].span().error("invalid regex")))?;
+ Ok(Value::Bool(re.is_match(&value)))
}
fn regex_replace(
@@ -149,15 +183,13 @@ fn regex_replace(
let pattern = ensure_string(name, ¶ms[1], &args[1])?;
let value = ensure_string(name, ¶ms[2], &args[2])?;
- let pattern = match Regex::new(&pattern) {
+ let re = match get_or_compile_regex(&pattern) {
Ok(p) => p,
// TODO: This behavior is due to OPA test not raising error. Should we raise error?
_ => return Ok(Value::Undefined),
};
- Ok(Value::String(
- pattern.replace_all(&s, value.as_ref()).into(),
- ))
+ Ok(Value::String(re.replace_all(&s, value.as_ref()).into()))
}
fn regex_split(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result {
@@ -166,11 +198,10 @@ fn regex_split(span: &Span, params: &[Ref], args: &[Value], _strict: bool)
let pattern = ensure_string(name, ¶ms[0], &args[0])?;
let value = ensure_string(name, ¶ms[1], &args[1])?;
- let pattern =
- Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
+ let re = get_or_compile_regex(&pattern)
+ .or_else(|_| bail!(params[0].span().error("invalid regex")))?;
Ok(Value::from_array(
- pattern
- .split(&value)
+ re.split(&value)
.map(|s| {
let value = Value::String(s.into());
// Guard output accumulation as each split segment is emitted.
@@ -211,13 +242,13 @@ fn regex_template_match(
}
// Fetch pattern, excluding delimiters.
- let pattern = Regex::new(&template[start + delimiter_start.len()..end])
+ let re = get_or_compile_regex(&template[start + delimiter_start.len()..end])
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
// Skip preceding literal in value.
value = &value[start..];
- let m = match pattern.find(value) {
+ let m = match re.find(value) {
Some(m) if m.start() == 0 => m,
_ => return Ok(Value::Bool(false)),
};
diff --git a/src/cache.rs b/src/cache.rs
new file mode 100644
index 0000000..58802d9
--- /dev/null
+++ b/src/cache.rs
@@ -0,0 +1,171 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Compiled-pattern caches for Rego builtins.
+//!
+//! When the `cache` feature is enabled, compiled [`regex::Regex`] and
+//! [`globset::GlobMatcher`] objects are held in bounded LRU caches so that
+//! repeated evaluations of the same pattern avoid recompilation.
+//!
+//! # Examples
+//!
+//! ```ignore
+//! use regorus::cache;
+//!
+//! // Configure cache capacities (0 = disabled).
+//! cache::configure(cache::Config {
+//! regex: 256,
+//! glob: 128,
+//! });
+//!
+//! // Flush all cached patterns.
+//! cache::clear();
+//! ```
+
+#[cfg(any(feature = "regex", feature = "glob"))]
+use core::num::NonZeroUsize;
+#[cfg(any(feature = "regex", feature = "glob"))]
+use lazy_static::lazy_static;
+#[cfg(all(feature = "std", any(feature = "regex", feature = "glob")))]
+use parking_lot::Mutex;
+#[cfg(all(not(feature = "std"), any(feature = "regex", feature = "glob")))]
+use spin::Mutex;
+
+#[cfg(any(feature = "regex", feature = "glob"))]
+use alloc::string::String;
+
+/// Configuration for builtin pattern caches.
+///
+/// Each field controls the maximum number of compiled patterns held in the
+/// corresponding LRU cache. A value of `0` disables that cache entirely
+/// (every lookup recompiles). Values exceeding [`Config::MAX_CAPACITY`] are
+/// clamped silently.
+#[derive(Debug, Clone, Copy)]
+pub struct Config {
+ /// Maximum compiled regex patterns (default 256).
+ pub regex: usize,
+ /// Maximum compiled glob matchers (default 128).
+ pub glob: usize,
+}
+
+impl Config {
+ /// Hard upper bound for any single cache capacity (2^16 = 65 536).
+ pub const MAX_CAPACITY: usize = 1 << 16;
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ regex: 256,
+ glob: 128,
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Internal generic LRU wrapper
+// ---------------------------------------------------------------------------
+
+#[cfg(any(feature = "regex", feature = "glob"))]
+pub(crate) struct LruCache {
+ inner: Option>,
+}
+
+#[cfg(any(feature = "regex", feature = "glob"))]
+impl LruCache {
+ pub(crate) fn new(capacity: usize) -> Self {
+ Self {
+ inner: NonZeroUsize::new(capacity).map(lru::LruCache::new),
+ }
+ }
+
+ /// Look up a key, returning a reference if present. Promotes to most-recent.
+ pub(crate) fn get(&mut self, key: &str) -> Option<&V> {
+ self.inner.as_mut()?.get(key)
+ }
+
+ /// Insert a key-value pair. Evicts the least-recently-used entry if full.
+ pub(crate) fn put(&mut self, key: String, value: V) {
+ if let Some(cache) = self.inner.as_mut() {
+ cache.put(key, value);
+ }
+ }
+
+ /// Remove all entries.
+ pub(crate) fn clear(&mut self) {
+ if let Some(cache) = self.inner.as_mut() {
+ cache.clear();
+ }
+ }
+
+ /// Resize the cache. If new capacity is 0, disables the cache.
+ pub(crate) fn resize(&mut self, capacity: usize) {
+ match NonZeroUsize::new(capacity) {
+ Some(cap) => match self.inner.as_mut() {
+ Some(cache) => cache.resize(cap),
+ None => self.inner = Some(lru::LruCache::new(cap)),
+ },
+ None => {
+ self.inner = None;
+ }
+ }
+ }
+
+ /// Number of entries currently cached.
+ #[allow(dead_code)]
+ pub(crate) fn len(&self) -> usize {
+ self.inner.as_ref().map_or(0, lru::LruCache::len)
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Global regex cache
+// ---------------------------------------------------------------------------
+
+#[cfg(feature = "regex")]
+lazy_static! {
+ pub(crate) static ref REGEX_CACHE: Mutex> =
+ Mutex::new(LruCache::new(Config::default().regex));
+}
+
+// ---------------------------------------------------------------------------
+// Global glob cache
+// ---------------------------------------------------------------------------
+
+#[cfg(feature = "glob")]
+lazy_static! {
+ pub(crate) static ref GLOB_CACHE: Mutex> =
+ Mutex::new(LruCache::new(Config::default().glob));
+}
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+/// Apply a new cache configuration.
+///
+/// Resizes each cache to the specified capacity. Existing entries are
+/// preserved (subject to LRU eviction if the new capacity is smaller).
+/// Values exceeding [`Config::MAX_CAPACITY`] are clamped.
+pub fn configure(config: Config) {
+ let regex = config.regex.min(Config::MAX_CAPACITY);
+ let glob = config.glob.min(Config::MAX_CAPACITY);
+
+ #[cfg(feature = "regex")]
+ REGEX_CACHE.lock().resize(regex);
+
+ #[cfg(feature = "glob")]
+ GLOB_CACHE.lock().resize(glob);
+
+ // Suppress unused-variable warnings when neither regex nor glob is enabled.
+ let _ = (regex, glob);
+}
+
+/// Remove all entries from every pattern cache.
+pub fn clear() {
+ #[cfg(feature = "regex")]
+ REGEX_CACHE.lock().clear();
+
+ #[cfg(feature = "glob")]
+ GLOB_CACHE.lock().clear();
+}
diff --git a/src/interpreter.rs b/src/interpreter.rs
index 6f90382..04b0eb1 100644
--- a/src/interpreter.rs
+++ b/src/interpreter.rs
@@ -289,7 +289,7 @@ impl Interpreter {
}
fn execution_timer_tick(&mut self, work_units: u32) -> Result<()> {
- if self.execution_timer.limit().is_none() {
+ if !self.execution_timer.accumulate(work_units) {
return Ok(());
}
@@ -297,7 +297,7 @@ impl Interpreter {
return Ok(());
};
- self.execution_timer.tick(work_units, now)?;
+ self.execution_timer.check_now(now)?;
Ok(())
}
diff --git a/src/languages/rego/compiler/destructuring.rs b/src/languages/rego/compiler/destructuring.rs
index 674878c..69cc298 100644
--- a/src/languages/rego/compiler/destructuring.rs
+++ b/src/languages/rego/compiler/destructuring.rs
@@ -91,6 +91,19 @@ impl<'a> Compiler<'a> {
AssignmentPlan::EqualityCheck { lhs_expr, rhs_expr } => {
let lhs_reg = self.compile_rego_expr_with_span(lhs_expr, lhs_expr.span(), false)?;
let rhs_reg = self.compile_rego_expr_with_span(rhs_expr, rhs_expr.span(), false)?;
+ if !self.soft_assert_mode {
+ // AssertEq handles the equality assertion inline; the returned
+ // register is not used as a boolean by the caller — it is the
+ // expression's "result register" for potential downstream use.
+ self.emit_instruction(
+ Instruction::AssertEq {
+ left: lhs_reg,
+ right: rhs_reg,
+ },
+ span,
+ );
+ return Ok(lhs_reg);
+ }
let dest = self.alloc_register();
self.emit_instruction(
Instruction::Eq {
@@ -100,9 +113,6 @@ impl<'a> Compiler<'a> {
},
span,
);
- if !self.soft_assert_mode {
- self.emit_instruction(Instruction::AssertCondition { condition: dest }, span);
- }
Ok(dest)
}
AssignmentPlan::WildcardMatch {
@@ -196,35 +206,47 @@ impl<'a> Compiler<'a> {
DestructuringPlan::EqualityExpr(expected_expr) => {
let expected_reg =
self.compile_rego_expr_with_span(expected_expr, expected_expr.span(), false)?;
- let cmp_reg = self.alloc_register();
+ if self.soft_assert_mode {
+ let cmp_reg = self.alloc_register();
+ self.emit_instruction(
+ Instruction::Eq {
+ dest: cmp_reg,
+ left: value_register,
+ right: expected_reg,
+ },
+ span,
+ );
+ return Ok(Some(cmp_reg));
+ }
self.emit_instruction(
- Instruction::Eq {
- dest: cmp_reg,
+ Instruction::AssertEq {
left: value_register,
right: expected_reg,
},
span,
);
- if self.soft_assert_mode {
- return Ok(Some(cmp_reg));
- }
- self.emit_instruction(Instruction::AssertCondition { condition: cmp_reg }, span);
}
DestructuringPlan::EqualityValue(expected_value) => {
let expected_reg = self.load_literal_value(expected_value, span);
- let cmp_reg = self.alloc_register();
+ if self.soft_assert_mode {
+ let cmp_reg = self.alloc_register();
+ self.emit_instruction(
+ Instruction::Eq {
+ dest: cmp_reg,
+ left: value_register,
+ right: expected_reg,
+ },
+ span,
+ );
+ return Ok(Some(cmp_reg));
+ }
self.emit_instruction(
- Instruction::Eq {
- dest: cmp_reg,
+ Instruction::AssertEq {
left: value_register,
right: expected_reg,
},
span,
);
- if self.soft_assert_mode {
- return Ok(Some(cmp_reg));
- }
- self.emit_instruction(Instruction::AssertCondition { condition: cmp_reg }, span);
}
DestructuringPlan::Array { element_plans } => {
self.assert_array_length(value_register, element_plans.len(), span)?;
@@ -371,16 +393,13 @@ impl<'a> Compiler<'a> {
span,
);
- let cmp_reg = self.alloc_register();
self.emit_instruction(
- Instruction::Eq {
- dest: cmp_reg,
+ Instruction::AssertEq {
left: actual_len_reg,
right: expected_len_reg,
},
span,
);
- self.emit_instruction(Instruction::AssertCondition { condition: cmp_reg }, span);
Ok(())
}
}
diff --git a/src/languages/rego/compiler/expressions.rs b/src/languages/rego/compiler/expressions.rs
index 3f39214..ff184a6 100644
--- a/src/languages/rego/compiler/expressions.rs
+++ b/src/languages/rego/compiler/expressions.rs
@@ -5,6 +5,8 @@
mod collection_literals;
mod operations;
+pub(super) use collection_literals::try_eval_const;
+
use super::{Compiler, CompilerError, Register, Result};
use crate::ast::{Expr, ExprRef};
use crate::compiler::destructuring_planner::plans::BindingPlan;
diff --git a/src/languages/rego/compiler/expressions/collection_literals.rs b/src/languages/rego/compiler/expressions/collection_literals.rs
index 1ff8871..ac9ea91 100644
--- a/src/languages/rego/compiler/expressions/collection_literals.rs
+++ b/src/languages/rego/compiler/expressions/collection_literals.rs
@@ -7,20 +7,62 @@
)]
use super::{Compiler, Register, Result};
-use crate::ast::ExprRef;
+use crate::ast::{Expr, ExprRef};
use crate::lexer::Span;
use crate::rvm::instructions::{ArrayCreateParams, ObjectCreateParams, SetCreateParams};
use crate::rvm::Instruction;
use crate::{Rc, Value};
-use alloc::collections::BTreeMap;
+use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
+/// Try to evaluate an expression as a compile-time constant.
+pub(in crate::languages::rego::compiler) fn try_eval_const(expr: &Expr) -> Option {
+ match expr {
+ Expr::Number { value, .. }
+ | Expr::String { value, .. }
+ | Expr::RawString { value, .. }
+ | Expr::Bool { value, .. }
+ | Expr::Null { value, .. } => Some(value.clone()),
+ Expr::UnaryExpr { expr, .. } => match expr.as_ref() {
+ Expr::Number {
+ value: Value::Number(n),
+ ..
+ } => Some(Value::Number(n.neg()?)),
+ _ => None,
+ },
+ Expr::Array { items, .. } => items
+ .iter()
+ .map(|i| try_eval_const(i.as_ref()))
+ .collect::