Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 4552b68e45 chore: release 2026-05-22 13:56:13 -05:00
112 changed files with 2572 additions and 8217 deletions
-12
View File
@@ -1,12 +0,0 @@
;;; Directory Local Variables -*- no-byte-compile: t; -*-
;;; For more information see (info "(emacs) Directory Variables")
;; Regorus is a cargo-verus project (package.metadata.verus.verify = true), so
;; verus-mode.el runs `cargo verus verify' rather than the raw `verus' binary.
;; The cargo-verus path ignores `package.metadata.verus.ide.extra_args' and
;; instead reads `verus-cargo-verus-arguments'. We set it here so that Verus is
;; invoked with the `verus' Cargo feature enabled.
;;
;; Everything before `--' is passed to cargo-verus; everything after `--' is
;; forwarded to the Verus binary. The `--' is required by verus-mode.el.
((verus-mode . ((verus-cargo-verus-arguments . ("--features" "verus" "--")))))
+9 -9
View File
@@ -62,7 +62,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Setup language-specific dependencies BEFORE CodeQL init for proper tracing setup
- name: Setup Rust
@@ -86,26 +86,26 @@ jobs:
- name: Setup Python
if: matrix.language == 'python'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
- name: Setup Java
if: matrix.language == 'java-kotlin'
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'corretto'
java-version: '8'
- name: Setup Go
if: matrix.language == 'go'
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '1.21'
- name: Setup .NET
if: matrix.language == 'csharp'
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ./bindings/csharp/global.json
@@ -115,12 +115,12 @@ jobs:
- name: Setup Node.js
if: matrix.language == 'javascript-typescript'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '18'
- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -141,7 +141,7 @@ jobs:
- name: Setup Ruby
if: matrix.language == 'rust' && contains(matrix.working-directory, 'ruby')
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: '3.4.2'
bundler-cache: true
@@ -188,6 +188,6 @@ jobs:
run: cargo xtask build-wasm --release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
category: "/language:${{matrix.language}}"
@@ -33,7 +33,7 @@ jobs:
# ONLY cargo update and cargo metadata (which do NOT execute build
# scripts) may run against this checkout. Do NOT add cargo build/check/
# test/run steps.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
- bindings/wasm/Cargo.lock
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Run cargo audit
uses: rustsec/audit-check@v2
@@ -53,7 +53,7 @@ jobs:
- xtask/Cargo.toml
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/toolchains/rust
+1 -1
View File
@@ -67,7 +67,7 @@ jobs:
features: arc,opa-no-std
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
MIRIFLAGS: "-Zmiri-disable-isolation"
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- uses: ./.github/actions/toolchains/rust
with:
toolchain: nightly
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
+5 -5
View File
@@ -35,10 +35,10 @@ jobs:
os: windows-latest
extension: dll
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 8
distribution: "corretto"
@@ -46,7 +46,7 @@ jobs:
with:
targets: ${{ matrix.target }}
- if: ${{ matrix.build_cmd == 'zigbuild' }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- if: ${{ matrix.build_cmd == 'zigbuild' }}
@@ -66,10 +66,10 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 8
distribution: "corretto"
+6 -6
View File
@@ -20,8 +20,8 @@ jobs:
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
- uses: ./.github/actions/toolchains/rust
@@ -52,8 +52,8 @@ jobs:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
architecture: ${{ matrix.target }}
@@ -84,8 +84,8 @@ jobs:
matrix:
target: [x86_64, aarch64, universal2-apple-darwin]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
- uses: ./.github/actions/toolchains/rust
+2 -2
View File
@@ -15,11 +15,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
+2 -2
View File
@@ -17,13 +17,13 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Run release-plz
uses: MarcoIeni/release-plz-action@2eb1d8bcb770b4c48ccfaad919734b38b51958c9 # v0.5.131
uses: MarcoIeni/release-plz-action@1528104d2ca23787631a1c1f022abb64b34c1e11 # v0.5.128
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+2 -2
View File
@@ -32,7 +32,7 @@ jobs:
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
@@ -52,7 +52,7 @@ jobs:
- name: Upload analysis results to GitHub
if: ${{ hashFiles('rust-clippy-results.sarif') != '' }}
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v3.29.11
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v3.29.11
with:
sarif_file: rust-clippy-results.sarif
wait-for-processing: true
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
+5 -5
View File
@@ -39,7 +39,7 @@ jobs:
**/release/libregorus_ffi.dylib
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
@@ -73,11 +73,11 @@ jobs:
needs: build-ffi
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ./bindings/csharp/global.json
@@ -131,13 +131,13 @@ jobs:
target: aarch64-apple-darwin
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ./bindings/csharp/global.json
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
@@ -30,7 +30,7 @@ jobs:
- name: Fetch FFI crate dependencies
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
architecture: x64
+2 -2
View File
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 8
distribution: "corretto"
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/toolchains/rust
with:
targets: x86_64-unknown-linux-musl
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/toolchains/rust
with:
targets: thumbv7m-none-eabi
+4 -4
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ${{ matrix.host.name }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
@@ -39,7 +39,7 @@ jobs:
- name: Fetch Python crate dependencies
run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml --target ${{ matrix.host.target }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.10"
architecture: x64
@@ -68,7 +68,7 @@ jobs:
runs-on: ${{ matrix.host.name }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
@@ -82,7 +82,7 @@ jobs:
- name: Fetch Python crate dependencies
run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
architecture: x64
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
@@ -33,7 +33,7 @@ jobs:
run: cargo fetch --locked --manifest-path bindings/wasm/Cargo.toml
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
-80
View File
@@ -1,80 +0,0 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: verus
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
# This workflow only checks out code, downloads a pinned Verus release asset,
# and runs verification. It never writes to the repository, so restrict the
# GITHUB_TOKEN to read-only access to repository contents.
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
with:
components: ""
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus-verus
- name: Install Verus and run verification
shell: bash
run: |
set -euxo pipefail
asset_url=https://github.com/verus-lang/verus/releases/download/release%2F0.2026.07.12.0b42f4c/verus-0.2026.07.12.0b42f4c-x86-linux.zip
asset_sha256=f6f4f5d08e07d3e1ad721d775bda5ba96b9dd0c73b48fc17f2e071866fbd01c0
test -n "$asset_url"
curl -fsSL "$asset_url" -o verus.zip
# Verify the download integrity before trusting/executing its contents.
echo "${asset_sha256} verus.zip" | sha256sum --check --strict
unzip -q verus.zip -d verus-dist
# Search under an absolute path so that `find` yields absolute paths;
# this keeps the PATH entries below valid regardless of the working
# directory.
verus_bin="$(find "$PWD/verus-dist" -type f -name verus -perm -u+x | head -n1)"
cargo_verus_bin="$(find "$PWD/verus-dist" -type f -name cargo-verus -perm -u+x | head -n1)"
version_json="$(find "$PWD/verus-dist" -type f -name version.json | head -n1)"
test -n "$verus_bin"
test -n "$cargo_verus_bin"
test -n "$version_json"
# Verus is built against a specific Rust toolchain and refuses to run
# against any other version. Read the required toolchain from the
# release metadata so we track it automatically instead of hardcoding.
required_toolchain="$(sed -n 's/.*"toolchain"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$version_json")"
test -n "$required_toolchain"
echo "Verus requires Rust toolchain: $required_toolchain"
# Install the exact toolchain Verus expects, including the extra
# components (rustc-dev, llvm-tools) that Verus links against and that
# are not part of the default rustup profile.
rustup toolchain install "$required_toolchain" \
--profile minimal \
--component rustc-dev --component llvm-tools --component rustfmt
# Force cargo/rustc to resolve to the Verus toolchain for the commands
# below, overriding any repository/directory toolchain override.
export RUSTUP_TOOLCHAIN="$required_toolchain"
# Put cargo-verus on PATH for the commands below.
export PATH="$(dirname "$cargo_verus_bin"):$(dirname "$verus_bin"):$PATH"
cargo verus --help
cargo fetch --locked
cargo verus verify --locked --features verus
-29
View File
@@ -6,35 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.11.0](https://github.com/microsoft/regorus/compare/regorus-v0.10.1...regorus-v0.11.0) - 2026-07-21
### Added
- *(compiler)* support registered host-await builtins for natural function call syntax ([#667](https://github.com/microsoft/regorus/pull/667))
- *(value)* introduce Set storage abstraction ([#740](https://github.com/microsoft/regorus/pull/740))
### Fixed
- *(rvm)* assert every-quantifier results so failing cases don't pass ([#765](https://github.com/microsoft/regorus/pull/765))
- `Engine::add_data` now deep-merges nested data documents instead of only merging top-level keys. Adding `{ "a": { "x": 1 } }` followed by `{ "a": { "y": 2 } }` now yields `{ "a": { "x": 1, "y": 2 } }` (matching OPA's data-document merge). Nested sets under a shared key are unioned. Only genuine leaf conflicts (the same path holding two different values) are reported as errors. ([#760](https://github.com/microsoft/regorus/pull/760))
- A zero-arg function producing two different complete values (e.g. `f() := { "a": 1 }` and `f() := { "b": 2 }`) is now reported as a conflict, matching OPA's complete-rule semantics, instead of silently combining the outputs.
### Security
- `Engine::add_data` now rejects data nested beyond 128 levels instead of risking a stack overflow on adversarially deep input.
### Other
- *(deps)* bump the rust-dependencies group across 5 directories with 11 updates ([#764](https://github.com/microsoft/regorus/pull/764))
- Expand keyword-in-ref coverage for complex parser edge cases (interpreter + RVM) ([#744](https://github.com/microsoft/regorus/pull/744))
- *(deps)* bump the rust-dependencies group across 5 directories with 4 updates ([#754](https://github.com/microsoft/regorus/pull/754))
- *(deps)* bump the rust-dependencies group across 5 directories with 6 updates ([#750](https://github.com/microsoft/regorus/pull/750))
- *(value)* migrate Value::Object to Object storage abstraction ([#736](https://github.com/microsoft/regorus/pull/736))
- normalize path separators in folder filter on Windows ([#742](https://github.com/microsoft/regorus/pull/742))
- Introduce Object storage abstraction ([#735](https://github.com/microsoft/regorus/pull/735))
- *(rvm)* add debug-mode invariant assertions ([#737](https://github.com/microsoft/regorus/pull/737))
- *(deps)* bump the rust-dependencies group across 5 directories with 5 updates ([#734](https://github.com/microsoft/regorus/pull/734))
## [0.10.1](https://github.com/microsoft/regorus/compare/regorus-v0.10.0...regorus-v0.10.1) - 2026-05-22
### Fixed
Generated
+389 -350
View File
File diff suppressed because it is too large Load Diff
+6 -21
View File
@@ -8,17 +8,12 @@ members = [
[package]
name = "regorus"
description = "A fast, lightweight Rego (OPA policy language) interpreter"
version = "0.11.0"
version = "0.10.1"
edition = "2021"
license = "MIT AND Apache-2.0 AND BSD-3-Clause"
repository = "https://github.com/microsoft/regorus"
keywords = ["interpreter", "no_std", "opa", "policy-as-code", "rego"]
# Support verification with Verus, a Rust verifier (https://github.com/verus-lang/verus)
[package.metadata.verus]
verify = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
@@ -26,7 +21,6 @@ doctest = false
[features]
default = ["full-opa", "arc", "rvm"]
verus = ["dep:vstd"]
arc = []
ast = []
@@ -49,7 +43,7 @@ cache = ["dep:lru"]
rvm = ["dep:postcard", "dep:indexmap"]
semver = ["dep:semver"]
allocator-memory-limits = ["std", "mimalloc", "mimalloc/allocator-memory-limits"]
std = ["rand/std", "rand/std_rng", "serde_json/std", "indexmap?/std", "msvc_spectre_libs", "dep:parking_lot", "vstd?/std" ]
std = ["rand/std", "rand/std_rng", "serde_json/std", "indexmap?/std", "msvc_spectre_libs", "dep:parking_lot" ]
time = ["dep:chrono", "dep:chrono-tz"]
uuid = ["dep:uuid"]
urlquery = ["dep:url"]
@@ -104,23 +98,23 @@ rand = ["dep:rand"]
[dependencies]
anyhow = { version = "1.0.102", default-features = false }
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] }
serde_json = { version = "1.0.150", default-features = false, features = ["alloc"] }
serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] }
hashbrown = { version = "0.17", default-features = false, features = ["default-hasher"], optional = true }
lazy_static = { version = "1.4.0", default-features = false }
thiserror = { version = "2.0", default-features = false }
data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] }
num-bigint = { version = "0.5", default-features = false }
num-bigint = { version = "0.4", default-features = false }
num-traits = { version = "0.2", default-features = false }
parking_lot = { version = "0.12", optional = true }
spin = { version = "0.12.0", default-features = false, features = ["mutex", "spin_mutex"] }
spin = { version = "0.10.0", default-features = false, features = ["mutex", "spin_mutex"] }
globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true }
regex = {version = "1.12.3", optional = true, default-features = false }
semver = {version = "1.0.28", optional = true, default-features = false }
url = { version = "2.5.4", optional = true }
uuid = { version = "1.22.0", default-features = false, features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.48.5", default-features = false, optional = true }
jsonschema = { version = "0.46.5", default-features = false, optional = true }
chrono = { version = "0.4.44", optional = true }
chrono-tz = { version = "0.10.1", optional = true }
ipnet = { version = "2.12.0", optional = true, default-features = false }
@@ -140,11 +134,6 @@ mimalloc = { package = "regorus-mimalloc", path = "mimalloc", version = "2.2.7",
indexmap = { version = "2.13.1", default-features = false, features = ["serde"], optional = true }
postcard = { version = "1.1.3", default-features = false, features = ["alloc"], optional = true }
# Verus-related dependencies.
# vstd is enabled via the `verus` feature. In no_std builds only the `alloc` feature is used;
# the crate's `std` feature additionally enables `vstd/std` (matching vstd's default features).
vstd = { version = "=0.0.0-2026-07-12-0122", optional = true, default-features = false, features = ["alloc"] }
[dev-dependencies]
anyhow = "1.0.102"
cfg-if = "1.0.0"
@@ -225,7 +214,3 @@ doctest=false
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints.rust]
# Allow `verus_keep_ghost` configuration flag (used by Verus)
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(verus_keep_ghost)'] }
+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RegorusPackageVersion>0.11.0</RegorusPackageVersion>
<RegorusPackageVersion>0.10.1</RegorusPackageVersion>
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
</PropertyGroup>
@@ -115,10 +115,6 @@ public class MemoryGrowthTests
if (i % LogEvery == 0)
{
// Collect transient managed garbage so the working-set delta reflects
// retained (leaked) memory rather than uncollected allocations. A real
// native leak from a missed Dispose() would survive GC and still be caught.
ForceFullGc();
process.Refresh();
var workingSet = process.WorkingSet64;
var managed = GC.GetTotalMemory(false);
@@ -232,10 +228,6 @@ public class MemoryGrowthTests
if (i % LogEvery == 0)
{
// Collect transient managed garbage so the working-set delta reflects
// retained (leaked) memory rather than uncollected allocations. A real
// native leak from a missed Dispose() would survive GC and still be caught.
ForceFullGc();
process.Refresh();
var workingSet = process.WorkingSet64;
var managed = GC.GetTotalMemory(false);
+340 -301
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,7 +2,7 @@
[package]
name = "regorus-ffi"
version = "0.11.0"
version = "0.10.1"
edition = "2021"
license = "MIT AND Apache-2.0 AND BSD-3-Clause"
@@ -13,7 +13,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
anyhow = "1.0"
regorus = { path = "../..", default-features = false }
serde_json = "1.0.150"
serde_json = "1.0.140"
parking_lot = { version = "0.12", optional = true }
[profile.release]
+322 -286
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,7 +2,7 @@
[package]
name = "regorus-java"
version = "0.11.0"
version = "0.10.1"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/java"
description = "Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
@@ -21,6 +21,6 @@ cache = ["regorus/cache"]
[dependencies]
anyhow = "1.0"
serde_json = "1.0.150"
serde_json = "1.0.112"
jni = "0.22.4"
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
+2 -2
View File
@@ -9,7 +9,7 @@
<groupId>com.microsoft.regorus</groupId>
<artifactId>regorus-java</artifactId>
<version>0.11.0</version>
<version>0.10.1</version>
<name>Regorus Java</name>
<description>Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust</description>
@@ -97,7 +97,7 @@
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
<version>3.5.5</version>
<configuration>
<!-- Add debug build to Java path, so it's discoverable by JVM. This is only for tests. -->
<argLine>-Djava.library.path=${project.basedir}/target/debug:${java.library.path}</argLine>
+327 -290
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -2,7 +2,7 @@
[package]
name = "regoruspy"
version = "0.11.0"
version = "0.10.1"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/python"
description = "Python bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
@@ -23,7 +23,7 @@ coverage = ["regorus/coverage"]
[dependencies]
anyhow = "1.0"
ordered-float = "5.3.0"
pyo3 = { version = "0.29.0", features = ["abi3-py310", "anyhow", "extension-module"] }
pyo3 = { version = "0.28.3", features = ["abi3-py310", "anyhow", "extension-module"] }
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
serde_json = "1.0.150"
serde_json = "1.0.140"
+1 -1
View File
@@ -1,5 +1,5 @@
[build-system]
requires = ["maturin>=1.14.1,<2.0"]
requires = ["maturin>=1.4,<2.0"]
build-backend = "maturin"
[project]
+321 -291
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -11,6 +11,6 @@ gem "minitest", "~> 6.0"
gem "rake", "~> 13.4"
gem "rake-compiler", "~> 1.3"
gem "rake-compiler-dock", "~> 1.12"
gem "rubocop", "~> 1.88", require: false
gem "rubocop-minitest", "~> 0.40.0", require: false
gem "rubocop", "~> 1.86", require: false
gem "rubocop-minitest", "~> 0.39.1", require: false
gem "rubocop-rake", "~> 0.7.1", require: false
+10 -10
View File
@@ -9,14 +9,14 @@ GEM
specs:
ast (2.4.3)
drb (2.2.3)
json (2.21.1)
language_server-protocol (3.17.0.6)
json (2.19.4)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
minitest (6.0.6)
minitest (6.0.5)
drb (~> 2.0)
prism (~> 1.5)
parallel (2.1.0)
parser (3.3.12.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
prism (1.9.0)
@@ -26,10 +26,10 @@ GEM
rake-compiler (1.3.1)
rake
rake-compiler-dock (1.12.0)
rb_sys (0.9.128)
rb_sys (0.9.127)
rake-compiler-dock (= 1.12.0)
regexp_parser (2.12.0)
rubocop (1.88.2)
rubocop (1.86.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -40,10 +40,10 @@ GEM
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.50.0)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-minitest (0.40.0)
rubocop-minitest (0.39.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
@@ -65,8 +65,8 @@ DEPENDENCIES
rake-compiler (~> 1.3)
rake-compiler-dock (~> 1.12)
regorusrb!
rubocop (~> 1.88)
rubocop-minitest (~> 0.40.0)
rubocop (~> 1.86)
rubocop-minitest (~> 0.39.1)
rubocop-rake (~> 0.7.1)
BUNDLED WITH
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "regorusrb"
version = "0.11.0"
version = "0.10.1"
edition = "2024"
description = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
license = "MIT AND Apache-2.0 AND BSD-3-Clause"
+1 -1
View File
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Regorus
VERSION = "0.11.0"
VERSION = "0.10.1"
end
+329 -293
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -2,7 +2,7 @@
[package]
name = "regorusjs"
version = "0.11.0"
version = "0.10.1"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/wasm"
description = "WASM bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
@@ -42,7 +42,7 @@ coverage = ["regorus/coverage"]
[dependencies]
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.150"
serde_json = "1.0.140"
wasm-bindgen = "0.2.100"
serde-wasm-bindgen = "0.6"
# Specify uuid as a mandatory dependency so as to enable `js` feature which is now required
@@ -55,7 +55,7 @@ getrandom03 = { package = "getrandom", version = "0.3.1", features = ["std", "wa
getrandom = { version = "0.4.2", features = ["wasm_js"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.72"
wasm-bindgen-test = "0.3.71"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] }
+1 -7
View File
@@ -254,13 +254,7 @@ include formatted state snapshots where possible.
7. **Host await**: In run-to-completion mode, `HostAwait` consumes a response
from `host_await_responses`. Suspendable mode yields control with a
`SuspendReason::HostAwait { dest, argument, identifier }` that the host must
service. The compiler supports two ways to emit `HostAwait`:
- **Explicit**: `__builtin_host_await(payload, identifier)` — raw 2-argument
form.
- **Registered**: `compile_from_policy_with_host_await` accepts a list of
`(name, arg_count)` pairs. Calls to registered names are compiled as
`HostAwait` with the function name as the identifier literal. Registered
names take precedence over user-defined functions and standard builtins.
service.
8. **Completion**: `Return` wraps the selected register value into
`InstructionOutcome::Return`, unwinding frames until the entry frame is
cleared. `RuleReturn` is a specialised variant used by rule execution
-69
View File
@@ -177,75 +177,6 @@ Parameter tables:
- Suspendable: emits `InstructionOutcome::Suspend` with `SuspendReason::HostAwait`.
The host must resume with a value that will be written into `dest`.
### Registered host-await builtins
The compiler can be configured with a list of function names that map directly
to `HostAwait` instructions. This allows policy authors to write natural
function calls (e.g. `lookup(input.account_id)`) instead of the raw
`__builtin_host_await(payload, identifier)` builtin.
Registration is done at compile time via `Compiler::compile_from_policy_with_host_await`:
```rust
let builtins = [("lookup", 1), ("persist", 1)];
let program = Compiler::compile_from_policy_with_host_await(
&compiled_policy, &entry_points, &builtins,
)?;
```
Each registered name is a `(name, arg_count)` pair. When the compiler
encounters a call to a registered name, it emits a `HostAwait` instruction
with:
- `arg` = the first argument register
- `id` = a register loaded with a string literal containing the function name
Both the explicit `__builtin_host_await(arg, id)` call and a registered
builtin call produce the **same `HostAwait` bytecode instruction**. The only
difference is how the `id` register is populated: explicit calls take it from
the second user-supplied argument, while registered calls auto-generate a
`Load` instruction for the function name string. The VM cannot distinguish
between the two at runtime.
**Resolution order** in `determine_call_target()`:
1. `__builtin_host_await` (magic 2-argument form)
2. Registered host-await builtins (matched by **bare** function name only)
3. User-defined functions (matched by package-qualified path)
4. Standard builtins (matched by bare function name)
Registered names shadow both user-defined functions and standard builtins.
This means `time.parse_duration_ns` can be overridden to route through the
host instead of the built-in Rust implementation.
**Only unqualified calls are intercepted.** Registration matches a call by
the name *as written in the policy*. A bare call — `lookup(x)` — is
intercepted and compiled to a `HostAwait`. A package-qualified call —
`data.pkg.lookup(x)` — is **not** intercepted; it is resolved normally, as
if the name were never registered.
```rego
# "lookup" is registered as a host-await builtin.
package other
import rego.v1
lookup(k) := k # an ordinary rule that happens to share the name
package demo
import rego.v1
a := lookup(input.k) # intercepted -> HostAwait
b := data.other.lookup(input.k) # NOT intercepted -> calls other.lookup
```
The qualified form is resolved exactly as it would be without registration:
if a rule exists at that path it is called, otherwise compilation fails with
`Unknown function`. (A standard builtin like `count` has no qualified form at
all, so `data.pkg.count(x)` is always an `Unknown function` error, registered
or not.)
**Argument handling**: The `HostAwait` instruction carries a single `arg`
register. Registered builtins must use `arg_count: 1`; the compiler rejects
`arg_count > 1` at registration time. To pass multiple values, use object
packing: `lookup({"user": x, "resource": y})`.
---
## Halt instruction
-84
View File
@@ -1,84 +0,0 @@
# Object
Opaque container for `Value::Object`'s key→value storage, enabling
alternative backends without call-site changes.
## Design
`Object` wraps the storage for a key→value collection of `Value`s and
provides a curated set of methods (`get`, `insert`, `remove`, `iter`,
`iter_sorted`, `cursor`, serde). The backing store is private; callers
never see or pattern-match on it, so the representation can change
without rippling through call sites.
Multiple backends can coexist at runtime. Because the backing store is
private, different `Object` instances in the same process can use
different implementations — e.g., a lazy DB-backed object for `input`,
inline small-map objects for SARIF location records, and a regular
sorted map elsewhere — all interoperating through the same opaque
type. This is stronger than the typical Cargo-feature-selected backend
seen in precedent crates.
Iteration is split intentionally. `iter()` makes no ordering promise,
which lets backends that don't keep entries sorted skip any sort work.
`iter_sorted()` returns entries in `Value` order and is what
serialization and `Ord` rely on for deterministic output. Cursor types
add resumable, incremental traversal for the RVM iteration state
without leaking iterator internals.
`Ord` and `PartialOrd` are defined against `iter_sorted()` rather than
derived from the storage. Two `Object`s built on different backends —
or with different insertion histories — compare equal whenever their
sorted entries match, so changing the backend never changes observable
comparison results.
## Precedents
Other crates that hide storage behind a stable API so the implementation
can change without breaking callers:
- **`serde_json::Map`** — opaque newtype allowing cargo-feature based
swap between `BTreeMap` (canonical order) and `IndexMap` (insertion
order).
- **`toml::Table`** — opaque newtype allowing cargo-feature based swap
between `BTreeMap` and `IndexMap`.
- **`simdjson` DOM** — opaque tree that lazily materializes nodes on
access instead of parsing the whole document up front.
## Use cases
- **SARIF small-object pressure** — SARIF reports contain millions of
small objects (location records, rule references, message arguments),
most with 2-5 keys. A small-map-optimized backend (inline storage
for ≤N entries, heap above) eliminates per-object BTreeMap allocation
for the common case.
- **Kubernetes admission policies** — large, deeply-nested resource
objects (Pod specs, CRDs) where policies typically touch a handful
of paths. A lazy-materializing backend (`LazyObjectProvider` over
the incoming JSON) parses only the accessed subtrees.
- **Azure Policy aliases** — ARM exposes the same logical property
under multiple aliases (e.g. paths like
`Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.id`).
An alias-aware backend resolves lookups across canonical and alias
forms without rewriting every policy.
- **Azure Policy case-insensitive compare** — ARM property names are
case-preserving but case-insensitive on lookup (`tags.Environment`
and `tags.environment` resolve identically). A case-insensitive
backend centralizes this once at the storage layer instead of at
every comparison site.
- **External data sources** — `input` or `data` backed by a database
query, CBOR slice, REST endpoint, or other streaming source via a
`LazyObjectProvider`. Entries materialize on demand; the policy
only pays for what it touches.
- **Eval-time temporaries** — objects constructed during evaluation
(comprehensions, intermediate rule results) on a bumpalo arena.
The whole arena drops at query end with zero per-entry free cost.
- **Host-language interop** — Python dicts or JS objects accessed via
FFI callbacks from the embedding application, without copying into
Rust on every binding boundary.
-79
View File
@@ -1,79 +0,0 @@
# Set
Opaque container for `Value::Set`'s element storage, enabling alternative
backends without call-site changes. Pairs with [`Object`](object.md) under
a shared design philosophy.
## Design
`Set` wraps a `BTreeSet<Value>` today but exposes only a curated method
surface (`contains`, `insert`, `remove`, `iter`, `iter_sorted`, `cursor`,
`is_subset`, `intersection`, `union`, `difference`, serde). The inner set is
private — callers cannot pattern-match it or hand out references to the
backing store, so the backend can change without churn at the ~400 call
sites that name `Set`.
Two iteration methods reflect a real distinction: `iter()` makes no
ordering promise (lets future hash/lazy backends skip sorting work);
`iter_sorted()` guarantees deterministic order (used by serialization and
`Ord`). Cursor types support incremental traversal needed by the RVM
iteration state without exposing iterator internals.
`Ord` is hand-written against `iter_sorted` rather than derived, so two
backends that store elements differently still compare equal when their
sorted contents match.
## Scenarios enabled
- **Hash-backed storage** — `FxHashSet`-backed inner turns O(log n)
membership checks into O(1); swap in for policies where elements aren't
compared ordinally.
- **Lazy/streaming** — wrap a `LazySetProvider` (DB query, CBOR slice,
REST endpoint) and materialize elements on demand.
- **Arena allocation** — bumpalo-backed inner for eval-time temporaries;
drop the whole arena at query end with zero per-element free cost.
- **FFI-backed** — host-language collections (Python set, JS Set) without
copying into Rust.
- **Bloom-filter pre-check** — front a large backing set with a Bloom
filter for fast negative-membership tests on read-mostly allowlists.
## Known use cases
- **Azure Policy allowed-values lists** — large allowlists (allowed
regions, allowed SKUs, allowed image publishers) compared against
single resource values. Hash-backed Set turns O(log n) membership
checks into O(1).
- **SARIF rule deduplication** — collapsing duplicate rule references
across thousands of result records. Set-of-objects with structural
hashing avoids the BTreeSet sort cost on every insert.
- **RBAC role membership** — checking whether a principal belongs to any
of dozens of role groups. Hash-backed Set scales to thousands of
members with constant-time membership.
- **Azure Policy denied-resource-type sets** — exclusion lists used by
deny-effect policies; same hash-backed pattern as allowed-values.
## Precedents
- **`indexmap::IndexSet`** — opaque newtype that pairs hash lookup with
insertion-order iteration; precedent for "Set with alternative
ordering semantics behind a stable surface."
- **`hashbrown::HashSet`** — backs Rust's `std::collections::HashSet`
and demonstrates a fully swappable backend behind a stable API.
- **`roaring::RoaringBitmap`** — bitmap-backed integer set. Not
applicable to `Value` keys directly, but a precedent for the broader
idea of "Set with alternative storage representations chosen by
workload shape."
- **`serde_json`** — note that `serde_json` has no Set equivalent: its
Value enum collapses sets into arrays. Regorus's first-class Set with
storage abstraction is therefore unusually well-positioned among JSON
value libraries.
## Notes
Cursor types are `pub` (referenced by public `IterationState`) but not
re-exported at the crate root. The crate-internal `Set`/`Map`/`MapEntry`
aliases for `BTreeSet`/`BTreeMap` in `lib.rs` were renamed to
`MapSet`/`Map`/`MapEntry` when this type landed, to free the `Set` name
for the new public type. Future Array and String abstractions follow the
same shape — see `docs/value/array.md` and `docs/value/string.md` when
they land.
+1 -1
View File
@@ -319,7 +319,7 @@ pub fn resolve_path(root: &Value, path: &str) -> Value {
match &current {
Value::Object(map) => {
let mut next = None;
for (key, value) in map.iter_sorted() {
for (key, value) in map.iter() {
if let Value::String(ref key_str) = *key {
if strings::keys::eq(key_str, &segment) {
next = Some(value.clone());
@@ -8,10 +8,10 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Object;
use crate::value::Value;
use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use anyhow::Result;
@@ -72,7 +72,7 @@ fn fn_intersection(
// Intersection of objects: keep key-value pairs from the first
// object only when the key exists in every other object AND
// the value is equal across all of them.
let mut result: Object = first.as_ref().clone();
let mut result: BTreeMap<Value, Value> = first.as_ref().clone();
for arg in rest {
let Value::Object(ref other) = *arg else {
return Ok(Value::Undefined);
@@ -114,7 +114,7 @@ fn fn_union(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool)
Value::Object(_) => {
// Union of objects: recursive merge. Nested objects are merged
// recursively; all other types (including arrays) use last-writer-wins.
let mut result = Object::new();
let mut result = BTreeMap::<Value, Value>::new();
for arg in args {
let Value::Object(ref obj) = *arg else {
return Ok(Value::Undefined);
@@ -264,7 +264,7 @@ fn fn_create_object(
);
}
let mut map = Object::new();
let mut map = BTreeMap::<Value, Value>::new();
for pair in args.chunks(2) {
#[allow(clippy::pattern_type_mismatch)]
@@ -280,9 +280,9 @@ fn fn_create_object(
/// Recursively merge two objects. Nested objects are merged; everything
/// else (including arrays) uses the value from `incoming`.
fn merge_objects(base: &Object, overlay: &Object) -> Value {
fn merge_objects(base: &BTreeMap<Value, Value>, overlay: &BTreeMap<Value, Value>) -> Value {
let mut result = base.clone();
for (k, v) in overlay.iter() {
for (k, v) in overlay {
#[allow(clippy::needless_borrowed_reference)]
let merged = match (result.get(k), v) {
(Some(&Value::Object(ref prev)), &Value::Object(ref next)) => merge_objects(prev, next),
@@ -8,10 +8,10 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Object;
use crate::value::Value;
use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::Result;
@@ -84,8 +84,8 @@ fn fn_items(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool)
return Ok(Value::Undefined);
};
let mut result = Vec::with_capacity(obj.len());
for (k, v) in obj.iter_sorted() {
let mut entry = Object::new();
for (k, v) in obj.as_ref() {
let mut entry = BTreeMap::<Value, Value>::new();
entry.insert(Value::from("key"), k.clone());
entry.insert(Value::from("value"), v.clone());
result.push(Value::Object(Rc::new(entry)));
+1 -1
View File
@@ -308,7 +308,7 @@ fn urlquery_encode_object(
{
let mut pairs = url.query_pairs_mut();
for (key, value) in obj.iter_sorted() {
for (key, value) in obj.iter() {
let key = ensure_string(name, &params[0], key)?;
match value {
Value::String(v) => {
+3 -4
View File
@@ -7,11 +7,10 @@ use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::builtins::utils::{enforce_limit, ensure_args_count, ensure_object};
use crate::lexer::Span;
use crate::value::Object;
use crate::value::Value;
use crate::*;
use alloc::collections::BTreeSet;
use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{bail, Result};
@@ -81,7 +80,7 @@ fn reachable(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) ->
}
fn visit(
graph: &Object,
graph: &BTreeMap<Value, Value>,
visited: &mut BTreeSet<Value>,
node: &Value,
path: &mut Vec<Value>,
@@ -212,7 +211,7 @@ fn walk_visit(path: &mut Vec<Value>, value: &Value, paths: &mut Vec<Value>) -> R
}
}
Value::Object(obj) => {
for (key, value) in obj.iter_sorted() {
for (key, value) in obj.iter() {
path.push(key.clone());
// Guard path stack growth while traversing object entries.
enforce_limit()?;
+1 -1
View File
@@ -205,7 +205,7 @@ fn merge_filters(
let vref = match f {
Value::Object(obj) => {
let obj = Rc::make_mut(obj);
let entry = obj.get_or_insert_with(p.clone(), Value::new_object);
let entry = obj.entry(p.clone()).or_insert_with(Value::new_object);
// Guard filter map growth when creating nested objects.
enforce_limit()?;
entry
+2 -2
View File
@@ -207,7 +207,7 @@ fn to_string(v: &Value, unescape: bool) -> String {
}
Value::Object(o) => {
"{".to_owned()
+ &o.iter_sorted()
+ &o.iter()
.map(|(k, v)| to_string(k, true) + ": " + &to_string(v, true))
.collect::<Vec<String>>()
.join(", ")
@@ -568,7 +568,7 @@ fn replace_n(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -
let mut s = ensure_string(name, &params[1], &args[1])?;
let span = params[0].span();
for item in obj.as_ref().iter_sorted() {
for item in obj.as_ref().iter() {
match item {
(Value::String(k), Value::String(v)) => {
s = s.replace(k.as_ref(), v.as_ref()).into();
+2 -3
View File
@@ -5,12 +5,11 @@
use crate::ast::{Expr, Ref};
use crate::lexer::Span;
use crate::number::Number;
use crate::value::Object;
use crate::Rc;
use crate::Value;
use crate::*;
use alloc::collections::BTreeSet;
use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{bail, Result};
@@ -169,7 +168,7 @@ pub fn ensure_set(fcn: &str, arg: &Expr, v: Value) -> Result<Rc<BTreeSet<Value>>
})
}
pub fn ensure_object(fcn: &str, arg: &Expr, v: Value) -> Result<Rc<Object>> {
pub fn ensure_object(fcn: &str, arg: &Expr, v: Value) -> Result<Rc<BTreeMap<Value, Value>>> {
Ok(match v {
Value::Object(o) => o,
_ => {
+1 -1
View File
@@ -217,7 +217,7 @@ pub(crate) struct CompiledPolicyData {
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
pub(crate) functions: FunctionTable,
pub(crate) rule_paths: MapSet<String>,
pub(crate) rule_paths: Set<String>,
#[cfg(feature = "azure_policy")]
pub(crate) target_info: Option<TargetInfo>,
#[cfg(feature = "azure_policy")]
+4 -35
View File
@@ -434,13 +434,7 @@ impl Engine {
/// Add data document.
///
/// The specified data document is deep-merged into the existing data document. Nested
/// objects are merged recursively (matching OPA's data-document merge), so adding
/// `{ "a": { "x": 1 } }` and then `{ "a": { "y": 2 } }` yields `{ "a": { "x": 1, "y": 2 } }`.
/// A conflict — the same path holding two different values — is an error.
///
/// The merge is atomic: if any conflict is detected (including one deep in a nested
/// document), the call fails and the existing data document is left unchanged.
/// The specified data document is merged into existing data document.
///
/// ```
/// # use regorus::*;
@@ -459,13 +453,9 @@ impl Engine {
/// // Merge { "z" : 3 }. Conflict error.
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 3 }"#)?).is_err());
///
/// // Nested objects are deep-merged. Merge { "y" : { "a" : 10 } } then { "y" : { "b" : 20 } }.
/// assert!(engine.add_data(Value::from_json_str(r#"{ "y" : { "a" : 10 } }"#)?).is_ok());
/// assert!(engine.add_data(Value::from_json_str(r#"{ "y" : { "b" : 20 } }"#)?).is_ok());
///
/// assert_eq!(
/// engine.eval_query("data".to_string(), false)?.result[0].expressions[0].value,
/// Value::from_json_str(r#"{ "x": 1, "y": { "a": 10, "b": 20 }, "z": 2}"#)?
/// Value::from_json_str(r#"{ "x": 1, "y": {}, "z": 2}"#)?
/// );
/// # Ok(())
/// # }
@@ -474,29 +464,8 @@ impl Engine {
if data.as_object().is_err() {
bail!("data must be object");
}
// add_data is all-or-nothing; the atomic strategy differs by build because the failure
// modes do: a conflict (same path, differing values) is possible everywhere, an
// allocator-limit failure mid-merge only under `allocator-memory-limits`.
#[cfg(not(feature = "allocator-memory-limits"))]
{
// Conflict is the only failure mode; `check_mergeable` catches it up front without
// allocating, so validate then deep-merge in place (zero-copy fast path).
self.interpreter.get_init_data().check_mergeable(&data)?;
self.prepared = false;
self.interpreter.get_init_data_mut().deep_merge(data)
}
#[cfg(feature = "allocator-memory-limits")]
{
// A limit failure can strike mid-merge and can't be predicted, so merge into a
// candidate and commit only on success. `Value` is copy-on-write, so only touched
// subtrees are cloned.
let mut candidate = self.interpreter.get_init_data().clone();
candidate.deep_merge(data)?;
*self.interpreter.get_init_data_mut() = candidate;
self.prepared = false;
Ok(())
}
self.prepared = false;
self.interpreter.get_init_data_mut().merge(data)
}
/// Get the data document.
+49 -187
View File
@@ -28,6 +28,7 @@ use crate::{Expression, Extension, Location, QueryResult, QueryResults};
use crate::query::traversal::traverse;
use crate::Rc;
use alloc::collections::btree_map::Entry as BTreeMapEntry;
use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{anyhow, bail, Result};
use core::ops::Bound::*;
@@ -60,17 +61,6 @@ enum FunctionModifier {
Value(Value),
}
/// How [`Interpreter::update_data`] merges a rule's value into the data document.
#[derive(Debug, Clone, Copy)]
enum RuleValueMerge {
/// Shallow-merge keeping disjoint keys, so rules sharing a path prefix scaffold into one
/// object (`a.foo` + `a.bar` → one `a`) instead of conflicting.
Combine,
/// Complete-rule semantics: existing value must be absent or exactly equal, else conflict.
/// Used for zero-arg function outputs (`f() := …`), which OPA treats like complete rules.
Strict,
}
type RuleValues = BTreeMap<Vec<Value>, (Value, Ref<Expr>)>;
#[derive(Debug)]
@@ -1258,34 +1248,7 @@ impl Interpreter {
// Apply with modifiers.
for wm in &stmt.with_mods {
let path = Parser::get_path_ref_components(&wm.refr)?;
let mut path: Vec<String> = path.iter().map(|s| s.text().to_string()).collect();
// Matching OPA, a leading import alias is rewritten before
// any lookups: functions register as overrides below,
// anything else becomes a data override. Only the alias
// component is replaced so bracketed keys containing dots
// survive the rewrite.
let rewritten: Option<Vec<String>> = match path.split_first() {
Some((head, rest)) if head.as_str() != "data" => {
self.lookup_import(head).and_then(|import_expr| {
// Use the import target's parsed components, not
// its dot-joined string, so bracketed keys
// containing dots survive in the import path too.
let comps = Parser::get_path_ref_components(import_expr).ok()?;
Some(
comps
.iter()
.map(|s| s.text().to_string())
.chain(rest.iter().cloned())
.collect(),
)
})
}
_ => None,
};
if let Some(new_path) = rewritten {
path = new_path;
}
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
let mut target = path.join(".");
let mut target_is_function = self.lookup_function_by_name(&target).is_some()
@@ -1324,17 +1287,11 @@ impl Interpreter {
if self.lookup_function_by_name(&function_path).is_none() {
// Lookup without current module path prefixed.
function_path = get_path_string(&wm.r#as, None)?;
if self.lookup_function_by_name(&function_path).is_none() {
// Resolve an aliased replacement before builtins.
let resolved = self
.resolve_fcn_path_through_imports(&function_path)
.filter(|r| self.compiled_policy.functions.contains_key(r));
if let Some(resolved) = resolved {
function_path = resolved;
} else if !Self::is_builtin(wm.r#as.span(), &function_path) {
// bail!(wm.r#as.span().error("could not evaluate expression"));
skip_exec = true;
}
if self.lookup_function_by_name(&function_path).is_none()
&& !Self::is_builtin(wm.r#as.span(), &function_path)
{
// bail!(wm.r#as.span().error("could not evaluate expression"));
skip_exec = true;
}
}
self.with_functions
@@ -1355,10 +1312,10 @@ impl Interpreter {
*obj = Value::new_object();
}
obj = obj.as_object_mut()?.get_or_insert_with(
Value::String(p.to_string().into()),
Value::new_object,
);
obj = obj
.as_object_mut()?
.entry(Value::String(p.to_string().into()))
.or_insert(Value::new_object());
}
*obj = value;
// Mark modified rules as processed.
@@ -1725,7 +1682,8 @@ impl Interpreter {
let set = obj
.as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))?
.get_or_insert_with(p, Value::new_set)
.entry(p)
.or_insert(Value::new_set())
.as_set_mut()
.map_err(|_| anyhow!(span.error("previous value is not a set")))?;
set.append(value.as_set_mut()?);
@@ -1733,13 +1691,20 @@ impl Interpreter {
let obj = obj
.as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))?;
if value == Value::Undefined {
// TODO: clean this assumption between Undefined vs Object.
obj.get_or_insert_with(p, Value::new_object);
} else {
let existing = obj.get_or_insert_with(p, || value.clone());
if *existing != value {
bail!(span.error("complete rules should not produce multiple outputs"))
match obj.entry(p) {
BTreeMapEntry::Vacant(v) => {
if value != Value::Undefined {
v.insert(value);
} else {
// TODO: clean this assumption between Undefined vs Object.
v.insert(Value::new_object());
}
}
BTreeMapEntry::Occupied(o) => {
if o.get() != &value && value != Value::Undefined {
bail!(span
.error("complete rules should not produce multiple outputs"))
}
}
}
}
@@ -1748,7 +1713,8 @@ impl Interpreter {
obj = obj
.as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))?
.get_or_insert_with(p, Value::new_object);
.entry(p)
.or_insert(Value::new_object());
}
}
Ok(())
@@ -1856,7 +1822,8 @@ impl Interpreter {
let set = ctx_mut
.rule_value
.as_object_mut()?
.get_or_insert_with(Value::from_array(comps), Value::new_set);
.entry(Value::from_array(comps))
.or_insert(Value::new_set());
if output != Value::Undefined {
set.as_set_mut()?.insert(output);
return Ok(true);
@@ -1865,13 +1832,20 @@ impl Interpreter {
}
// Non-set rule.
let key = Value::from_array(comps);
let obj_mut = ctx_mut.rule_value.as_object_mut()?;
let existing = obj_mut.get_or_insert_with(key, || output.clone());
if *existing != output {
bail!(rule_ref
match ctx_mut
.rule_value
.as_object_mut()?
.entry(Value::from_array(comps))
{
BTreeMapEntry::Vacant(v) => {
v.insert(output);
}
BTreeMapEntry::Occupied(o) if o.get() != &output => bail!(rule_ref
.span()
.error("rules must not produce multiple outputs"));
.error("rules must not produce multiple outputs")),
_ => {
// Rule produced same value.
}
}
return Ok(true);
@@ -2404,72 +2378,6 @@ impl Interpreter {
}
}
/// Look up the import of the current module with the given alias, e.g.
/// the `data.a.b` import expression for `b` after `import data.a.b`.
fn lookup_import(&self, alias: &str) -> Option<&Ref<Expr>> {
if self.compiled_policy.imports.is_empty() {
return None;
}
let import_key = format!("{}.{}", self.current_module_path, alias);
self.compiled_policy.imports.get(&import_key)
}
/// Look up the dot-joined target path of an import of the current module
/// with the given alias, e.g. `data.a.b` for `b` after `import data.a.b`.
fn lookup_import_alias(&self, alias: &str) -> Option<String> {
get_path_string(self.lookup_import(alias)?, None).ok()
}
/// Rewrite a path whose leading component is an import alias of the
/// current module to the import's target, e.g. `b.f` to `data.a.b.f`
/// after `import data.a.b`.
fn rewrite_path_through_imports(&self, path: &str) -> Option<String> {
if path.starts_with("data.") {
return None;
}
let (alias, rest) = match path.split_once('.') {
Some((alias, rest)) => (alias, Some(rest)),
None => (path, None),
};
let target = self.lookup_import_alias(alias)?;
Some(match rest {
Some(rest) => format!("{target}.{rest}"),
None => target,
})
}
/// Rewrite an import-aliased call path to its target, e.g. `b.f(1)` to
/// `data.a.b.f` after `import data.a.b`. Resolves only when the target is
/// a known function or default function, so an alias whose target defines
/// the called function shadows a like-named builtin namespace, while other
/// spellings keep their prior meaning (e.g. a builtin call). OPA instead
/// rewrites aliases unconditionally and rejects calls to a missing target
/// at compile time.
fn resolve_fcn_path_through_imports(&self, path: &str) -> Option<String> {
let candidate = self.rewrite_path_through_imports(path)?;
(self.compiled_policy.functions.contains_key(&candidate)
|| self.is_default_function(&candidate))
.then_some(candidate)
}
/// True if `path` is the exact path of a `default` function rule.
/// `default_rules` also indexes every prefix of a rule path, so it cannot
/// be consulted alone: `rule_paths` holds only exact rule paths, and the
/// non-empty argument list distinguishes functions from value rules.
fn is_default_function(&self, path: &str) -> bool {
self.compiled_policy.rule_paths.contains(path)
&& self
.compiled_policy
.default_rules
.get(path)
.is_some_and(|rules| {
rules.iter().any(|(rule, _)| {
matches!(rule.as_ref(), Rule::Default { args, .. } if !args.is_empty())
})
})
}
fn eval_builtin_call(
&mut self,
span: &Span,
@@ -2563,7 +2471,7 @@ impl Interpreter {
}
Value::Object(map) => {
s.push('{');
for (idx, (k, entry_value)) in map.iter_sorted().enumerate() {
for (idx, (k, entry_value)) in map.iter().enumerate() {
if idx > 0 {
s.push_str(", ");
}
@@ -2641,13 +2549,6 @@ impl Interpreter {
param_values.push(self.eval_expr(p)?);
}
// Resolve a leading import alias before the `with` override and builtin
// lookups, so an override keyed by the full path reaches aliased calls
// and the alias shadows a like-named builtin namespace (matching OPA).
let fcn_path = self
.resolve_fcn_path_through_imports(&fcn_path)
.unwrap_or(fcn_path);
let orig_fcn_path = fcn_path.clone();
let mut with_functions_saved = None;
@@ -2824,12 +2725,7 @@ impl Interpreter {
let value = match self.eval_rule_bodies(ctx, span, bodies) {
Ok(v) => v,
Err(e) => {
// If the rule produces an error, save the error. Restore
// the caller's module even so: leaving the callee's module
// in place would make the rest of the caller's body
// resolve paths through the wrong module's imports when
// the error is swallowed below in non-strict mode.
self.set_current_module(prev_module)?;
// If the rule produces an error, save the error.
errors.push(e);
self.scopes = scopes;
continue;
@@ -3530,23 +3426,6 @@ impl Interpreter {
}
}
/// Materialize a complete-rule value: the existing value must be absent or *exactly equal*
/// to `new`, else it is a conflict.
///
/// Unlike the shallow [`Self::merge_rule_value`], differing outputs conflict instead of
/// combining — `f() := {"a": 1}` and `f() := {"b": 2}` conflict — matching OPA's semantics
/// for zero-arg functions.
fn merge_rule_value_strict(span: &Span, value: &mut Value, new: Value) -> Result<()> {
if *value == Value::Undefined {
*value = new;
Ok(())
} else if *value == new {
Ok(())
} else {
Err(span.error("rules should not produce multiple outputs."))
}
}
pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result<String> {
let mut comps = vec![];
let mut expr_opt = Some(refr);
@@ -3802,7 +3681,6 @@ impl Interpreter {
_refr: &Expr,
path: &[&str],
value: Value,
merge: RuleValueMerge,
) -> Result<()> {
if value == Value::Undefined {
return Ok(());
@@ -3810,10 +3688,7 @@ impl Interpreter {
// Ensure that path is created.
let vref = Self::make_or_get_value_mut(&mut self.data, path)?;
if Self::get_value_chained(self.init_data.clone(), path) == Value::Undefined {
match merge {
RuleValueMerge::Strict => Self::merge_rule_value_strict(span, vref, value),
RuleValueMerge::Combine => Self::merge_rule_value(span, vref, value),
}
Self::merge_rule_value(span, vref, value)
} else {
// Retain specified value.
Ok(())
@@ -3921,13 +3796,7 @@ impl Interpreter {
// `a` is created as an empty object.
if let Some((_, prefix)) = path.split_last() {
if !prefix.is_empty() {
self.update_data(
span,
refr,
prefix,
Value::new_object(),
RuleValueMerge::Combine,
)?;
self.update_data(span, refr, prefix, Value::new_object())?;
}
}
@@ -3939,13 +3808,7 @@ impl Interpreter {
};
let value = self.eval_rule_bodies(ctx, span, rule_body)?;
self.update_data(
refr.span(),
refr,
&path[..],
value,
RuleValueMerge::Strict,
)?;
self.update_data(refr.span(), refr, &path[..], value)?;
}
}
}
@@ -4192,7 +4055,6 @@ impl Interpreter {
rule_refr,
&prefix_path,
Value::new_object(),
RuleValueMerge::Combine,
)?;
}
}
@@ -213,10 +213,10 @@ pub fn denormalize_with_aliases(
// Phase 4: Attach properties to result.
if !properties.is_empty() {
if let Some(Value::Object(existing_rc)) = result.get_mut("properties") {
// Merge directly into the Object, avoiding full ObjMap round-trip.
// Merge directly into the BTreeMap, avoiding full ObjMap round-trip.
let existing = Rc::make_mut(existing_rc);
for (k, v) in properties {
existing.get_or_insert_with(Value::String(k), || v);
existing.entry(Value::String(k)).or_insert(v);
}
} else {
obj_insert(&mut result, "properties", make_value(properties));
@@ -7,7 +7,6 @@ use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec::Vec;
use crate::value::Object;
use crate::Value;
use super::super::obj_map::{make_value, new_map, obj_insert, val_str, ObjMap};
@@ -142,7 +141,7 @@ fn rewrap_nested_array(
/// BTreeMap-native recursion for nested sub-resource array re-wrapping,
/// avoiding ObjMap round-trips on each array element.
fn rewrap_nested_array_in_btree(
btree: &mut Object,
btree: &mut alloc::collections::BTreeMap<Value, Value>,
parent_parts: &[&str],
array_name: &str,
envelope_fields: &BTreeSet<String>,
@@ -188,7 +187,10 @@ fn rewrap_nested_array_in_btree(
}
/// Find a key in a BTreeMap using case-insensitive comparison.
fn find_key_ci_btree(btree: &Object, key: &str) -> Option<Value> {
fn find_key_ci_btree(
btree: &alloc::collections::BTreeMap<Value, Value>,
key: &str,
) -> Option<Value> {
btree
.keys()
.find(|k| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(key)))
@@ -6,12 +6,11 @@
use alloc::string::String;
use alloc::vec::Vec;
use crate::value::Object;
use crate::Value;
use super::super::obj_map::{
obj_get, obj_get_mut, obj_insert, set_nested, set_nested_lowercased, set_nested_verbatim,
ObjMap,
obj_get, obj_get_mut, obj_insert, set_nested_in_btree, set_nested_lowercased,
set_nested_verbatim, ObjMap,
};
use super::super::types::PrecomputedRemap;
@@ -119,7 +118,7 @@ fn apply_remap_at_depth(
/// BTreeMap-native recursion for element-level remap, avoiding ObjMap
/// round-trips on each array element.
fn remap_at_depth_in_btree(
btree: &mut Object,
btree: &mut alloc::collections::BTreeMap<Value, Value>,
array_chain: &[Vec<String>],
depth: usize,
source_field: &str,
@@ -178,7 +177,12 @@ fn remap_at_depth_in_btree(
}
/// Remap a value between dotted paths directly in a BTreeMap.
fn remap_deep_field_in_btree(btree: &mut Object, source: &str, target: &str, lowercase: bool) {
fn remap_deep_field_in_btree(
btree: &mut alloc::collections::BTreeMap<Value, Value>,
source: &str,
target: &str,
lowercase: bool,
) {
let val = match read_dotted_path_btree(btree, source) {
Some(v) => v,
None => return,
@@ -194,11 +198,14 @@ fn remap_deep_field_in_btree(btree: &mut Object, source: &str, target: &str, low
}
return;
}
set_nested(btree, &segments, val, lowercase);
set_nested_in_btree(btree, &segments, val, lowercase);
}
/// Read a value at a dotted path from a BTreeMap.
fn read_dotted_path_btree(btree: &Object, path: &str) -> Option<Value> {
fn read_dotted_path_btree(
btree: &alloc::collections::BTreeMap<Value, Value>,
path: &str,
) -> Option<Value> {
let segments: Vec<&str> = path.split('.').collect();
let first = segments.first()?;
let mut cur: &Value = btree.get(&Value::from(*first))?;
@@ -13,7 +13,6 @@ mod flatten;
// Re-export items used by the denormalizer.
pub(crate) use element_remap::{apply_element_remap, ElementRemap};
use crate::value::Object;
use crate::Value;
use super::obj_map::{
@@ -110,7 +109,7 @@ pub fn normalize_with_aliases(
/// Merge `properties` fields into the result map, skipping keys that already
/// exist.
fn merge_properties(
obj: &Object,
obj: &alloc::collections::BTreeMap<Value, Value>,
result: &mut ObjMap,
sub_arrays: Option<&alloc::collections::BTreeSet<alloc::string::String>>,
) {
+51 -33
View File
@@ -4,7 +4,7 @@
//! Lightweight string-keyed map used during normalization/denormalization.
//!
//! Internally uses `hashbrown::HashMap<Rc<str>, Value>` for O(1) lookups,
//! then converts to `Value::Object` (an `Object`) only at
//! then converts to `Value::Object` (a `BTreeMap<Value, Value>`) only at
//! the output boundary via [`make_value`].
use alloc::string::String;
@@ -12,7 +12,6 @@ use alloc::vec::Vec;
use hashbrown::HashMap;
use crate::value::Object;
use crate::Rc;
use crate::Value;
@@ -82,13 +81,14 @@ pub fn obj_remove(map: &mut ObjMap, key: &str) -> Option<Value> {
/// Convert an [`ObjMap`] into a [`Value::Object`].
///
/// Keys are converted from `Rc<str>` to `Value::String` and inserted into
/// an `Object` to match the `Value::Object` representation.
/// a `BTreeMap` to match the `Value::Object` representation.
pub fn make_value(map: ObjMap) -> Value {
let obj: Object = map
.into_iter()
.map(|(k, v)| (Value::String(k), v))
.collect();
Value::Object(Rc::new(obj))
use alloc::collections::BTreeMap;
let mut btree = BTreeMap::new();
for (k, v) in map {
btree.insert(Value::String(k), v);
}
Value::Object(Rc::new(btree))
}
/// Convert a `Vec<Value>` into a `Value::Array`.
@@ -115,14 +115,14 @@ pub fn extract_type_field(resource: &Value) -> Option<&str> {
})
}
/// Convert a `Value::Object` (Object) into an [`ObjMap`].
/// Convert a `Value::Object` (BTreeMap<Value, Value>) into an [`ObjMap`].
///
/// Non-string keys are silently skipped.
#[allow(dead_code)]
pub fn value_to_obj_map(value: &Value) -> Option<ObjMap> {
let obj = value.as_object().ok()?;
let mut map = ObjMap::with_capacity(obj.len());
for (k, v) in obj.iter() {
let btree = value.as_object().ok()?;
let mut map = ObjMap::with_capacity(btree.len());
for (k, v) in btree.iter() {
if let Value::String(s) = k {
map.insert(Rc::clone(s), v.clone());
}
@@ -194,7 +194,7 @@ fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase
// Descend directly into the BTreeMap, avoiding ObjMap round-trip.
if let Some(Value::Object(inner_rc)) = obj.get_mut(&*seg) {
let inner_btree = Rc::make_mut(inner_rc);
set_nested(
set_nested_in_btree(
inner_btree,
segments.get(1..).unwrap_or_default(),
value,
@@ -203,12 +203,17 @@ fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase
}
}
/// Set a value at a path directly in an `Object`, creating
/// Set a value at a path directly in a `BTreeMap<Value, Value>`, creating
/// intermediate `Value::Object` nodes as needed.
///
/// This avoids the `btree_to_obj_map` / `obj_map_to_btree` round-trip that
/// would clone every sibling entry at each nesting level.
pub fn set_nested(obj: &mut Object, segments: &[&str], value: Value, lowercase: bool) {
pub fn set_nested_in_btree(
btree: &mut alloc::collections::BTreeMap<Value, Value>,
segments: &[&str],
value: Value,
lowercase: bool,
) {
let Some(&first) = segments.first() else {
return;
};
@@ -221,18 +226,18 @@ pub fn set_nested(obj: &mut Object, segments: &[&str], value: Value, lowercase:
let key_val = Value::String(Rc::clone(&key_rc));
if segments.len() == 1 {
obj.insert(key_val, value);
btree.insert(key_val, value);
return;
}
// Ensure an intermediate object exists.
if !obj.contains_key(&key_val) {
obj.insert(key_val.clone(), make_value(new_map()));
if !btree.contains_key(&key_val) {
btree.insert(key_val.clone(), make_value(new_map()));
}
if let Some(Value::Object(inner_rc)) = obj.get_mut(&key_val) {
if let Some(Value::Object(inner_rc)) = btree.get_mut(&key_val) {
let inner = Rc::make_mut(inner_rc);
set_nested(
set_nested_in_btree(
inner,
segments.get(1..).unwrap_or_default(),
value,
@@ -347,15 +352,20 @@ fn remove_field_at_depth(obj: &mut ObjMap, array_chain: &[Vec<String>], depth: u
for elem in inner.iter_mut() {
if let Value::Object(obj_rc) = elem {
let inner_btree = Rc::make_mut(obj_rc);
remove_field_at_depth_obj(inner_btree, array_chain, depth.saturating_add(1), field);
remove_field_at_depth_in_btree(
inner_btree,
array_chain,
depth.saturating_add(1),
field,
);
}
}
}
}
/// Object-native recursion for element-level field removal.
fn remove_field_at_depth_obj(
obj: &mut Object,
/// BTreeMap-native recursion for element-level field removal.
fn remove_field_at_depth_in_btree(
btree: &mut alloc::collections::BTreeMap<Value, Value>,
array_chain: &[Vec<String>],
depth: usize,
field: &str,
@@ -364,10 +374,10 @@ fn remove_field_at_depth_obj(
let segments: Vec<&str> = field.split('.').collect();
if segments.len() == 1 {
if let Some(&seg) = segments.first() {
obj.remove(&Value::from(seg));
btree.remove(&Value::from(seg));
}
} else if segments.len() > 1 {
remove_at_dotted_path_obj(obj, &segments);
remove_at_dotted_path_in_btree(btree, &segments);
}
return;
};
@@ -379,12 +389,12 @@ fn remove_field_at_depth_obj(
let key_val = Value::from(first);
let arr_val = if nav.len() == 1 {
match obj.get_mut(&key_val) {
match btree.get_mut(&key_val) {
Some(v) => v,
None => return,
}
} else {
let mut cur: &mut Value = match obj.get_mut(&key_val) {
let mut cur: &mut Value = match btree.get_mut(&key_val) {
Some(v) => v,
None => return,
};
@@ -405,19 +415,27 @@ fn remove_field_at_depth_obj(
for elem in inner.iter_mut() {
if let Value::Object(obj_rc) = elem {
let inner_btree = Rc::make_mut(obj_rc);
remove_field_at_depth_obj(inner_btree, array_chain, depth.saturating_add(1), field);
remove_field_at_depth_in_btree(
inner_btree,
array_chain,
depth.saturating_add(1),
field,
);
}
}
}
}
/// Remove the leaf segment at a dotted path directly in an Object.
fn remove_at_dotted_path_obj(obj: &mut Object, segments: &[&str]) {
/// Remove the leaf segment at a dotted path directly in a BTreeMap.
fn remove_at_dotted_path_in_btree(
btree: &mut alloc::collections::BTreeMap<Value, Value>,
segments: &[&str],
) {
let Some((&leaf, parent_segs)) = segments.split_last() else {
return;
};
if parent_segs.is_empty() {
obj.remove(&Value::from(leaf));
btree.remove(&Value::from(leaf));
return;
}
@@ -425,7 +443,7 @@ fn remove_at_dotted_path_obj(obj: &mut Object, segments: &[&str]) {
return;
};
let first_key = Value::from(first);
let parent_val = match obj.get_mut(&first_key) {
let parent_val = match btree.get_mut(&first_key) {
Some(v) => v,
None => return,
};
@@ -11,7 +11,7 @@
//! to fetch a related resource and an optional `existenceCondition` evaluated
//! inline.
use crate::value::Object;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::ToString as _;
use alloc::vec::Vec;
@@ -814,7 +814,7 @@ pub(super) fn build_object_from_keys(
span: &crate::lexer::Span,
) -> Result<u8> {
// Build template: object with all keys set to Undefined.
let mut template = Object::new();
let mut template = BTreeMap::new();
for &(key_idx, _) in &keys {
// key_idx was returned by `add_literal_u16` in the calling code,
// so it is always in bounds. We use `.get()` + `?` instead of
@@ -272,7 +272,7 @@ impl Compiler {
fn insert_string_set_annotation(
annot: &mut alloc::collections::BTreeMap<String, Value>,
key: &str,
observed: &alloc::collections::BTreeSet<String>,
observed: &BTreeSet<String>,
) {
if !observed.is_empty() {
let set: BTreeSet<Value> = observed
@@ -11,9 +11,8 @@ use crate::ast::{Expr, ExprRef};
use crate::lexer::Span;
use crate::rvm::instructions::{ArrayCreateParams, ObjectCreateParams, SetCreateParams};
use crate::rvm::Instruction;
use crate::value::Object;
use crate::{Rc, Value};
use alloc::collections::BTreeSet;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
/// Try to evaluate an expression as a compile-time constant.
@@ -44,7 +43,7 @@ pub(in crate::languages::rego::compiler) fn try_eval_const(expr: &Expr) -> Optio
Expr::Object { fields, .. } => fields
.iter()
.map(|(_, k, v)| Some((try_eval_const(k.as_ref())?, try_eval_const(v.as_ref())?)))
.collect::<Option<Object>>()
.collect::<Option<BTreeMap<_, _>>>()
.map(|m| Value::Object(Rc::new(m))),
_ => None,
}
@@ -118,7 +117,7 @@ impl<'a> Compiler<'a> {
fields: &[(crate::lexer::Span, ExprRef, ExprRef)],
span: &Span,
) -> Result<Register> {
let all_const: Option<Object> = fields
let all_const: Option<BTreeMap<_, _>> = fields
.iter()
.map(|(_, k, v)| Some((try_eval_const(k.as_ref())?, try_eval_const(v.as_ref())?)))
.collect();
@@ -167,7 +166,7 @@ impl<'a> Compiler<'a> {
let mut template_keys = literal_keys.clone();
template_keys.sort();
let mut template_obj = Object::new();
let mut template_obj = BTreeMap::new();
for key in &template_keys {
template_obj.insert(key.clone(), Value::Undefined);
}
+9 -113
View File
@@ -17,20 +17,8 @@ use crate::lexer::Span;
use crate::rvm::instructions::{BuiltinCallParams, FunctionCallParams};
use crate::rvm::Instruction;
use crate::utils::get_path_string;
use crate::value::Value;
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use alloc::{format, string::ToString, vec::Vec};
/// Resolved destination of a Rego function-call expression. Produced by
/// [`Compiler::determine_call_target`] and consumed by
/// [`Compiler::compile_function_call`] to choose which instruction to emit.
/// Carrying the discrimination in the type (rather than re-matching on a
/// magic name at the emit site) keeps the host-await handling honest under
/// future refactors — the compiler will refuse to build if a new variant is
/// added without updating every match site.
enum CallTarget {
User {
rule_index: u16,
@@ -40,14 +28,9 @@ enum CallTarget {
builtin_index: u16,
expected_args: Option<usize>,
},
/// Explicit `__builtin_host_await(arg, id)` call form (2 user args).
/// The identifier is supplied by the policy author at runtime via the
/// second argument register.
ExplicitHostAwait,
/// A registered host-awaitable builtin invoked by its registered name
/// (1 user arg). The identifier is the registered name itself and is
/// baked into the bytecode as a string literal at compile time.
RegisteredHostAwait { identifier: String },
HostAwait {
expected_args: Option<usize>,
},
}
impl<'a> Compiler<'a> {
@@ -63,9 +46,6 @@ impl<'a> Compiler<'a> {
let original_fcn_path = fcn_path.clone();
let full_fcn_path = if self.policy.inner.rules.contains_key(&fcn_path) {
fcn_path
} else if let Some(resolved) = self.resolve_fcn_path_through_imports(&original_fcn_path) {
// Resolve a leading import alias before module-prefixing and builtins.
resolved
} else {
get_path_string(fcn, Some(&self.current_package))
.map_err(|_| CompilerError::InvalidFunctionExpressionWithPackage.at(&span))?
@@ -79,11 +59,7 @@ impl<'a> Compiler<'a> {
let expected_args = match &call_target {
CallTarget::User { expected_args, .. } => *expected_args,
CallTarget::Builtin { expected_args, .. } => *expected_args,
// Both host-await variants have a known fixed arity; carrying it
// in the variant lets the rest of the compiler depend on the type
// rather than re-matching on the magic name `__builtin_host_await`.
CallTarget::ExplicitHostAwait => Some(2),
CallTarget::RegisteredHostAwait { .. } => Some(1),
CallTarget::HostAwait { expected_args } => *expected_args,
};
if let Some(expected) = expected_args {
@@ -150,8 +126,7 @@ impl<'a> Compiler<'a> {
});
self.emit_instruction(Instruction::BuiltinCall { params_index }, &span);
}
CallTarget::ExplicitHostAwait => {
// Explicit __builtin_host_await(arg, id) — 2 arguments
CallTarget::HostAwait { .. } => {
if arg_regs.len() != 2 {
return Err(CompilerError::General {
message: format!(
@@ -161,6 +136,7 @@ impl<'a> Compiler<'a> {
}
.at(&span));
}
self.emit_instruction(
Instruction::HostAwait {
dest,
@@ -170,37 +146,6 @@ impl<'a> Compiler<'a> {
&span,
);
}
CallTarget::RegisteredHostAwait { identifier } => {
// Registered host-awaitable builtin — the identifier is the
// registered name and is baked into the bytecode as a literal.
if arg_regs.len() != 1 {
return Err(CompilerError::General {
message: format!(
"host-awaitable builtin '{}' expects exactly 1 argument, got {}",
identifier,
arg_regs.len()
),
}
.at(&span));
}
let id_reg = self.alloc_register();
let literal_idx = self.add_literal(Value::String(identifier.into()));
self.emit_instruction(
Instruction::Load {
dest: id_reg,
literal_idx,
},
&span,
);
self.emit_instruction(
Instruction::HostAwait {
dest,
arg: arg_regs[0],
id: id_reg,
},
&span,
);
}
}
if let Some((plan, plan_span)) = &out_param_plan {
@@ -223,37 +168,6 @@ impl<'a> Compiler<'a> {
Ok(dest)
}
/// Rewrite an import-aliased call path to its target, e.g. `b.f(1)` to
/// `data.a.b.f` after `import data.a.b`. Resolves only when the target is
/// a known function (the `rules` map cannot be used here: it also indexes
/// value rules and every rule-path prefix, which must not become callable
/// through an alias), so an alias whose target defines the called
/// function shadows a like-named builtin namespace, while other spellings
/// keep their prior meaning (e.g. a builtin call). OPA instead rewrites
/// aliases unconditionally and rejects calls to a missing target at
/// compile time.
fn resolve_fcn_path_through_imports(&self, path: &str) -> Option<String> {
if self.policy.inner.imports.is_empty() || path.starts_with("data.") {
return None;
}
let (alias, rest) = match path.split_once('.') {
Some((alias, rest)) => (alias, Some(rest)),
None => (path, None),
};
let import_key = format!("{}.{}", self.current_package, alias);
let import_expr = self.policy.inner.imports.get(&import_key)?;
let target = get_path_string(import_expr, None).ok()?;
let candidate = match rest {
Some(rest) => format!("{target}.{rest}"),
None => target,
};
self.policy
.inner
.functions
.contains_key(&candidate)
.then_some(candidate)
}
fn lookup_builtin_arity(&self, name: &str) -> Option<usize> {
if name == "print" {
Some(2)
@@ -273,26 +187,8 @@ impl<'a> Compiler<'a> {
span: &Span,
) -> Result<CallTarget> {
if original_fcn_path == "__builtin_host_await" {
return Ok(CallTarget::ExplicitHostAwait);
}
// Check registered host-awaitable builtins. Registered builtins are
// restricted to arg_count == 1 at registration time (see
// `Compiler::register_host_await_builtin`), so the variant doesn't
// need to carry an arity — it's fixed at 1.
//
// We deliberately match against `original_fcn_path` only, not
// `full_fcn_path`. Registration intercepts the *unqualified* call
// form (e.g. `lookup(x)` inside the policy's own package). A
// package-qualified call like `data.other.lookup(x)` is left to
// resolve through the normal user-defined / builtin path, so a
// registered name does not leak into unrelated packages that
// happen to expose a rule with the same identifier. This is
// documented on `register_host_await_builtin`; the
// `registered_host_await.yaml` suite pins the behavior.
if self.host_await_builtins.contains_key(original_fcn_path) {
return Ok(CallTarget::RegisteredHostAwait {
identifier: original_fcn_path.to_string(),
return Ok(CallTarget::HostAwait {
expected_args: Some(2),
});
}
+1 -33
View File
@@ -13,7 +13,7 @@ use crate::ast::{self, ExprRef, LiteralStmt, Query};
use crate::compiler::destructuring_planner::plans::BindingPlan;
use crate::compiler::hoist::{HoistedLoop, LoopType};
use crate::lexer::Span;
use crate::rvm::instructions::{GuardMode, LoopMode, LoopStartParams};
use crate::rvm::instructions::{LoopMode, LoopStartParams};
use crate::rvm::Instruction;
use crate::Value;
use alloc::format;
@@ -197,19 +197,6 @@ impl<'a> Compiler<'a> {
*end = loop_end;
}
// The loop writes its overall pass/fail into `result_reg`
// (`success_count == total_iterations` for `Every`). The enclosing query
// must fail (evaluate to undefined) when the quantifier does not hold, so
// guard on `result_reg` here. Without this the `every` result is computed
// but discarded, leaving the surrounding rule to always succeed.
self.emit_instruction(
Instruction::Guard {
register: result_reg,
mode: GuardMode::Condition,
},
span,
);
Ok(())
}
@@ -325,25 +312,6 @@ impl<'a> Compiler<'a> {
*end = loop_end;
}
// A hoisted index-iteration loop inside an `every` body acts as a
// condition on the current iteration: if the indexed reference matches
// nothing the iteration must fail. The `every` body emits no context
// yield, so the loop result register is otherwise discarded (same
// situation as `some ... in`). Guard on it so a non-matching indexed
// reference fails the enclosing `every` iteration.
if matches!(
self.context_stack.last().map(|c| &c.context_type),
Some(ContextType::Every)
) {
self.emit_instruction(
Instruction::Guard {
register: result_reg,
mode: GuardMode::Condition,
},
collection.span(),
);
}
Ok(())
}
-72
View File
@@ -26,9 +26,7 @@ use crate::rvm::program::{Program, RuleType, SpanInfo};
use crate::CompiledPolicy;
use crate::Value;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::String;
use alloc::string::ToString as _;
use alloc::vec;
use alloc::vec::Vec;
use indexmap::IndexMap;
@@ -141,10 +139,6 @@ pub struct Compiler<'a> {
current_call_stack: Vec<u16>,
entry_points: IndexMap<String, usize>,
soft_assert_mode: bool,
/// Registered host-awaitable builtins: name → expected arg count.
/// When the compiler encounters a call to one of these names, it emits a
/// `HostAwait` instruction instead of a regular function or builtin call.
host_await_builtins: BTreeMap<String, usize>,
}
impl<'a> Compiler<'a> {
@@ -179,75 +173,9 @@ impl<'a> Compiler<'a> {
current_call_stack: Vec::new(),
entry_points: IndexMap::new(),
soft_assert_mode: false,
host_await_builtins: BTreeMap::new(),
}
}
/// Register a function name as a host-awaitable builtin.
///
/// When the compiler encounters an **unqualified** call to `name(arg)`
/// (i.e. `name(arg)` from inside the policy's own package, not
/// `data.pkg.name(arg)` or any other package-qualified form), it will
/// emit a `HostAwait` instruction with the argument and `name` as the
/// identifier, instead of treating it as a user-defined or standard
/// builtin function.
///
/// Package-qualified calls (e.g. `data.other.name(arg)`) are **not**
/// intercepted by registration. Those resolve through the normal
/// user-defined / builtin lookup against their fully-qualified path
/// (`data.other.name`).
///
/// `arg_count` must be exactly 1. The `HostAwait` instruction carries a
/// single argument register; use object packing to pass multiple values
/// (e.g. `name({"key1": v1, "key2": v2})`).
///
/// Returns `Err` when:
/// - `name` is the reserved identifier `__builtin_host_await`,
/// - `name` is empty, only whitespace, or has leading/trailing
/// whitespace (whitespace-padded names would never match the
/// trimmed identifier produced by the Rego parser, creating dead
/// registrations),
/// - `name` is already registered (duplicate registration is rejected
/// rather than silently overwritten),
/// - `arg_count` is not exactly 1.
pub fn register_host_await_builtin(&mut self, name: &str, arg_count: usize) -> Result<()> {
if name == "__builtin_host_await" {
return Err(CompilerError::General {
message: "__builtin_host_await is a reserved name and cannot be registered as a host-await builtin"
.to_string(),
}
.into());
}
if name.is_empty() || name != name.trim() {
return Err(CompilerError::General {
message: format!(
"host-await builtin name {name:?} must not be empty or contain leading/trailing whitespace"
),
}
.into());
}
if self.host_await_builtins.contains_key(name) {
return Err(CompilerError::General {
message: format!(
"host-await builtin '{name}' is already registered; \
duplicate registration is not allowed"
),
}
.into());
}
if arg_count != 1 {
return Err(CompilerError::General {
message: format!(
"registered host-await builtin '{name}' must have arg_count == 1, got {arg_count}. \
Use object packing to pass multiple values."
),
}
.into());
}
self.host_await_builtins.insert(name.to_string(), arg_count);
Ok(())
}
pub(super) fn with_soft_assert_mode<F, R>(&mut self, enabled: bool, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
+1 -20
View File
@@ -70,31 +70,12 @@ impl<'a> Compiler<'a> {
..
} = &stmt.literal
{
let some_result_reg = self.compile_some_in_loop_with_remaining_statements(
self.compile_some_in_loop_with_remaining_statements(
key,
value,
collection,
&stmts[idx..],
)?;
// Inside an `every` body a `some ... in` acts as a condition
// on the current iteration: if it matches nothing the
// iteration must fail. Unlike a top-level rule body (where
// per-iteration context yields produce the results), the
// `every` body has no yield, so the loop result register is
// otherwise discarded. Guard on it so a `some` that matches
// nothing fails the enclosing `every` iteration.
if matches!(
self.context_stack.last().map(|c| &c.context_type),
Some(ContextType::Every)
) {
self.emit_instruction(
Instruction::Guard {
register: some_result_reg,
mode: GuardMode::Condition,
},
&stmt.span,
);
}
return Ok(());
}
}
-12
View File
@@ -234,20 +234,8 @@ impl<'a> Compiler<'a> {
pub fn compile_from_policy(
policy: &CompiledPolicy,
entry_points: &[&str],
) -> Result<Arc<Program>> {
Self::compile_from_policy_with_host_await(policy, entry_points, &[])
}
/// Compile from a CompiledPolicy to RVM Program with registered host-awaitable builtins.
pub fn compile_from_policy_with_host_await(
policy: &CompiledPolicy,
entry_points: &[&str],
host_await_builtins: &[(&str, usize)],
) -> Result<Arc<Program>> {
let mut compiler = Compiler::with_policy(policy);
for &(name, arg_count) in host_await_builtins {
compiler.register_host_await_builtin(name, arg_count)?;
}
compiler.current_rule_path = "".to_string();
let rules = policy.get_rules();
+3 -3
View File
@@ -155,7 +155,7 @@ pub mod target;
#[cfg(any(test, all(feature = "yaml", feature = "std")))]
pub mod test_utils;
pub mod utils;
pub mod value;
mod value;
#[cfg(feature = "azure_policy")]
pub use {
@@ -205,10 +205,10 @@ pub use alloc::sync::Arc as Rc;
pub use alloc::rc::Rc;
#[cfg(feature = "std")]
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as MapSet};
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as Set};
#[cfg(not(feature = "std"))]
use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as MapSet};
use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as Set};
use alloc::{
borrow::ToOwned as _,
+15 -88
View File
@@ -27,14 +27,10 @@ use num_traits::{One, Signed, ToPrimitive, Zero};
use serde::ser::Serializer;
use serde::Serialize;
#[cfg(feature = "verus")]
use vstd::prelude::*;
use crate::*;
pub type BigInt = NumBigInt;
#[cfg_attr(feature = "verus", verus_verify)]
const F64_SAFE_INTEGER: f64 = 9_007_199_254_740_992.0; // 2^53
#[derive(Clone)]
@@ -144,9 +140,13 @@ impl Number {
}
}
fn ints_to_bigint(a: &Number, b: &Number) -> (BigInt, BigInt) {
(a.to_bigint_owned().unwrap(), b.to_bigint_owned().unwrap())
}
fn normalize_float(value: f64) -> Number {
if let Some(i) = Self::float_to_small_bigint(value) {
return Self::from_bigint_owned(i);
if let Some(int) = Self::float_to_small_bigint(value) {
return Self::from_bigint_owned(int);
}
Number::Float(value)
}
@@ -587,11 +587,7 @@ impl Number {
}
}
(Number::Int(a), Number::Int(b)) => {
if *a == i64::MIN && *b == -1 {
// Rust panics on i64::MIN % -1i64, so handle it specially
let quotient = BigInt::from(*a) / BigInt::from(*b);
Ok(Number::from_bigint_owned(quotient))
} else if *a % *b == 0 {
if *a % *b == 0 {
if let Some(q) = a.checked_div(*b) {
Ok(Number::Int(q))
} else {
@@ -660,17 +656,15 @@ impl Number {
}
pub fn modulo(self, rhs: &Self) -> Result<Number> {
// Conversion fails for a non-integral float, and also for an integral
// one whose magnitude exceeds 2^53, which cannot be represented exactly.
let (a, b) = match (self.to_bigint_owned(), rhs.to_bigint_owned()) {
(Some(a), Some(b)) => (a, b),
_ => bail!("modulo on floating-point number"),
};
if b.is_zero() {
if rhs.is_zero() {
bail!("modulo by zero");
}
if !self.is_integer() || !rhs.is_integer() {
bail!("modulo on floating-point number");
}
let (a, b) = Number::ints_to_bigint(&self, rhs);
let rem = a % &b;
Ok(Number::from_bigint_owned(rem))
}
@@ -691,7 +685,6 @@ impl Number {
}
}
#[allow(clippy::if_then_some_else_none)]
fn ensure_integers(a: &Number, b: &Number) -> Option<(BigInt, BigInt)> {
if a.is_integer() && b.is_integer() {
Some((a.to_bigint_owned()?, b.to_bigint_owned()?))
@@ -783,8 +776,7 @@ impl Number {
if e >= 0 {
Ok(two_pow_positive(e as u32))
} else {
// Must cast to i64 before negating in case it's i32::MIN
let denom = two_pow_positive((-(e as i64)) as u32);
let denom = two_pow_positive((-e) as u32);
Number::from(1u64).divide(&denom)
}
}
@@ -793,8 +785,7 @@ impl Number {
if e >= 0 {
Ok(ten_pow_positive(e as u32))
} else {
// Must cast to i64 before negating in case it's i32::MIN
let denom = ten_pow_positive((-(e as i64)) as u32);
let denom = ten_pow_positive((-e) as u32);
Number::from(1u64).divide(&denom)
}
}
@@ -993,67 +984,3 @@ fn scientific_parts_to_bigint(mantissa: &str, exponent: i32) -> Option<BigInt> {
Some(value)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)] // tests expect() to assert arithmetic results
use super::*;
use alloc::string::ToString;
/// Regression test: `i64::MIN / -1` overflows `i64` and panics in Rust's
/// native integer division/remainder. `divide` must promote the result
/// instead of panicking.
#[test]
fn i64_min_by_negative_one() {
let quotient = Number::Int(i64::MIN)
.divide(&Number::Int(-1))
.expect("division should succeed");
// 2^63 does not fit in i64, but does fit in u64.
assert_eq!(quotient.as_u64(), Some(9_223_372_036_854_775_808));
assert_eq!(quotient.as_i64(), None);
assert_eq!(quotient.as_i128(), Some(9_223_372_036_854_775_808));
assert_eq!(
*quotient.to_big().expect("to_big should succeed"),
-BigInt::from(i64::MIN)
);
// The same overflow case reached via the mixed `Int`/`BigInt` path.
let big_quotient = Number::Int(i64::MIN)
.divide(&Number::BigInt(Rc::new(BigInt::from(-1))))
.expect("division should succeed");
assert_eq!(big_quotient.as_u64(), Some(9_223_372_036_854_775_808));
// `i64::MIN % -1` also panics natively; the result must be zero.
let remainder = Number::Int(i64::MIN)
.modulo(&Number::Int(-1))
.expect("modulo should succeed");
assert_eq!(remainder.as_i64(), Some(0));
}
#[test]
fn modulo_handles_floats_that_are_really_integers() {
// An integral float is a valid operand.
assert!(matches!(
Number::Float(4.0).modulo(&Number::Int(3)),
Ok(Number::UInt(1))
));
// `1e300` has no fractional part, but it is too large to convert to an
// integer exactly. This must report an error, not panic.
assert_eq!(
Number::Float(1e300)
.modulo(&Number::Int(3))
.err()
.map(|e| e.to_string()),
Some("modulo on floating-point number".to_string())
);
assert_eq!(
Number::Int(3)
.modulo(&Number::Float(1e300))
.err()
.map(|e| e.to_string()),
Some("modulo on floating-point number".to_string())
);
}
}
+5 -35
View File
@@ -354,33 +354,6 @@ impl<'source> Parser<'source> {
}
}
/// Parse a field name after `.` in a ref expression.
///
/// Unlike [`Self::parse_var`] and [`Self::parse_ident`], this method accepts **any**
/// `TokenKind::Ident` token, including reserved keywords (e.g. `as`, `default`, `else`,
/// `false`, `if`, `import`, `in`, `not`, `null`, `package`, `some`, `true`, `with`).
///
/// The position immediately after `.` is unambiguously a field name, so there is no
/// syntactic ambiguity with statement-level keywords. This matches OPA's
/// `keywords_in_refs` capability, which is enabled by default in standard OPA builds.
///
/// # Example
/// ```rego
/// allow if { input.v0.package.format == "npm" } # `package` is a keyword but valid here
/// ```
fn parse_ref_field(&mut self) -> Result<Span> {
let span = self.tok.1.clone();
match self.tok.0 {
TokenKind::Ident => {
self.next_token()?;
Ok(span)
}
_ => Err(self
.source
.error(self.tok.1.line, self.tok.1.col, "expecting identifier")),
}
}
fn read_number(&mut self, span: Span) -> Result<Expr> {
match Number::from_str(span.text()) {
Ok(v) => Ok(Expr::Number {
@@ -770,10 +743,9 @@ impl<'source> Parser<'source> {
);
}
"." => {
// Read identifier. Keywords are allowed as field names in
// dot-notation refs (e.g. `input.package.name`).
// Read identifier.
self.next_token()?;
let field = self.parse_ref_field()?;
let field = self.parse_var()?;
span.end = self.end;
// Disallow any whitespace between . and identifier.
@@ -1446,10 +1418,9 @@ impl<'source> Parser<'source> {
);
}
"." => {
// Read identifier. Keywords are allowed as field names in
// dot-notation refs (e.g. `import data.my.package`).
// Read identifier.
self.next_token()?;
let field = self.parse_ref_field()?;
let field = self.parse_ident()?;
span.end = self.end;
// Disallow any whitespace between . and identifier.
@@ -1552,8 +1523,7 @@ impl<'source> Parser<'source> {
"." => {
let sep_pos = self.tok.1.start;
self.next_token()?;
// Keywords are allowed as field names in dot-notation refs.
let field = self.parse_ref_field()?;
let field = self.parse_var()?;
span.end = self.end;
// Disallow any whitespace between . and identifier.
+7 -6
View File
@@ -11,9 +11,9 @@
//! values are converted through [`MetadataValue`] — a postcard/bincode-safe
//! enum that avoids `deserialize_any`.
use crate::value::Object;
use crate::Rc;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::collections::BTreeMap;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
@@ -52,7 +52,7 @@ impl ProgramMetadata {
pub fn to_value(&self) -> crate::value::Value {
use crate::value::Value;
let mut obj = Object::new();
let mut obj = BTreeMap::new();
obj.insert(
Value::String("compiler_version".into()),
Value::String(self.compiler_version.as_str().into()),
@@ -75,7 +75,7 @@ impl ProgramMetadata {
);
if !self.annotations.is_empty() {
let mut annotations_obj = Object::new();
let mut annotations_obj = BTreeMap::new();
for (k, v) in &self.annotations {
annotations_obj.insert(Value::String(k.as_str().into()), v.clone());
}
@@ -198,7 +198,7 @@ impl MetadataValue {
match *self {
MetadataValue::String(ref s) => Value::String(s.as_str().into()),
MetadataValue::StringSet(ref set) => {
let mut bset = BTreeSet::new();
let mut bset = alloc::collections::BTreeSet::new();
for s in set {
bset.insert(Value::String(s.as_str().into()));
}
@@ -211,7 +211,7 @@ impl MetadataValue {
Value::Array(Rc::new(values))
}
MetadataValue::Map(ref map) => {
let mut obj = Object::new();
let mut obj = BTreeMap::new();
for (k, v) in map {
obj.insert(Value::String(k.as_str().into()), v.to_value());
}
@@ -257,6 +257,7 @@ mod metadata_serde {
mod tests {
use super::*;
use crate::value::Value;
use alloc::collections::BTreeSet;
/// Round-trip: Value → MetadataValue → Value must be equivalent for
/// all lossless variants (strings, bools, integers, arrays, objects).
+5 -6
View File
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use alloc::collections::BTreeSet;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
@@ -11,7 +11,6 @@ use serde::ser::{SerializeSeq as _, SerializeTuple as _};
use serde::{Deserialize, Serialize};
use crate::number::Number;
use crate::value::Object;
use crate::value::Value;
const VARIANT_NULL: u32 = 0;
@@ -133,7 +132,7 @@ impl<'a> Serialize for BinarySetRef<'a> {
}
}
struct BinaryObjectRef<'a>(&'a Object);
struct BinaryObjectRef<'a>(&'a BTreeMap<Value, Value>);
impl<'a> Serialize for BinaryObjectRef<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -141,7 +140,7 @@ impl<'a> Serialize for BinaryObjectRef<'a> {
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for (key, value) in self.0.iter_sorted() {
for (key, value) in self.0.iter() {
seq.serialize_element(&BinaryEntryRef(key, value))?;
}
seq.end()
@@ -262,11 +261,11 @@ impl<'de> Visitor<'de> for BinaryValueVisitor {
}
(BinaryVariant::Object, variant) => {
let entries: Vec<(BinaryValue, BinaryValue)> = variant.newtype_variant()?;
let mut map = Object::new();
let mut map = BTreeMap::new();
for (key, value) in entries {
map.insert(key.into_value(), value.into_value());
}
Ok(BinaryValue(Value::Object(crate::Rc::new(map))))
Ok(BinaryValue(Value::from(map)))
}
(BinaryVariant::Undefined, variant) => {
variant.unit_variant()?;
+4 -3
View File
@@ -6,6 +6,8 @@
// Disable both to keep patterns consistent within this file.
#![allow(clippy::pattern_type_mismatch, clippy::needless_borrowed_reference)]
use alloc::collections::BTreeSet;
use crate::number::Number;
use crate::value::Value;
@@ -30,9 +32,8 @@ impl RegoVM {
match (a, b) {
(&Value::Number(ref x), &Value::Number(ref y)) => Ok(Value::from(x.sub(y)?)),
(&Value::Set(ref left), &Value::Set(ref right)) => {
let diff: alloc::collections::BTreeSet<Value> =
left.difference(right).cloned().collect();
Ok(Value::from(diff))
let diff: BTreeSet<Value> = left.difference(right).cloned().collect();
Ok(Value::from_set(diff))
}
_ => Err(VmError::InvalidSubtraction {
left: a.clone(),
+103 -88
View File
@@ -2,9 +2,9 @@
// Licensed under the MIT License.
use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode};
use crate::value::Object;
use crate::value::Value;
use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::sync::Arc;
use alloc::vec::Vec;
@@ -34,12 +34,12 @@ impl RegoVM {
let initial_result = match params.mode {
ComprehensionMode::Set => Value::new_set(),
ComprehensionMode::Array => Value::new_array(),
ComprehensionMode::Object => Value::Object(Rc::new(Object::new())),
ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::new())),
};
self.set_register(params.result_reg, initial_result.clone())?;
let auto_iterate = params.collection_reg != params.result_reg;
let mut iteration_state = if auto_iterate {
let iteration_state = if auto_iterate {
let source_value = self.get_register(params.collection_reg)?.clone();
match source_value {
Value::Array(items) => {
@@ -53,9 +53,11 @@ impl RegoVM {
if obj.is_empty() {
None
} else {
// O(1) cursor over shared Rc<Object>.
let cursor = obj.cursor();
Some(IterationState::Object { obj, cursor })
Some(IterationState::Object {
obj,
current_key: None,
first_iteration: true,
})
}
}
Value::Set(set) => {
@@ -77,7 +79,7 @@ impl RegoVM {
None
};
let has_iteration = if let Some(state) = iteration_state.as_mut() {
let has_iteration = if let Some(state) = iteration_state.as_ref() {
self.setup_next_iteration(state, params.key_reg, params.value_reg)?
} else {
false
@@ -121,12 +123,12 @@ impl RegoVM {
let initial_result = match params.mode {
ComprehensionMode::Set => Value::new_set(),
ComprehensionMode::Array => Value::new_array(),
ComprehensionMode::Object => Value::Object(Rc::new(Object::new())),
ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::new())),
};
self.set_register(params.result_reg, initial_result.clone())?;
let auto_iterate = params.collection_reg != params.result_reg;
let mut iteration_state = if auto_iterate {
let iteration_state = if auto_iterate {
let source_value = self.get_register(params.collection_reg)?.clone();
match source_value {
Value::Array(items) => {
@@ -140,8 +142,11 @@ impl RegoVM {
if obj.is_empty() {
None
} else {
let cursor = obj.cursor();
Some(IterationState::Object { obj, cursor })
Some(IterationState::Object {
obj,
current_key: None,
first_iteration: true,
})
}
}
Value::Set(set) => {
@@ -163,7 +168,7 @@ impl RegoVM {
None
};
let has_iteration = if let Some(state) = iteration_state.as_mut() {
let has_iteration = if let Some(state) = iteration_state.as_ref() {
self.setup_next_iteration(state, params.key_reg, params.value_reg)?
} else {
false
@@ -226,7 +231,6 @@ impl RegoVM {
}
}
#[allow(clippy::if_then_some_else_none)]
fn execute_comprehension_yield_run_to_completion(
&mut self,
value_reg: u8,
@@ -251,21 +255,6 @@ impl RegoVM {
};
let result_reg = comprehension_context.result_reg;
// Snapshot the iteration value register BEFORE taking the result
// register: if the comprehension compiler ever allocates
// `result_reg == context.value_reg`, the writeback at the bottom
// of this function would clobber the value register, and a
// post-writeback read here would feed the wrong value into
// `IterationState::Set::current_item`. Only Set needs the snapshot
// (Object uses a self-advancing cursor; Array advances by index).
let set_resume_snapshot = if matches!(
comprehension_context.iteration_state,
Some(IterationState::Set { .. })
) {
Some(self.get_register(comprehension_context.value_reg)?.clone())
} else {
None
};
// Take ownership of the result register so Rc refcount stays at 1,
// allowing Rc::make_mut to mutate in-place instead of deep-cloning.
let mut current_result = self.take_register(result_reg)?;
@@ -303,16 +292,29 @@ impl RegoVM {
self.set_register(result_reg, current_result)?;
if let Some(iter_state) = comprehension_context.iteration_state.as_mut() {
// Set's `Bound::Excluded(current_item)` resume scheme needs the
// pre-mutation snapshot taken at the top of this function.
// Object uses a self-advancing cursor and needs no snapshot.
if let IterationState::Set {
ref mut current_item,
..
} = *iter_state
{
*current_item = set_resume_snapshot;
match *iter_state {
IterationState::Object {
ref mut current_key,
..
} => {
let tracked_key =
if comprehension_context.key_reg != comprehension_context.value_reg {
self.get_register(comprehension_context.key_reg)?.clone()
} else {
self.get_register(comprehension_context.value_reg)?.clone()
};
*current_key = Some(tracked_key);
}
IterationState::Set {
ref mut current_item,
..
} => {
*current_item =
Some(self.get_register(comprehension_context.value_reg)?.clone());
}
IterationState::Array { .. } | IterationState::Single { .. } => {}
}
iter_state.advance();
let has_next = self.setup_next_iteration(
iter_state,
@@ -333,7 +335,6 @@ impl RegoVM {
Ok(())
}
#[allow(clippy::if_then_some_else_none)]
fn execute_comprehension_yield_suspendable(
&mut self,
value_reg: u8,
@@ -358,7 +359,8 @@ impl RegoVM {
result_reg_idx,
key_reg_idx,
value_reg_idx,
iter_is_set,
iteration_key,
iteration_value,
) = {
let frame =
self.execution_stack
@@ -380,8 +382,8 @@ impl RegoVM {
let result_reg_idx = context.result_reg;
let mode = context.mode.clone();
let iter_is_set =
matches!(context.iteration_state, Some(IterationState::Set { .. }));
let iteration_key = self.get_register(context.key_reg)?.clone();
let iteration_value = self.get_register(context.value_reg)?.clone();
(
value_to_add,
@@ -390,7 +392,8 @@ impl RegoVM {
result_reg_idx,
context.key_reg,
context.value_reg,
iter_is_set,
iteration_key,
iteration_value,
)
} else {
return Err(VmError::InvalidIteration {
@@ -400,18 +403,6 @@ impl RegoVM {
}
};
// Snapshot the iteration value register BEFORE the result writeback:
// if the compiler ever allocates `result_reg == value_reg_idx`, a
// post-writeback read would feed the result accumulator into
// `IterationState::Set::current_item`, breaking the next iteration.
// Only Set needs this (Object cursor self-advances; Array advances
// by index).
let set_resume_snapshot = if iter_is_set {
Some(self.get_register(value_reg_idx)?.clone())
} else {
None
};
// Take ownership of the result register so Rc refcount stays at 1,
// allowing Rc::make_mut to mutate in-place instead of deep-cloning.
let mut current_result = self.take_register(result_reg_idx)?;
@@ -459,13 +450,27 @@ impl RegoVM {
} = &mut frame.kind
{
if let Some(iter_state) = context.iteration_state.as_mut() {
if let IterationState::Set {
ref mut current_item,
..
} = *iter_state
{
*current_item = set_resume_snapshot;
match *iter_state {
IterationState::Object {
ref mut current_key,
..
} => {
let tracked_key = if context.key_reg != context.value_reg {
iteration_key.clone()
} else {
iteration_value.clone()
};
*current_key = Some(tracked_key);
}
IterationState::Set {
ref mut current_item,
..
} => {
*current_item = Some(iteration_value.clone());
}
IterationState::Array { .. } | IterationState::Single { .. } => {}
}
iter_state.advance();
}
@@ -482,21 +487,8 @@ impl RegoVM {
}
};
if let Some(mut state) = iteration_state_snapshot {
let has_next = self.setup_next_iteration(&mut state, key_reg_idx, value_reg_idx)?;
// `setup_next_iteration` advances Object's internal cursor; the
// owning frame holds the iteration_state, so we must write the
// updated state back. (The Array/Set variants are also unchanged
// by copy, so the writeback is uniform.)
if let Some(frame) = self.execution_stack.get_mut(comprehension_index) {
if let FrameKind::Comprehension {
ref mut context, ..
} = frame.kind
{
context.iteration_state = Some(state);
}
}
if let Some(state) = iteration_state_snapshot.as_ref() {
let has_next = self.setup_next_iteration(state, key_reg_idx, value_reg_idx)?;
if has_next {
if let Some(frame) = self.execution_stack.get_mut(comprehension_index) {
@@ -536,6 +528,7 @@ impl RegoVM {
Ok(false)
}
}
pub(super) fn handle_comprehension_condition_failure_suspendable(&mut self) -> Result<bool> {
if let Some(mut frame) = self.execution_stack.pop() {
let handled = if let &mut FrameKind::Comprehension {
@@ -561,16 +554,11 @@ impl RegoVM {
context: &mut ComprehensionContext,
) -> Result<()> {
if let Some(iter_state) = context.iteration_state.as_mut() {
// Snapshot the current value into Set's `current_item` so the
// next iteration can resume from `Bound::Excluded(current)`.
// Object uses a self-advancing cursor and needs no snapshot here.
if let IterationState::Set {
ref mut current_item,
..
} = *iter_state
{
*current_item = Some(self.get_register(context.value_reg)?.clone());
}
self.capture_comprehension_iteration_position(
iter_state,
context.key_reg,
context.value_reg,
)?;
iter_state.advance();
let has_next =
self.setup_next_iteration(iter_state, context.key_reg, context.value_reg)?;
@@ -587,10 +575,37 @@ impl RegoVM {
Ok(())
}
fn capture_comprehension_iteration_position(
&mut self,
iter_state: &mut IterationState,
key_reg: u8,
value_reg: u8,
) -> Result<()> {
match *iter_state {
IterationState::Object {
ref mut current_key,
..
} => {
let tracked_key = if key_reg != value_reg {
self.get_register(key_reg)?.clone()
} else {
self.get_register(value_reg)?.clone()
};
*current_key = Some(tracked_key);
}
IterationState::Set {
ref mut current_item,
..
} => {
*current_item = Some(self.get_register(value_reg)?.clone());
}
IterationState::Array { .. } | IterationState::Single { .. } => {}
}
Ok(())
}
fn execute_comprehension_end_run_to_completion(&mut self) -> Result<()> {
// `ComprehensionEnd` is reached from a loaded program; an empty stack
// here means malformed user-supplied bytecode, which must still surface
// as a typed error rather than a panic — including in debug builds.
self.comprehension_stack.pop().map_or_else(
|| {
Err(VmError::InvalidIteration {
+10 -103
View File
@@ -3,9 +3,8 @@
use crate::rvm::instructions::{ComprehensionMode, LoopMode};
use crate::value::Value;
use crate::value::{Object, ObjectCursor};
use crate::Rc;
use alloc::collections::BTreeSet;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
/// Loop execution context for managing iteration state
@@ -25,18 +24,7 @@ pub struct LoopContext {
pub current_iteration_failed: bool, // Track if current iteration had condition failures
}
/// Iterator state for different collection types.
///
/// Snapshot independence for `Object` is provided by the shared
/// `Rc<Object>` — `Rc::make_mut` on an aliased Rc allocates a new
/// collection, leaving the iterator's Rc pointing at the original
/// pre-mutation state. The `ObjectCursor` is opaque and resumes in
/// O(log n) for the BTree backend.
///
/// `Set` continues to use the pre-existing snapshot-by-cloned-key
/// approach (`current_item` + `first_iteration`); migration of `Set`
/// to a cursor-based iterator ships with the `Set` storage abstraction
/// in a follow-up PR.
/// Iterator state for different collection types
#[derive(Debug, Clone)]
pub enum IterationState {
Array {
@@ -44,8 +32,9 @@ pub enum IterationState {
index: usize,
},
Object {
obj: Rc<Object>,
cursor: ObjectCursor,
obj: Rc<BTreeMap<Value, Value>>,
current_key: Option<Value>,
first_iteration: bool,
},
Set {
items: Rc<BTreeSet<Value>>,
@@ -65,21 +54,13 @@ impl IterationState {
pub(super) const fn advance(&mut self) {
match *self {
Self::Array { ref mut index, .. } => {
// Array iteration uses `usize` as the cursor and advances via
// `saturating_add(1)`. A cursor already at `usize::MAX` here
// means a stuck (non-progressing) iteration was emitted by
// malformed bytecode; assert in debug to surface it loudly.
debug_assert!(
*index < usize::MAX,
"IterationState::Array index already at usize::MAX on advance"
);
*index = index.saturating_add(1);
}
// For Object the cursor advances inside `setup_next_iteration`
// when it pulls the next item via `Object::next`, so `advance`
// is a no-op for the cursor-backed Object variant.
Self::Object { .. } => {}
Self::Set {
Self::Object {
ref mut first_iteration,
..
}
| Self::Set {
ref mut first_iteration,
..
} => {
@@ -88,12 +69,6 @@ impl IterationState {
Self::Single {
ref mut consumed, ..
} => {
// `Single` yields exactly once; advancing a consumed Single
// means the compiler emitted a redundant LoopNext.
debug_assert!(
!*consumed,
"IterationState::Single advanced after consumption"
);
*consumed = true;
}
}
@@ -132,71 +107,3 @@ pub(super) struct ComprehensionContext {
/// Resume location for the parent frame once this comprehension completes
pub(super) resume_pc: usize,
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::unreachable,
clippy::pattern_type_mismatch,
clippy::shadow_unrelated,
clippy::panic
)]
mod tests {
use super::*;
use crate::value::Object;
/// IterationState::Object holds an `Rc<Object>` plus an opaque cursor.
/// Mutating an aliased Rc via `Rc::make_mut` allocates a new collection
/// (CoW) so the in-flight iterator's source is unaffected.
#[test]
fn iteration_state_object_is_snapshot_independent_of_source() {
let mut obj = Object::new();
obj.insert(Value::from("a"), Value::from(1));
obj.insert(Value::from("b"), Value::from(2));
obj.insert(Value::from("c"), Value::from(3));
let source = Value::Object(Rc::new(obj));
let snapshot_obj = match &source {
Value::Object(o) => Rc::clone(o),
_ => unreachable!(),
};
let state = IterationState::Object {
obj: Rc::clone(&snapshot_obj),
cursor: snapshot_obj.cursor(),
};
// Mutate a clone of the source mid-iteration.
let mut alias = source.clone();
let inner = alias.as_object_mut().expect("object");
inner.insert(Value::from("a"), Value::from(999));
inner.insert(Value::from("d"), Value::from(4));
inner.remove(&Value::from("b"));
// Drain the snapshot via the cursor — must still report the original
// 3 entries with original values.
let mut collected: Vec<(Value, Value)> = Vec::new();
if let IterationState::Object {
ref obj,
mut cursor,
} = state
{
while let Some((k, v)) = obj.next(&mut cursor) {
collected.push((k.clone(), v.clone()));
}
} else {
unreachable!();
}
assert_eq!(collected.len(), 3);
assert!(collected.contains(&(Value::from("a"), Value::from(1))));
assert!(collected.contains(&(Value::from("b"), Value::from(2))));
assert!(collected.contains(&(Value::from("c"), Value::from(3))));
assert!(!collected.iter().any(|kv| kv.0 == Value::from("d")));
// The original source Value (untouched) is also unchanged.
let src_obj = source.as_object().expect("object");
assert_eq!(src_obj.len(), 3);
assert_eq!(src_obj.get(&Value::from("a")), Some(&Value::from(1)));
}
}
+3 -2
View File
@@ -4,6 +4,7 @@
use crate::rvm::instructions::{GuardMode, Instruction, LiteralOrRegister};
use crate::rvm::program::Program;
use crate::value::Value;
use alloc::collections::BTreeSet;
use alloc::vec::Vec;
use core::mem;
@@ -669,7 +670,7 @@ impl RegoVM {
}
}
SetNew { dest } => {
let empty_set = Value::new_set();
let empty_set = Value::Set(crate::Rc::new(BTreeSet::new()));
self.set_register(dest, empty_set)?;
Ok(InstructionOutcome::Continue)
}
@@ -706,7 +707,7 @@ impl RegoVM {
if any_undefined {
self.set_register(params.dest, Value::Undefined)?;
} else {
let mut set = alloc::collections::BTreeSet::new();
let mut set = BTreeSet::new();
for &reg in params.element_registers() {
set.insert(self.get_register(reg)?.clone());
}
-7
View File
@@ -295,13 +295,6 @@ pub enum VmError {
#[error("Call rule stack underflow during rule finalization (pc={pc})")]
CallRuleStackUnderflow { pc: usize },
#[error("Call rule stack mismatch during rule finalization: expected rule_index {expected}, popped {actual} (pc={pc})")]
CallRuleStackMismatch {
expected: u16,
actual: u16,
pc: usize,
},
#[error("Internal VM error: {message} (pc={pc})")]
Internal { message: String, pc: usize },
}
-14
View File
@@ -117,10 +117,6 @@ impl RegoVM {
let target = self.convert_pc(target, "jump target")?;
self.pc = target;
while self.pc < program.instructions.len() {
// Per-instruction sanity check: every iteration of the dispatch
// loop must re-enter with the VM in a Running/Ready state and the
// working data structures coherent.
self.assert_vm_invariants();
self.memory_check()?;
if self.executed_instructions >= self.max_instructions {
return Err(VmError::InstructionLimitExceeded {
@@ -193,9 +189,6 @@ impl RegoVM {
}
fn execute_suspendable_entry(&mut self, entry_point_pc: usize) -> Result<Value> {
// Precondition: callers (execute_entry_point_by_{index,name}) reset the
// VM before invoking this method, so the VM must be in a clean state.
self.debug_assert_state_is_clean();
self.execution_state = ExecutionState::Running;
self.reset_execution_timer_state();
match self.run_stackless_from(entry_point_pc) {
@@ -208,10 +201,6 @@ impl RegoVM {
}
pub fn resume(&mut self, resume_value: Option<Value>) -> Result<Value> {
// Precondition is enforced below by returning `VmError::InvalidResumeState`
// for any non-`Suspended` state. A `debug_assert!` here would diverge
// debug vs release behavior and, when invoked via FFI, would trip the
// unwind guard and poison the engine on a recoverable misuse.
let (reason, mut last_result) = match self.execution_state.clone() {
ExecutionState::Suspended {
reason,
@@ -300,9 +289,6 @@ impl RegoVM {
fn run_stackless_loop(&mut self, program: &Program, last_result: &mut Value) -> Result<()> {
while !self.execution_stack.is_empty() {
// Per-instruction sanity check: see `assert_vm_invariants` for the
// exact contract. Compiled out in release.
self.assert_vm_invariants();
self.memory_check()?;
self.frame_pc_overridden = false;
let should_finalize_rule = self.execution_stack.last().is_some_and(|frame| {
+62 -67
View File
@@ -3,7 +3,6 @@
use crate::rvm::instructions::LoopMode;
use crate::value::Value;
use crate::Rc;
use super::context::{IterationState, LoopContext};
use super::errors::{Result, VmError};
@@ -90,13 +89,13 @@ impl RegoVM {
) -> Result<()> {
self.set_register(params.result_reg, Value::Bool(false))?;
let mut iteration_state = match self.resolve_iteration_state(mode, &params)? {
let iteration_state = match self.resolve_iteration_state(mode, &params)? {
Some(state) => state,
None => return Ok(()),
};
let has_next =
self.setup_next_iteration(&mut iteration_state, params.key_reg, params.value_reg)?;
self.setup_next_iteration(&iteration_state, params.key_reg, params.value_reg)?;
if !has_next {
self.pc = usize::from(params.loop_end);
return Ok(());
@@ -156,10 +155,15 @@ impl RegoVM {
LoopAction::Continue => {}
}
// Snapshot the current value for Set so its next iteration can resume
// from `Bound::Excluded(current)`. Object uses a cursor and advances
// inside `setup_next_iteration` itself.
if let &mut IterationState::Set {
if let &mut IterationState::Object {
ref mut current_key,
..
} = &mut loop_ctx.iteration_state
{
if loop_ctx.key_reg != loop_ctx.value_reg {
*current_key = Some(self.get_register(loop_ctx.key_reg)?.clone());
}
} else if let &mut IterationState::Set {
ref mut current_item,
..
} = &mut loop_ctx.iteration_state
@@ -169,7 +173,7 @@ impl RegoVM {
loop_ctx.iteration_state.advance();
let has_next = self.setup_next_iteration(
&mut loop_ctx.iteration_state,
&loop_ctx.iteration_state,
loop_ctx.key_reg,
loop_ctx.value_reg,
)?;
@@ -207,13 +211,13 @@ impl RegoVM {
) -> Result<()> {
self.set_register(params.result_reg, Value::Bool(false))?;
let mut iteration_state = match self.resolve_iteration_state(mode, &params)? {
let iteration_state = match self.resolve_iteration_state(mode, &params)? {
Some(state) => state,
None => return Ok(()),
};
let has_next =
self.setup_next_iteration(&mut iteration_state, params.key_reg, params.value_reg)?;
self.setup_next_iteration(&iteration_state, params.key_reg, params.value_reg)?;
if !has_next {
self.pc = usize::from(params.loop_end);
return Ok(());
@@ -312,14 +316,7 @@ impl RegoVM {
Ok(())
}
LoopAction::Continue => {
let (
mode,
success_count,
total_iterations,
key_reg,
value_reg,
mut iteration_state,
) = {
let (mode, success_count, total_iterations, key_reg, value_reg, iteration_state) = {
let (mode, success_count, total_iterations, key_reg, value_reg) = {
let frame = self
.execution_stack
@@ -337,6 +334,11 @@ impl RegoVM {
}
};
let key_value = if key_reg != value_reg {
Some(self.get_register(key_reg)?.clone())
} else {
None
};
let value_value = self.get_register(value_reg)?.clone();
let frame = self
@@ -347,16 +349,20 @@ impl RegoVM {
&mut FrameKind::Loop {
ref mut context, ..
} => {
// Snapshot the current value for Set so its next
// iteration can resume from `Bound::Excluded(current)`.
// Object uses a cursor and advances inside
// `setup_next_iteration` itself.
if let &mut IterationState::Set {
if let &mut IterationState::Object {
ref mut current_key,
..
} = &mut context.iteration_state
{
if context.key_reg != context.value_reg {
*current_key = key_value;
}
} else if let &mut IterationState::Set {
ref mut current_item,
..
} = &mut context.iteration_state
{
*current_item = Some(value_value);
*current_item = Some(value_value.clone());
}
context.iteration_state.advance();
@@ -375,21 +381,7 @@ impl RegoVM {
}
};
let has_next =
self.setup_next_iteration(&mut iteration_state, key_reg, value_reg)?;
// `setup_next_iteration` advances Object's internal cursor;
// the owning frame holds the iteration_state, so we must
// write the updated state back. (Array/Set are unchanged by
// the call, so the writeback is uniform.)
if let Some(frame) = self.execution_stack.last_mut() {
if let FrameKind::Loop {
ref mut context, ..
} = frame.kind
{
context.iteration_state = iteration_state;
}
}
let has_next = self.setup_next_iteration(&iteration_state, key_reg, value_reg)?;
if has_next {
if let Some(frame) = self.execution_stack.last_mut() {
@@ -467,14 +459,10 @@ impl RegoVM {
self.handle_empty_collection(mode, params.result_reg, params.loop_end)?;
return Ok(None);
}
// O(1) resumable cursor over the shared Rc<Object>.
// No eager pair snapshot: avoids O(N) setup, O(N) memory
// floor, and O(N) memory-limit checks. Snapshot
// independence is via the shared Rc (CoW).
let cursor = obj.cursor();
Ok(Some(IterationState::Object {
obj: Rc::clone(obj),
cursor,
obj: obj.clone(),
current_key: None,
first_iteration: true,
}))
}
}
@@ -495,15 +483,8 @@ impl RegoVM {
// over a virtual null element.
Ok(Some(IterationState::Single { consumed: false }))
} else {
// Standard Rego: iterating a non-collection scalar (number,
// string, bool, null, undefined) yields no iterations. For
// `every` this makes the quantifier undefined (it fails) — it
// is NOT vacuously true, which only applies to a genuinely
// empty collection. `any`/`forEach` remain false.
let result = match *mode {
LoopMode::Every => Value::Undefined,
LoopMode::Any | LoopMode::ForEach => Value::Bool(false),
};
// Standard Rego or count/forEach: non-collection → immediate result.
let result = non_collection_result(mode);
self.set_register(params.result_reg, result)?;
self.pc = usize::from(params.loop_end).saturating_sub(1);
Ok(None)
@@ -531,7 +512,7 @@ impl RegoVM {
pub(super) fn setup_next_iteration(
&mut self,
state: &mut IterationState,
state: &IterationState,
key_reg: u8,
value_reg: u8,
) -> Result<bool> {
@@ -557,19 +538,33 @@ impl RegoVM {
}
IterationState::Object {
ref obj,
ref mut cursor,
ref current_key,
ref first_iteration,
} => {
// Object iterates via a resumable cursor on the shared
// `Rc<Object>`; `next` both yields the current entry and
// advances the cursor. No explicit `current_key` snapshot is
// needed — see the doc on `IterationState`.
if let Some((key, value)) = obj.next(cursor) {
let value = value.clone();
if key_reg != value_reg {
self.set_register(key_reg, key.clone())?;
if *first_iteration {
if let Some((key, value)) = obj.iter().next() {
if key_reg != value_reg {
self.set_register(key_reg, key.clone())?;
}
self.set_register(value_reg, value.clone())?;
Ok(true)
} else {
Ok(false)
}
} else if let Some(ref current) = *current_key {
let mut range_iter = obj.range((
core::ops::Bound::Excluded(current),
core::ops::Bound::Unbounded,
));
if let Some((key, value)) = range_iter.next() {
if key_reg != value_reg {
self.set_register(key_reg, key.clone())?;
}
self.set_register(value_reg, value.clone())?;
Ok(true)
} else {
Ok(false)
}
self.set_register(value_reg, value)?;
Ok(true)
} else {
Ok(false)
}
-16
View File
@@ -277,19 +277,6 @@ impl RegoVM {
.call_rule_stack
.pop()
.ok_or(VmError::CallRuleStackUnderflow { pc: self.pc })?;
// Stack discipline: the context we just popped must belong to the
// rule we are finalizing. A mismatch indicates a missing push or an
// extra pop somewhere in this rule's execution and would otherwise
// silently restore the wrong return_pc / rule_type. Surface as a
// typed VmError so the contract holds the same in debug and release
// builds (avoiding FFI poisoning via a debug-only panic).
if rule_index != call_context.rule_index {
return Err(VmError::CallRuleStackMismatch {
expected: rule_index,
actual: call_context.rule_index,
pc: self.pc,
});
}
self.pc = call_context.return_pc;
let result_from_rule = if !rule_failed_due_to_inconsistency {
@@ -844,9 +831,6 @@ impl RegoVM {
self.registers = parent_registers;
// Underflow here means malformed/poisoned program state; surface as a
// typed error rather than a debug-only panic so the public load_program
// contract holds the same in debug and release.
if self.call_rule_stack.pop().is_none() {
return Err(VmError::CallRuleStackUnderflow { pc: self.pc });
}
-133
View File
@@ -34,139 +34,6 @@ impl RegoVM {
// Builtin cache entries only live for a single execution
self.builtins_cache.clear();
// Postcondition: every stack/cache that `reset_execution_state` touches
// must be in its documented "clean" shape. This catches accidental
// omissions in future edits to this function.
self.debug_assert_state_is_clean();
}
/// Debug-only postcondition for `reset_execution_state`.
///
/// Asserts the invariants every caller of `reset_execution_state` relies on
/// before starting a fresh execution. The body is fully gated by
/// `#[cfg(debug_assertions)]` so this is a zero-cost no-op in release.
#[inline]
pub(super) fn debug_assert_state_is_clean(&self) {
#[cfg(debug_assertions)]
{
// --- Stacks: every per-execution stack must be drained. ---
debug_assert!(
self.execution_stack.is_empty(),
"reset_execution_state postcondition: execution_stack must be empty"
);
debug_assert!(
self.loop_stack.is_empty(),
"reset_execution_state postcondition: loop_stack must be empty"
);
debug_assert!(
self.comprehension_stack.is_empty(),
"reset_execution_state postcondition: comprehension_stack must be empty"
);
debug_assert!(
self.call_rule_stack.is_empty(),
"reset_execution_state postcondition: call_rule_stack must be empty"
);
debug_assert!(
self.register_stack.is_empty(),
"reset_execution_state postcondition: register_stack must be empty"
);
// --- Caches: cleared so a new program/input cannot read stale entries. ---
debug_assert!(
self.builtins_cache.is_empty(),
"reset_execution_state postcondition: builtins_cache must be empty"
);
// --- Registers: window resized to the program's base count and zeroed. ---
debug_assert_eq!(
self.registers.len(),
self.base_register_count,
"reset_execution_state postcondition: registers must be sized to base_register_count"
);
debug_assert!(
self.registers.iter().all(|v| matches!(v, Value::Undefined)),
"reset_execution_state postcondition: all registers must be Undefined"
);
// --- Rule cache: sized to the current program and marked uncomputed. ---
debug_assert_eq!(
self.rule_cache.len(),
self.program.rule_infos.len(),
"reset_execution_state postcondition: rule_cache size must match program rule_infos"
);
debug_assert!(
self.rule_cache.iter().all(|entry| !entry.0),
"reset_execution_state postcondition: rule_cache entries must be uncomputed"
);
// --- Counters and execution-state machine: zeroed and back to Ready. ---
debug_assert_eq!(
self.pc, 0,
"reset_execution_state postcondition: pc must be 0"
);
debug_assert_eq!(
self.executed_instructions, 0,
"reset_execution_state postcondition: executed_instructions must be 0"
);
debug_assert!(
matches!(self.execution_state, ExecutionState::Ready),
"reset_execution_state postcondition: execution_state must be Ready"
);
}
}
/// Per-opcode VM invariants checked from the inner dispatch loop.
///
/// These hold every time control re-enters the dispatch loop with another
/// instruction to execute. Only conditions that are *purely VM-internal*
/// (i.e. cannot be made false by any host-supplied program or out-of-order
/// API call) are asserted here — anything reachable from `load_program`
/// input must surface as a typed `VmError` instead, to avoid panicking in
/// debug builds and poisoning the engine across FFI.
///
/// Fully `#[cfg(debug_assertions)]`-gated so the method body compiles out
/// in release.
#[inline]
pub(super) fn assert_vm_invariants(&self) {
#[cfg(debug_assertions)]
{
// The dispatch loop only runs while execution is live. Once the VM
// has transitioned to a terminal state (Suspended/Completed/Error)
// the loop must have exited. Note `Ready` is also valid here because
// some entry points (e.g. `execute_entry_point_by_index` in
// RunToCompletion mode) drive `jump_to` without flipping the state.
// `execution_state` is mutated only inside the VM and is not
// host-controllable.
debug_assert!(
matches!(
self.execution_state,
ExecutionState::Ready | ExecutionState::Running
),
"vm invariant: execution_state must be Ready or Running inside the dispatch loop, was {:?}",
self.execution_state
);
// Rule cache is sized once at reset (against the currently loaded
// program) and the VM does not resize it mid-execution. Any
// mismatch here would indicate an internal accounting bug rather
// than malformed input.
debug_assert_eq!(
self.rule_cache.len(),
self.program.rule_infos.len(),
"vm invariant: rule_cache size must equal program.rule_infos size"
);
// NOTE: `!registers.is_empty()` and an `execution_stack` depth
// ceiling were intentionally *not* asserted here: both can be
// triggered by a host-loaded program (registers via
// `RuleInfo::num_registers == 0`; stack depth via deeply nested
// rules/loops/comprehensions) and would therefore panic in debug
// and poison the engine across FFI. Register access is already
// guarded by `VmError::RegisterIndexOutOfBounds`; runaway recursion
// is bounded in production by `set_max_instructions` and
// `memory_check`.
}
}
/// Return all active objects to their respective pools for reuse
+3 -3
View File
@@ -569,7 +569,7 @@ impl Analyzer {
}
Ok(false)
}
Expr::Array { .. } | Expr::Object { .. } => Ok(true),
Array { .. } | Object { .. } => Ok(true),
_ => Ok(false),
})?;
Ok(true)
@@ -666,7 +666,7 @@ impl Analyzer {
Ok(false)
}
// TODO: key vs value for object binding
Expr::Array { .. } | Expr::Object { .. } => Ok(true),
Array { .. } | Object { .. } => Ok(true),
_ => Ok(false),
})?;
Ok(vars)
@@ -853,7 +853,7 @@ impl Analyzer {
Ok(false)
}
// TODO: Object key/value
Expr::Array { .. } | Expr::Object { .. } => Ok(true),
Array { .. } | Object { .. } => Ok(true),
_ => {
non_vars.push(e.clone());
Ok(false)
+10 -19
View File
@@ -1498,7 +1498,7 @@ fn test_deserialize_object_default_empty_object() {
let s = Schema::from_serde_json_value(schema).unwrap();
match s.as_type() {
Type::Object { default, .. } => {
assert_eq!(default, &Some(Value::new_object()));
assert_eq!(default, &Some(Value::Object(Rc::new(BTreeMap::new()))));
}
_ => panic!("Expected Type::Object"),
}
@@ -1778,11 +1778,8 @@ fn test_deserialize_enum_values_with_object_non_string_keys() {
match s.as_type() {
Type::Enum { values, .. } => match &values[0] {
Value::Object(obj) => {
assert_eq!(*obj.get(&Value::from("1")).expect("1"), Value::from("one"));
assert_eq!(
*obj.get(&Value::from("true")).expect("true"),
Value::from("bool")
);
assert_eq!(obj[&Value::from("1")], Value::from("one"));
assert_eq!(obj[&Value::from("true")], Value::from("bool"));
}
_ => panic!("Expected object in enum values"),
},
@@ -1805,21 +1802,18 @@ fn test_deserialize_enum_values_with_deeply_nested_structures() {
match s.as_type() {
Type::Enum { values, .. } => match &values[0] {
Value::Object(obj) => {
let a = obj.get(&Value::from("a")).expect("a");
let a = &obj[&Value::from("a")];
match a {
Value::Array(arr) => match &arr[0] {
Value::Object(inner) => {
let b = inner.get(&Value::from("b")).expect("b");
let b = &inner[&Value::from("b")];
match b {
Value::Array(barr) => {
assert_eq!(barr[0], Value::from(1));
assert_eq!(barr[1], Value::from(2));
match &barr[2] {
Value::Object(cobj) => {
assert_eq!(
*cobj.get(&Value::from("c")).expect("c"),
Value::Null
);
assert_eq!(cobj[&Value::from("c")], Value::Null);
}
_ => panic!("Expected object for 'c'"),
}
@@ -1892,11 +1886,8 @@ fn test_deserialize_const_value_object() {
match s.as_type() {
Type::Const { value, .. } => match value {
Value::Object(ref obj) => {
assert_eq!(
*obj.get(&Value::from("foo")).expect("foo"),
Value::from("bar")
);
assert_eq!(*obj.get(&Value::from("baz")).expect("baz"), Value::from(1));
assert_eq!(obj[&Value::from("foo")], Value::from("bar"));
assert_eq!(obj[&Value::from("baz")], Value::from(1));
}
_ => panic!("Expected object for const value"),
},
@@ -1949,13 +1940,13 @@ fn test_deserialize_const_value_deeply_nested() {
match s.as_type() {
Type::Const { value, .. } => match value {
Value::Object(ref obj) => {
let a = obj.get(&Value::from("a")).expect("a");
let a = &obj[&Value::from("a")];
match a {
Value::Array(arr) => {
assert_eq!(arr[0], Value::from(1));
match &arr[1] {
Value::Object(inner) => {
let b = inner.get(&Value::from("b")).expect("b");
let b = &inner[&Value::from("b")];
match b {
Value::Array(barr) => {
assert_eq!(barr[0], Value::Null);
+2 -3
View File
@@ -7,7 +7,6 @@
use crate::{
schema::{error::ValidationError, Schema, Type},
value::Object,
*,
};
use alloc::collections::BTreeMap;
@@ -538,7 +537,7 @@ impl SchemaValidator {
}
fn validate_discriminated_subobject_with_base(
object_value: &Object,
object_value: &BTreeMap<Value, Value>,
discriminated_subobject: &crate::schema::DiscriminatedSubobject,
base_properties: &BTreeMap<String, Schema>,
base_additional_properties: Option<&Schema>,
@@ -654,7 +653,7 @@ impl SchemaValidator {
}
fn validate_subobject(
object_value: &Object,
object_value: &BTreeMap<Value, Value>,
subobject: &crate::schema::Subobject,
path: &str,
) -> Result<(), ValidationError> {
-444
View File
@@ -24,7 +24,6 @@ use anyhow::{bail, Result};
use core::num::NonZeroU32;
use core::time::Duration;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use test_generator::test_resources;
use timer_test_support::{
apply_engine_timer, configure_time_source, reset_time_source, GlobalTimerGuard,
@@ -819,446 +818,3 @@ fn test_get_data() -> Result<()> {
Ok(())
}
#[test]
fn test_add_data_deep_merge() -> Result<()> {
let mut engine = Engine::new();
// Nested objects under a shared top-level key are deep-merged, not replaced.
engine.add_data(Value::from_json_str(r#"{ "a" : { "x" : 1 } }"#)?)?;
engine.add_data(Value::from_json_str(r#"{ "a" : { "y" : 2 } }"#)?)?;
assert_eq!(
engine.get_data(),
Value::from_json_str(r#"{ "a" : { "x" : 1, "y" : 2 } }"#)?
);
Ok(())
}
#[test]
fn test_add_data_deep_merge_multi_level() -> Result<()> {
let mut engine = Engine::new();
// Merging recurses through multiple levels of nesting.
engine.add_data(Value::from_json_str(
r#"{ "a" : { "b" : { "x" : 1 } }, "top" : 0 }"#,
)?)?;
engine.add_data(Value::from_json_str(
r#"{ "a" : { "b" : { "y" : 2 }, "c" : 3 } }"#,
)?)?;
assert_eq!(
engine.get_data(),
Value::from_json_str(r#"{ "a" : { "b" : { "x" : 1, "y" : 2 }, "c" : 3 }, "top" : 0 }"#)?
);
Ok(())
}
#[test]
fn test_add_data_leaf_conflict_errors() -> Result<()> {
let mut engine = Engine::new();
// A genuine leaf conflict (same nested path, different value) is an error.
engine.add_data(Value::from_json_str(r#"{ "a" : { "x" : 1 } }"#)?)?;
assert!(engine
.add_data(Value::from_json_str(r#"{ "a" : { "x" : 2 } }"#)?)
.is_err());
Ok(())
}
#[test]
fn test_add_data_object_vs_scalar_conflict_errors() -> Result<()> {
let mut engine = Engine::new();
// An object cannot be merged with a scalar at the same path.
engine.add_data(Value::from_json_str(r#"{ "a" : { "x" : 1 } }"#)?)?;
assert!(engine
.add_data(Value::from_json_str(r#"{ "a" : 5 }"#)?)
.is_err());
Ok(())
}
#[test]
fn test_add_data_equal_leaf_is_noop() -> Result<()> {
let mut engine = Engine::new();
// Re-adding identical data (including equal nested leaves) is tolerated as a no-op.
engine.add_data(Value::from_json_str(r#"{ "a" : { "x" : 1 } }"#)?)?;
engine.add_data(Value::from_json_str(r#"{ "a" : { "x" : 1 }, "b" : 2 }"#)?)?;
assert_eq!(
engine.get_data(),
Value::from_json_str(r#"{ "a" : { "x" : 1 }, "b" : 2 }"#)?
);
Ok(())
}
#[test]
fn test_add_data_set_union() -> Result<()> {
let mut engine = Engine::new();
// Sets under a shared key are unioned rather than conflicting (consistent with the
// rule-evaluation merge, where partial set rules accumulate elements). JSON cannot express
// sets, so the data documents are built via the `Value` API.
engine.add_data(Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64), Value::from(2_u64)])),
)])))?;
engine.add_data(Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(2_u64), Value::from(3_u64)])),
)])))?;
let expected = Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([
Value::from(1_u64),
Value::from(2_u64),
Value::from(3_u64),
])),
)]));
assert_eq!(engine.get_data(), expected);
Ok(())
}
#[test]
fn test_add_data_nested_set_union() -> Result<()> {
let mut engine = Engine::new();
// A set nested under an object key exercises the recursive merge: the outer objects are
// deep-merged and the inner sets are then unioned.
engine.add_data(Value::from(BTreeMap::from([(
Value::from("a"),
Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64)])),
)])),
)])))?;
engine.add_data(Value::from(BTreeMap::from([(
Value::from("a"),
Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(2_u64)])),
)])),
)])))?;
let expected = Value::from(BTreeMap::from([(
Value::from("a"),
Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64), Value::from(2_u64)])),
)])),
)]));
assert_eq!(engine.get_data(), expected);
Ok(())
}
#[test]
fn test_add_data_equal_set_is_noop() -> Result<()> {
let mut engine = Engine::new();
// Re-adding an identical set is tolerated as a no-op (not a conflict).
engine.add_data(Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64), Value::from(2_u64)])),
)])))?;
engine.add_data(Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64), Value::from(2_u64)])),
)])))?;
let expected = Value::from(BTreeMap::from([(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64), Value::from(2_u64)])),
)]));
assert_eq!(engine.get_data(), expected);
Ok(())
}
#[test]
fn test_add_data_failed_merge_is_atomic() -> Result<()> {
let mut engine = Engine::new();
engine.add_data(Value::from_json_str(r#"{ "a" : { "z" : 1 } }"#)?)?;
// Mixes a new key `m` with a conflicting leaf `z` (1 vs 3). Because `m` sorts
// before `z`, a naive in-place merge would insert `m` and only then hit the `z`
// conflict. add_data must be all-or-nothing: the whole call fails AND leaves the
// existing data untouched — `m` must not leak in.
assert!(engine
.add_data(Value::from_json_str(r#"{ "a" : { "m" : 2, "z" : 3 } }"#)?)
.is_err());
assert_eq!(
engine.get_data(),
Value::from_json_str(r#"{ "a" : { "z" : 1 } }"#)?
);
Ok(())
}
#[test]
fn test_add_data_failed_set_merge_is_atomic() -> Result<()> {
let mut engine = Engine::new();
// Existing data: a set `s` alongside a scalar `z` under `a`.
engine.add_data(Value::from(BTreeMap::from([(
Value::from("a"),
Value::from(BTreeMap::from([
(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64), Value::from(2_u64)])),
),
(Value::from("z"), Value::from(1_u64)),
])),
)])))?;
// This add would union `s` with {3} but conflicts on `z` (1 vs 2). Since `s`
// sorts before `z`, a naive in-place merge would union the set *before* failing
// on `z`, leaking {3} into `s`. The atomic add must reject the whole call and
// leave `s` as {1, 2}.
assert!(engine
.add_data(Value::from(BTreeMap::from([(
Value::from("a"),
Value::from(BTreeMap::from([
(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(3_u64)])),
),
(Value::from("z"), Value::from(2_u64)),
])),
)])))
.is_err());
// `s` must be unchanged ({1, 2}, not {1, 2, 3}) and `z` must still be 1.
let expected = Value::from(BTreeMap::from([(
Value::from("a"),
Value::from(BTreeMap::from([
(
Value::from("s"),
Value::from(BTreeSet::from([Value::from(1_u64), Value::from(2_u64)])),
),
(Value::from("z"), Value::from(1_u64)),
])),
)]));
assert_eq!(engine.get_data(), expected);
Ok(())
}
#[test]
fn test_add_data_failed_array_merge_is_atomic() -> Result<()> {
let mut engine = Engine::new();
engine.add_data(Value::from_json_str(r#"{ "a" : { "arr" : [1, 2] } }"#)?)?;
// Arrays are atomic leaves (never element-merged), so a differing array at the
// same path is a conflict. The new key `aa` sorts before `arr`, so a naive
// in-place merge would insert `aa` and only then hit the `arr` conflict. add_data
// must reject the whole call and leave the data untouched — `aa` must not leak in.
assert!(engine
.add_data(Value::from_json_str(
r#"{ "a" : { "aa" : 5, "arr" : [3] } }"#
)?)
.is_err());
assert_eq!(
engine.get_data(),
Value::from_json_str(r#"{ "a" : { "arr" : [1, 2] } }"#)?
);
Ok(())
}
// The `Value::merge` used by `add_data` is shared with the rule-evaluation path
// (`Interpreter::merge_rule_value`, reached via `with data.* as ...` and rule-value
// materialization). The tests below pin down that making `merge` recursive changed only the
// data-document semantics and left rule evaluation — in particular the `with data.* as ...`
// modifier — behaving exactly as before (an override, never a deep merge).
#[test]
fn test_with_data_modifier_replaces_nested_object() -> Result<()> {
let mut engine = Engine::new();
// Base data provides a nested object with two keys.
engine.add_data(Value::from_json_str(
r#"{ "base" : { "foo" : { "a" : 1, "b" : 2 } } }"#,
)?)?;
engine.add_policy(
"policy.rego".to_string(),
r#"
package test
result := x if {
x := data.base.foo with data.base.foo as {"a": 99}
}
"#
.to_string(),
)?;
// `with data.base.foo as {"a": 99}` REPLACES the whole subtree for the duration of the
// rule; it must NOT deep-merge with the base `{ "a": 1, "b": 2 }`. So `b` is gone.
assert_eq!(
engine
.eval_query("data.test.result".to_string(), false)?
.result[0]
.expressions[0]
.value
.clone(),
Value::from_json_str(r#"{ "a" : 99 }"#)?
);
Ok(())
}
#[test]
fn test_with_data_modifier_replaces_whole_subtree() -> Result<()> {
let mut engine = Engine::new();
engine.add_data(Value::from_json_str(
r#"{ "base" : { "foo" : 1, "bar" : 2 } }"#,
)?)?;
engine.add_policy(
"policy.rego".to_string(),
r#"
package test
result := x if {
x := data.base with data.base as {"only": 3}
}
"#
.to_string(),
)?;
// `with data.base as {...}` replaces the entire `data.base` object; the original
// `foo`/`bar` keys are not merged in.
assert_eq!(
engine
.eval_query("data.test.result".to_string(), false)?
.result[0]
.expressions[0]
.value
.clone(),
Value::from_json_str(r#"{ "only" : 3 }"#)?
);
Ok(())
}
#[test]
fn test_with_data_modifier_nested_replace_preserves_siblings() -> Result<()> {
let mut engine = Engine::new();
// `data.base` has a nested `foo` object AND a sibling `bar`.
engine.add_data(Value::from_json_str(
r#"{ "base" : { "foo" : { "a" : 1, "b" : 2 }, "bar" : 7 } }"#,
)?)?;
engine.add_policy(
"policy.rego".to_string(),
r#"
package test
# `with` targets the nested `data.base.foo`, but the rule observes the PARENT `data.base`.
result := x if {
x := data.base with data.base.foo as {"a": 99}
}
"#
.to_string(),
)?;
// The nested `foo` is deep-replaced (its `b` is gone — `with` never merges), while the
// sibling `bar` under the same parent is preserved.
assert_eq!(
engine
.eval_query("data.test.result".to_string(), false)?
.result[0]
.expressions[0]
.value
.clone(),
Value::from_json_str(r#"{ "foo" : { "a" : 99 }, "bar" : 7 }"#)?
);
Ok(())
}
#[test]
fn test_rule_reads_deep_merged_base_data() -> Result<()> {
let mut engine = Engine::new();
// Two add_data calls deep-merge into a single nested object...
engine.add_data(Value::from_json_str(
r#"{ "base" : { "foo" : { "a" : 1 } } }"#,
)?)?;
engine.add_data(Value::from_json_str(
r#"{ "base" : { "foo" : { "b" : 2 } } }"#,
)?)?;
engine.add_policy(
"policy.rego".to_string(),
r#"
package test
a := data.base.foo.a
b := data.base.foo.b
"#
.to_string(),
)?;
// ...and both merged leaves are visible to rule evaluation.
assert_eq!(
engine.eval_query("data.test".to_string(), false)?.result[0].expressions[0]
.value
.clone(),
Value::from_json_str(r#"{ "a" : 1, "b" : 2 }"#)?
);
Ok(())
}
#[test]
fn test_rule_values_coexist_with_merged_base_data() -> Result<()> {
let mut engine = Engine::new();
// Deep-merged base data under `base`...
engine.add_data(Value::from_json_str(
r#"{ "base" : { "foo" : { "a" : 1 } } }"#,
)?)?;
engine.add_data(Value::from_json_str(
r#"{ "base" : { "foo" : { "b" : 2 } } }"#,
)?)?;
engine.add_policy(
"policy.rego".to_string(),
r#"
package test
computed := data.base.foo.a + data.base.foo.b
"#
.to_string(),
)?;
let data = engine.eval_query("data".to_string(), false)?.result[0].expressions[0]
.value
.clone();
// Base data is preserved and deep-merged...
assert_eq!(
data["base"],
Value::from_json_str(r#"{ "foo" : { "a" : 1, "b" : 2 } }"#)?
);
// ...and the rule-computed value materializes alongside it without disturbing the merge.
assert_eq!(data["test"]["computed"], Value::from(3_u64));
Ok(())
}
+36 -202
View File
@@ -11,24 +11,6 @@
clippy::as_conversions
)] // value helpers index paths directly for performance
mod object;
mod set;
#[cfg(test)]
mod tests;
#[allow(unused_imports)] // surface for downstream PRs
pub use object::{IntoIter, Iter, IterMut, Object};
#[allow(unused_imports)] // surface for downstream PRs
pub use set::Set;
#[cfg(feature = "rvm")]
#[allow(unused_imports)] // surface for downstream PRs
pub use object::ObjectCursor;
#[cfg(feature = "rvm")]
#[allow(unused_imports)] // surface for downstream PRs
pub use set::SetCursor;
use crate::number::Number;
use alloc::collections::{BTreeMap, BTreeSet};
@@ -41,7 +23,7 @@ use core::str::FromStr;
use anyhow::{anyhow, bail, Result};
use serde::de::{self, Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor};
use serde::ser::Serializer;
use serde::ser::{SerializeMap, Serializer};
use serde::{Deserialize, Serialize};
use crate::*;
@@ -81,7 +63,7 @@ pub enum Value {
/// An object.
/// Unlike JSON, keys can be any value, not just string.
Object(Rc<Object>),
Object(Rc<BTreeMap<Value, Value>>),
/// Undefined value.
/// Used to indicate the absence of a value.
@@ -104,15 +86,26 @@ impl Serialize for Value {
where
S: Serializer,
{
use serde::ser::Error;
match self {
Value::Null => serializer.serialize_unit(),
Value::Bool(b) => serializer.serialize_bool(*b),
Value::String(s) => serializer.serialize_str(s.as_ref()),
Value::Number(n) => n.serialize(serializer),
Value::Array(a) => a.serialize(serializer),
// Delegate to the Object/Set serializers — single canonical path,
// handles non-string-key stringification internally.
Value::Object(fields) => fields.serialize(serializer),
Value::Object(fields) => {
let mut map = serializer.serialize_map(Some(fields.len()))?;
for (k, v) in fields.iter() {
match k {
Value::String(_) => map.serialize_entry(k, v)?,
_ => {
let key_str = serde_json::to_string(k).map_err(Error::custom)?;
map.serialize_entry(&key_str, v)?
}
}
}
map.end()
}
// display set as an array
Value::Set(s) => s.serialize(serializer),
@@ -352,7 +345,7 @@ impl Value {
/// assert_eq!(array[4], Value::from(12345u64));
/// let obj = array[5].as_object().expect("not an object");
/// assert_eq!(obj.len(), 1);
/// assert_eq!(obj.get(&Value::from("name")).expect("missing name"), &Value::from("regorus"));
/// assert_eq!(obj[&Value::from("name")], Value::from("regorus"));
/// # Ok(())
/// # }
/// ```
@@ -807,7 +800,7 @@ impl From<BTreeMap<Value, Value>> for Value {
/// # Ok(())
/// # }
fn from(s: BTreeMap<Value, Value>) -> Self {
Value::Object(Rc::new(Object::from(s)))
Value::Object(Rc::new(s))
}
}
@@ -1286,16 +1279,16 @@ impl Value {
}
}
/// Cast value to [`&Object`] if [`Value::Object`].
/// Cast value to [`& BTreeMap<Value, Value>`] if [`Value::Object`].
/// ```
/// # use regorus::*;
/// # use regorus::value::Object;
/// # use std::collections::BTreeMap;
/// # fn main() -> anyhow::Result<()> {
/// let v = Value::from(
/// [(Value::from("Hello"), Value::from("World"))]
/// .iter()
/// .cloned()
/// .collect::<Object>(),
/// .collect::<BTreeMap<Value, Value>>(),
/// );
/// assert_eq!(
/// v.as_object()?.iter().next(),
@@ -1303,28 +1296,28 @@ impl Value {
/// );
/// # Ok(())
/// # }
pub fn as_object(&self) -> Result<&Object> {
pub fn as_object(&self) -> Result<&BTreeMap<Value, Value>> {
match self {
Value::Object(m) => Ok(m),
_ => Err(anyhow!("not an object")),
}
}
/// Cast value to [`&mut Object`] if [`Value::Object`].
/// Cast value to [`&mut BTreeMap<Value, Value>`] if [`Value::Object`].
/// ```
/// # use regorus::*;
/// # use regorus::value::Object;
/// # use std::collections::BTreeMap;
/// # fn main() -> anyhow::Result<()> {
/// let mut v = Value::from(
/// [(Value::from("Hello"), Value::from("World"))]
/// .iter()
/// .cloned()
/// .collect::<Object>(),
/// .collect::<BTreeMap<Value, Value>>(),
/// );
/// v.as_object_mut()?.insert(Value::from("Good"), Value::from("Bye"));
/// # Ok(())
/// # }
pub fn as_object_mut(&mut self) -> Result<&mut Object> {
pub fn as_object_mut(&mut self) -> Result<&mut BTreeMap<Value, Value>> {
match self {
Value::Object(m) => Ok(Rc::make_mut(m)),
_ => Err(anyhow!("not an object")),
@@ -1332,13 +1325,6 @@ impl Value {
}
}
/// Depth cap for `deep_merge`/`check_mergeable`, set at serde_json's default recursion limit.
///
/// Prevents a stack overflow from adversarially nested data — an uncatchable abort that poisons
/// every engine in an FFI process. At serde_json's limit it only backstops `Value`s built without
/// a parse-time cap: the Python/Ruby native bindings, or programmatic construction.
const MAX_MERGE_DEPTH: usize = 128;
impl Value {
pub(crate) fn make_or_get_value_mut<'a>(&'a mut self, paths: &[&str]) -> Result<&'a mut Value> {
if paths.is_empty() {
@@ -1372,11 +1358,6 @@ impl Value {
}
}
/// Shallow-merge `new` into `self` with strict rule-output semantics.
///
/// Objects merge one level deep: a key on both sides must hold the *same* value or it is a
/// conflict; sets union; equal values are a no-op. Non-recursive by design — data documents
/// use [`Value::deep_merge`] instead.
pub(crate) fn merge(&mut self, mut new: Value) -> Result<()> {
if self == &new {
return Ok(());
@@ -1384,180 +1365,33 @@ impl Value {
match (self, &mut new) {
(v @ Value::Undefined, _) => *v = new,
(Value::Set(ref mut set), Value::Set(new)) => {
// Union without deep-cloning the RHS set (see `deep_merge`).
let dst = Rc::make_mut(set);
match Rc::try_unwrap(core::mem::take(new)) {
Ok(owned) => dst.extend(owned),
Err(shared) => dst.extend(shared.iter().cloned()),
}
Rc::make_mut(set).append(Rc::make_mut(new));
// Enforce allocator limit after merging set entries.
enforce_limit_anyhow()?;
}
(Value::Object(map), Value::Object(new)) => {
for (k, v) in new.iter() {
match map.get(k) {
// Same key, different value: the rule produced two outputs for one path.
Some(pv) if *pv != *v => bail!(
"value for key `{}` generated multiple times: `{}` and `{}`",
serde_json::to_string_pretty(&k).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&pv).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&v).map_err(anyhow::Error::msg)?,
),
_ => {
Rc::make_mut(map).insert(k.clone(), v.clone());
enforce_limit_anyhow()?;
}
};
}
}
_ => bail!("error: could not merge value"),
};
Ok(())
}
/// Recursively deep-merge `new` into `self` — the data-document merge behind [`Engine::add_data`].
///
/// Objects recurse per-key, sets union, equal values are a no-op, any other differing pair
/// conflicts. Set-union is a regorus extension (OPA data is JSON, which has no sets). Distinct
/// from the strict, non-recursive [`Value::merge`] used for rule outputs — use deep-merge ONLY
/// for data documents.
///
/// [`Engine::add_data`]: crate::Engine::add_data
pub(crate) fn deep_merge(&mut self, new: Value) -> Result<()> {
self.deep_merge_at(new, 0)
}
/// Depth-tracked worker for [`deep_merge`](Value::deep_merge). See [`MAX_MERGE_DEPTH`].
fn deep_merge_at(&mut self, mut new: Value, depth: usize) -> Result<()> {
if depth >= MAX_MERGE_DEPTH {
bail!("data merge exceeds maximum nesting depth of {MAX_MERGE_DEPTH}");
}
if self == &new {
return Ok(());
}
match (self, &mut new) {
(v @ Value::Undefined, _) => *v = new,
(Value::Set(ref mut set), Value::Set(new)) => {
// Union without deep-cloning the RHS set: move elements if uniquely owned,
// else clone only the element handles (`Rc` bumps), never the whole `BTreeSet`.
let dst = Rc::make_mut(set);
match Rc::try_unwrap(core::mem::take(new)) {
Ok(owned) => dst.extend(owned),
Err(shared) => dst.extend(shared.iter().cloned()),
}
enforce_limit_anyhow()?;
}
(Value::Object(map), Value::Object(new)) => {
// What each incoming key requires of the target map. Decided from a read-only
// probe so a no-op or a conflict never triggers `Rc::make_mut` (and never clones
// a shared map); `make_mut` is taken lazily, only when a key actually mutates.
enum Step {
Skip,
Insert,
Recurse,
Conflict,
}
for (k, v) in new.iter() {
let step = match map.get(k) {
None => Step::Insert,
Some(existing) if existing == v => Step::Skip,
Some(existing)
if matches!(
(existing, v),
(Value::Object(_), Value::Object(_))
| (Value::Set(_), Value::Set(_))
) =>
{
Step::Recurse
}
Some(_) => Step::Conflict,
};
match step {
Step::Skip => {}
Step::Insert => {
Rc::make_mut(map).insert(k.clone(), v.clone());
enforce_limit_anyhow()?;
}
// Both sides are containers: recurse so nested objects merge rather than
// the subtree being replaced (OPA data-merge semantics).
Step::Recurse => {
let existing = Rc::make_mut(map).get_mut(k).ok_or_else(|| {
anyhow!("internal error: key vanished during merge")
})?;
existing.deep_merge_at(v.clone(), depth.saturating_add(1))?;
}
Step::Conflict => {
let existing = map.get(k).ok_or_else(|| {
anyhow!("internal error: key vanished during merge")
})?;
Some(pv) if *pv != *v => {
bail!(
"value for key `{}` generated multiple times: `{}` and `{}`",
serde_json::to_string_pretty(&k).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&existing)
.map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&pv).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&v).map_err(anyhow::Error::msg)?,
)
}
}
_ => {
Rc::make_mut(map).insert(k.clone(), v.clone());
// Enforce allocator limit after merging object entries.
enforce_limit_anyhow()?;
}
};
}
}
_ => bail!("error: could not merge value"),
};
Ok(())
}
/// Read-only check that [`deep_merge`](Value::deep_merge)-ing `other` into `self` would not
/// conflict, without mutating or allocating.
///
/// Lets [`Engine::add_data`] validate before merging in place. Since a conflict is the only
/// way the default-build merge can fail and it depends only on the inputs, a passing scan
/// guarantees the in-place `deep_merge` won't fail — avoiding the alternative of cloning the
/// whole document into a candidate just to validate. Only overlapping keys are walked, so
/// disjoint additions are near-free.
///
/// [`Engine::add_data`]: crate::Engine::add_data
#[cfg(not(feature = "allocator-memory-limits"))]
pub(crate) fn check_mergeable(&self, other: &Value) -> Result<()> {
self.check_mergeable_at(other, 0)
}
/// Depth-tracked worker for [`check_mergeable`](Value::check_mergeable). See [`MAX_MERGE_DEPTH`].
#[cfg(not(feature = "allocator-memory-limits"))]
fn check_mergeable_at(&self, other: &Value, depth: usize) -> Result<()> {
if depth >= MAX_MERGE_DEPTH {
bail!("data merge exceeds maximum nesting depth of {MAX_MERGE_DEPTH}");
}
if self == other {
return Ok(());
}
match (self, other) {
(Value::Undefined, _) => Ok(()),
// Set union never conflicts.
(Value::Set(_), Value::Set(_)) => Ok(()),
(Value::Object(dst), Value::Object(src)) => {
for (k, sv) in src.iter() {
// Only overlapping keys can conflict.
if let Some(dv) = dst.get(k) {
let both_mergeable = matches!(
(dv, sv),
(Value::Object(_), Value::Object(_)) | (Value::Set(_), Value::Set(_))
);
if both_mergeable {
dv.check_mergeable_at(sv, depth.saturating_add(1))?;
} else if dv != sv {
bail!(
"value for key `{}` generated multiple times: `{}` and `{}`",
serde_json::to_string_pretty(&k).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&dv).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&sv).map_err(anyhow::Error::msg)?,
)
}
}
}
Ok(())
}
_ => bail!("error: could not merge value"),
}
}
}
impl ops::Index<&Value> for Value {
-148
View File
@@ -1,148 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Opaque iterator types for [`Object`].
//!
//! These newtypes wrap the storage backend's iterators so the backend can be
//! swapped without changing any iterator type signatures observed by callers.
use alloc::collections::btree_map;
use core::iter::FusedIterator;
use super::Object;
use crate::value::Value;
/// Owned iterator over `(Value, Value)` entries.
#[derive(Debug)]
pub struct IntoIter {
pub(super) inner: btree_map::IntoIter<Value, Value>,
}
impl Iterator for IntoIter {
type Item = (Value, Value);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl DoubleEndedIterator for IntoIter {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl ExactSizeIterator for IntoIter {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl FusedIterator for IntoIter {}
/// Borrowed iterator over `(&Value, &Value)` entries.
#[derive(Debug, Clone)]
pub struct Iter<'a> {
pub(super) inner: btree_map::Iter<'a, Value, Value>,
}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a Value, &'a Value);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> DoubleEndedIterator for Iter<'a> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl<'a> ExactSizeIterator for Iter<'a> {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl<'a> FusedIterator for Iter<'a> {}
/// Borrowed iterator over `(&Value, &mut Value)` entries.
#[derive(Debug)]
pub struct IterMut<'a> {
pub(super) inner: btree_map::IterMut<'a, Value, Value>,
}
impl<'a> Iterator for IterMut<'a> {
type Item = (&'a Value, &'a mut Value);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> DoubleEndedIterator for IterMut<'a> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl<'a> ExactSizeIterator for IterMut<'a> {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl<'a> FusedIterator for IterMut<'a> {}
impl IntoIterator for Object {
type Item = (Value, Value);
type IntoIter = IntoIter;
#[inline]
fn into_iter(self) -> Self::IntoIter {
IntoIter {
inner: self.inner.into_iter(),
}
}
}
impl<'a> IntoIterator for &'a Object {
type Item = (&'a Value, &'a Value);
type IntoIter = Iter<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
Iter {
inner: self.inner.iter(),
}
}
}
impl<'a> IntoIterator for &'a mut Object {
type Item = (&'a Value, &'a mut Value);
type IntoIter = IterMut<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
IterMut {
inner: self.inner.iter_mut(),
}
}
}
-265
View File
@@ -1,265 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! See [`Object`].
mod iter;
mod serde;
use alloc::collections::BTreeMap;
use core::cmp::Ordering;
use core::fmt;
use core::ops::Bound;
use crate::value::Value;
pub use iter::{IntoIter, Iter, IterMut};
/// Opaque, ordered key-value map keyed by [`Value`].
///
/// The current backing storage is `BTreeMap<Value, Value>`. The inner field
/// is private so the representation can change (two-tier inline+hash, lazy,
/// schema-shared) without touching call sites.
///
/// # Iteration
///
/// - [`Object::iter`] — implementation-defined order; non-resumable.
/// - [`Object::iter_sorted`] — sorted by `Value::Ord`; non-resumable.
/// - [`Object::cursor`] / [`Object::next`] — implementation-defined order,
/// resumable; cheapest per-step cost. Used by interpreter/RVM when iteration
/// must yield mid-flight.
#[derive(Default, Clone, Eq, PartialEq)]
pub struct Object {
inner: BTreeMap<Value, Value>,
}
impl Object {
/// Create an empty `Object`.
#[inline]
pub const fn new() -> Self {
Self {
inner: BTreeMap::new(),
}
}
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[inline]
pub fn get(&self, key: &Value) -> Option<&Value> {
self.inner.get(key)
}
#[inline]
pub fn contains_key(&self, key: &Value) -> bool {
self.inner.contains_key(key)
}
#[inline]
pub fn get_mut(&mut self, key: &Value) -> Option<&mut Value> {
self.inner.get_mut(key)
}
/// Iteration in implementation-defined order. Non-resumable.
///
/// For the current BTree-backed storage this happens to be sorted, but
/// callers MUST NOT depend on that. Use [`Object::iter_sorted`] when
/// deterministic order is required, or [`Object::cursor`] when iteration
/// must yield and resume.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&Value, &Value)> + '_ {
self.inner.iter()
}
/// Iteration in sorted key order (by `Value::Ord`). Non-resumable.
///
/// Use this for serialization, snapshots, hashing, `Debug`, the
/// `object.keys` builtin, etc.
#[inline]
pub fn iter_sorted(&self) -> Iter<'_> {
// BTree backend iterates sorted natively.
Iter {
inner: self.inner.iter(),
}
}
#[inline]
pub fn keys(&self) -> impl Iterator<Item = &Value> + '_ {
self.inner.keys()
}
/// Keys in sorted order (by `Value::Ord`). Symmetric with
/// [`Object::iter_sorted`].
#[inline]
pub fn keys_sorted(&self) -> impl Iterator<Item = &Value> + '_ {
self.iter_sorted().map(|(k, _)| k)
}
#[inline]
pub fn values(&self) -> impl Iterator<Item = &Value> + '_ {
self.inner.values()
}
#[inline]
pub fn iter_mut(&mut self) -> IterMut<'_> {
IterMut {
inner: self.inner.iter_mut(),
}
}
/// Insert a key-value pair. Returns the previous value if any.
#[inline]
pub fn insert(&mut self, key: Value, value: Value) -> Option<Value> {
self.inner.insert(key, value)
}
#[inline]
pub fn remove(&mut self, key: &Value) -> Option<Value> {
self.inner.remove(key)
}
#[inline]
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&Value, &mut Value) -> bool,
{
self.inner.retain(f);
}
#[inline]
pub fn clear(&mut self) {
self.inner.clear();
}
#[inline]
pub fn append(&mut self, other: &mut Object) {
self.inner.append(&mut other.inner);
}
/// Gets a mutable reference to the value associated with `key`, inserting
/// the result of `default()` if absent. Single O(log n) probe.
pub fn get_or_insert_with<F: FnOnce() -> Value>(
&mut self,
key: Value,
default: F,
) -> &mut Value {
self.inner.entry(key).or_insert_with(default)
}
/// Wrap into a `Value::Object`.
#[inline]
pub fn into_value(self) -> Value {
Value::Object(crate::Rc::new(self))
}
/// Create a resumable cursor over entries in implementation-defined
/// order. Stable for the lifetime of `&self`. O(1).
///
/// The cursor is fully self-owned (it stores a clone of the last-seen
/// key, not a reference) so it can be stored as a field of a
/// long-lived state struct — e.g. an RVM iteration frame that persists
/// across instruction dispatches. As a consequence, mutating the
/// `Object` between `next()` calls is not rejected by the borrow
/// checker; the resulting iteration order in that case is unspecified.
#[inline]
pub const fn cursor(&self) -> ObjectCursor {
ObjectCursor {
inner: ObjectCursorInner::BTree(None),
}
}
/// Advance `cursor` and yield the next entry. O(log n) for the BTree
/// backend (range probe); future hash/inline variants may be O(1).
pub fn next<'a>(&'a self, cursor: &mut ObjectCursor) -> Option<(&'a Value, &'a Value)> {
let ObjectCursorInner::BTree(ref mut last) = cursor.inner;
let next = last.as_ref().map_or_else(
|| self.inner.iter().next(),
|prev| {
self.inner
.range((Bound::Excluded(prev.clone()), Bound::Unbounded))
.next()
},
);
let (k, v) = next?;
*last = Some(k.clone());
Some((k, v))
}
}
/// Opaque resumable cursor over an [`Object`]'s entries in
/// implementation-defined order.
///
/// Self-owned: holds no borrow on the `Object`, so it can be stored as a
/// field of a long-lived state struct (e.g. an RVM iteration frame).
#[derive(Debug, Clone)]
pub struct ObjectCursor {
inner: ObjectCursorInner,
}
#[derive(Debug, Clone)]
enum ObjectCursorInner {
/// BTree backend cursor: tracks last-seen key. `None` means "before start".
BTree(Option<Value>),
}
// ---- Hand-written Ord/PartialOrd ----------------------------------------
//
// Implemented in terms of `iter_sorted()` so ordering is consistent with the
// canonical (sorted) view of the entries and is therefore independent of
// the storage variant.
impl Ord for Object {
fn cmp(&self, other: &Self) -> Ordering {
self.iter_sorted().cmp(other.iter_sorted())
}
}
impl PartialOrd for Object {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Debug for Object {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Use sorted iteration so Debug output is stable across storage
// variants.
f.debug_map().entries(self.iter_sorted()).finish()
}
}
impl Extend<(Value, Value)> for Object {
fn extend<I: IntoIterator<Item = (Value, Value)>>(&mut self, iter: I) {
self.inner.extend(iter);
}
}
impl FromIterator<(Value, Value)> for Object {
fn from_iter<I: IntoIterator<Item = (Value, Value)>>(iter: I) -> Self {
Self {
inner: BTreeMap::from_iter(iter),
}
}
}
impl From<BTreeMap<Value, Value>> for Object {
#[inline]
fn from(map: BTreeMap<Value, Value>) -> Self {
Self { inner: map }
}
}
impl From<Object> for Value {
#[inline]
fn from(o: Object) -> Self {
o.into_value()
}
}
-59
View File
@@ -1,59 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Serde `Serialize`/`Deserialize` impls for [`Object`].
use alloc::string::ToString as _;
use core::fmt;
use serde::de::{Deserialize, Deserializer, Error as _, MapAccess, Visitor};
use serde::ser::{Serialize, SerializeMap as _, Serializer};
use super::Object;
use crate::value::Value;
impl Serialize for Object {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::Error;
let mut map = serializer.serialize_map(Some(self.len()))?;
// Sorted iteration: canonical JSON.
for (k, v) in self.iter_sorted() {
match *k {
Value::String(_) => map.serialize_entry(k, v)?,
_ => {
// Non-string keys are stringified via serde_json::to_string
// so the resulting JSON has valid string keys.
let key_str = serde_json::to_string(k).map_err(Error::custom)?;
map.serialize_entry(&key_str, v)?;
}
}
}
map.end()
}
}
struct ObjectVisitor;
impl<'de> Visitor<'de> for ObjectVisitor {
type Value = Object;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a map of Value to Value")
}
fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
let mut obj = Object::new();
while let Some((k, v)) = access.next_entry::<Value, Value>()? {
obj.insert(k, v);
crate::utils::limits::check_memory_limit_if_needed()
.map_err(|err| A::Error::custom(err.to_string()))?;
}
Ok(obj)
}
}
impl<'de> Deserialize<'de> for Object {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_map(ObjectVisitor)
}
}
-103
View File
@@ -1,103 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Opaque iterator types for [`Set`].
//!
//! These newtypes wrap the storage backend's iterators so the backend can be
//! swapped without changing any iterator type signatures observed by callers.
use alloc::collections::btree_set;
use core::iter::FusedIterator;
use super::Set;
use crate::value::Value;
/// Owned iterator over `Value` elements.
#[derive(Debug)]
pub struct IntoIter {
pub(super) inner: btree_set::IntoIter<Value>,
}
impl Iterator for IntoIter {
type Item = Value;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl DoubleEndedIterator for IntoIter {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl ExactSizeIterator for IntoIter {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl FusedIterator for IntoIter {}
/// Borrowed iterator over `&Value` elements.
#[derive(Debug, Clone)]
pub struct Iter<'a> {
pub(super) inner: btree_set::Iter<'a, Value>,
}
impl<'a> Iterator for Iter<'a> {
type Item = &'a Value;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> DoubleEndedIterator for Iter<'a> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl<'a> ExactSizeIterator for Iter<'a> {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl<'a> FusedIterator for Iter<'a> {}
impl IntoIterator for Set {
type Item = Value;
type IntoIter = IntoIter;
#[inline]
fn into_iter(self) -> Self::IntoIter {
IntoIter {
inner: self.inner.into_iter(),
}
}
}
impl<'a> IntoIterator for &'a Set {
type Item = &'a Value;
type IntoIter = Iter<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
Iter {
inner: self.inner.iter(),
}
}
}
-269
View File
@@ -1,269 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! See [`Set`].
mod iter;
mod serde;
use alloc::collections::BTreeSet;
use core::cmp::Ordering;
use core::fmt;
use core::ops::Bound;
use crate::value::Value;
#[allow(unused_imports)] // surface for downstream PRs
pub use iter::{IntoIter, Iter};
/// Opaque, ordered set of [`Value`]s.
///
/// The current backing storage is `BTreeSet<Value>`. The inner field is
/// private so the representation can change (hash-backed, lazy, bloom-fronted,
/// FFI-backed) without touching call sites.
///
/// # Iteration
///
/// - [`Set::iter`] — implementation-defined order; non-resumable.
/// - [`Set::iter_sorted`] — sorted by `Value::Ord`; non-resumable.
/// - [`Set::cursor`] / [`Set::next`] — implementation-defined order,
/// resumable; cheapest per-step cost. Used by interpreter/RVM when iteration
/// must yield mid-flight.
#[derive(Default, Clone, Eq, PartialEq)]
pub struct Set {
inner: BTreeSet<Value>,
}
impl Set {
/// Create an empty `Set`.
#[inline]
pub const fn new() -> Self {
Self {
inner: BTreeSet::new(),
}
}
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[inline]
pub fn contains(&self, value: &Value) -> bool {
self.inner.contains(value)
}
#[inline]
pub fn get(&self, value: &Value) -> Option<&Value> {
self.inner.get(value)
}
/// First element in sorted order (by `Value::Ord`).
#[inline]
pub fn first(&self) -> Option<&Value> {
self.iter_sorted().next()
}
/// Last element in sorted order (by `Value::Ord`).
#[inline]
pub fn last(&self) -> Option<&Value> {
self.iter_sorted().next_back()
}
/// Iteration in implementation-defined order. Non-resumable.
///
/// For the current BTree-backed storage this happens to be sorted, but
/// callers MUST NOT depend on that. Use [`Set::iter_sorted`] when
/// deterministic order is required, or [`Set::cursor`] when iteration
/// must yield and resume.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &Value> + '_ {
self.inner.iter()
}
/// Iteration in sorted order (by `Value::Ord`). Non-resumable.
///
/// Use this for serialization, snapshots, hashing, `Debug`, etc.
#[inline]
pub fn iter_sorted(&self) -> Iter<'_> {
// BTree backend iterates sorted natively.
Iter {
inner: self.inner.iter(),
}
}
/// Insert `value`. Returns `true` if the value was newly inserted.
#[inline]
pub fn insert(&mut self, value: Value) -> bool {
self.inner.insert(value)
}
#[inline]
pub fn remove(&mut self, value: &Value) -> bool {
self.inner.remove(value)
}
#[inline]
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&Value) -> bool,
{
self.inner.retain(f);
}
#[inline]
pub fn clear(&mut self) {
self.inner.clear();
}
#[inline]
pub fn append(&mut self, other: &mut Set) {
self.inner.append(&mut other.inner);
}
/// Set intersection. Returns a new `Set` containing the elements
/// present in both `self` and `other`.
pub fn intersection(&self, other: &Set) -> Set {
Set {
inner: self.inner.intersection(&other.inner).cloned().collect(),
}
}
/// Set union. Returns a new `Set` containing the elements present in
/// either `self` or `other`.
pub fn union(&self, other: &Set) -> Set {
Set {
inner: self.inner.union(&other.inner).cloned().collect(),
}
}
/// Set difference. Returns a new `Set` containing the elements present
/// in `self` but not in `other`.
pub fn difference(&self, other: &Set) -> Set {
Set {
inner: self.inner.difference(&other.inner).cloned().collect(),
}
}
#[inline]
pub fn is_subset(&self, other: &Set) -> bool {
self.inner.is_subset(&other.inner)
}
/// Wrap into a `Value::Set`.
#[inline]
pub fn into_value(self) -> Value {
Value::Set(crate::Rc::new(self.inner))
}
/// Create a resumable cursor over elements in implementation-defined
/// order. Stable for the lifetime of `&self`. O(1).
///
/// The cursor is fully self-owned (it stores a clone of the last-seen
/// element, not a reference) so it can be stored as a field of a
/// long-lived state struct — e.g. an RVM iteration frame that persists
/// across instruction dispatches. As a consequence, mutating the `Set`
/// between `next()` calls is not rejected by the borrow checker; the
/// resulting iteration order in that case is unspecified.
#[inline]
pub const fn cursor(&self) -> SetCursor {
SetCursor {
inner: SetCursorInner::BTree(None),
}
}
/// Advance `cursor` and yield the next element. O(log n) for the BTree
/// backend (range probe); future hash/inline variants may be O(1).
pub fn next<'a>(&'a self, cursor: &mut SetCursor) -> Option<&'a Value> {
let SetCursorInner::BTree(ref mut last) = cursor.inner;
let next = last.as_ref().map_or_else(
|| self.inner.iter().next(),
|prev| {
// `(Bound<&T>, Bound<&T>)` impls `RangeBounds<T>` — no clone
// needed to build the resume bound.
self.inner
.range((Bound::Excluded(prev), Bound::Unbounded))
.next()
},
);
let v = next?;
*last = Some(v.clone());
Some(v)
}
}
/// Opaque resumable cursor over a [`Set`]'s elements in
/// implementation-defined order.
///
/// Self-owned: holds no borrow on the `Set`, so it can be stored as a
/// field of a long-lived state struct (e.g. an RVM iteration frame).
#[derive(Debug, Clone)]
pub struct SetCursor {
inner: SetCursorInner,
}
#[derive(Debug, Clone)]
enum SetCursorInner {
/// BTree backend cursor: tracks last-seen element. `None` means "before start".
BTree(Option<Value>),
}
// ---- Hand-written Ord/PartialOrd ----------------------------------------
//
// Implemented in terms of `iter_sorted()` so ordering is consistent with the
// canonical (sorted) view of the elements and is therefore independent of
// the storage variant.
impl Ord for Set {
fn cmp(&self, other: &Self) -> Ordering {
self.iter_sorted().cmp(other.iter_sorted())
}
}
impl PartialOrd for Set {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Debug for Set {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Use sorted iteration so Debug output is stable across storage
// variants.
f.debug_set().entries(self.iter_sorted()).finish()
}
}
impl Extend<Value> for Set {
fn extend<I: IntoIterator<Item = Value>>(&mut self, iter: I) {
self.inner.extend(iter);
}
}
impl FromIterator<Value> for Set {
fn from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Self {
Self {
inner: BTreeSet::from_iter(iter),
}
}
}
impl From<BTreeSet<Value>> for Set {
#[inline]
fn from(set: BTreeSet<Value>) -> Self {
Self { inner: set }
}
}
impl From<Set> for Value {
#[inline]
fn from(s: Set) -> Self {
s.into_value()
}
}
-44
View File
@@ -1,44 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Serde `Serialize`/`Deserialize` impls for [`Set`].
use core::fmt;
use serde::de::{Deserialize, Deserializer, Error as _, SeqAccess, Visitor};
use serde::ser::{Serialize, Serializer};
use super::Set;
use crate::value::Value;
impl Serialize for Set {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// Sets serialize as JSON arrays. Sorted iteration: canonical output.
serializer.collect_seq(self.iter_sorted())
}
}
struct SetVisitor;
impl<'de> Visitor<'de> for SetVisitor {
type Value = Set;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a sequence of Values")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
let mut set = Set::new();
while let Some(v) = access.next_element::<Value>()? {
set.insert(v);
crate::utils::limits::check_memory_limit_if_needed().map_err(A::Error::custom)?;
}
Ok(set)
}
}
impl<'de> Deserialize<'de> for Set {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_seq(SetVisitor)
}
}
-868
View File
@@ -1,868 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(
clippy::panic,
clippy::expect_used,
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::as_conversions,
clippy::arithmetic_side_effects,
clippy::unseparated_literal_suffix,
clippy::map_unwrap_or,
clippy::option_if_let_else,
clippy::pattern_type_mismatch
)]
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::vec::Vec;
use super::{Object, Set};
use crate::value::Value;
fn val(i: u64) -> Value {
Value::from(i)
}
fn make_pairs(n: u64) -> Vec<(Value, Value)> {
(0..n).map(|i| (val(i), val(i.saturating_mul(2)))).collect()
}
const SIZES: &[u64] = &[0, 1, 2, 4, 8, 64, 256, 1024];
/// `iter_sorted` must yield entries in the same order as a `BTreeMap` oracle.
#[test]
fn object_iter_sorted_matches_btreemap_oracle() {
for &n in SIZES {
let pairs = make_pairs(n);
let oracle: BTreeMap<Value, Value> = pairs.iter().cloned().collect();
let obj: Object = pairs.into_iter().collect();
let actual: Vec<(&Value, &Value)> = obj.iter_sorted().collect();
let expected: Vec<(&Value, &Value)> = oracle.iter().collect();
assert_eq!(actual, expected, "size {n}");
}
}
/// `iter` may be in any order, but as a multiset must equal the oracle's entries.
#[test]
fn object_iter_multiset_equality_with_oracle() {
for &n in SIZES {
let pairs = make_pairs(n);
let oracle: BTreeMap<Value, Value> = pairs.iter().cloned().collect();
let obj: Object = pairs.into_iter().collect();
assert_eq!(obj.len(), oracle.len(), "size {n}");
let mut a: Vec<(Value, Value)> = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
let mut b: Vec<(Value, Value)> =
oracle.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
a.sort();
b.sort();
assert_eq!(a, b);
}
}
/// Serialize-then-deserialize must round-trip through JSON without loss.
#[test]
fn object_serde_roundtrip() {
for &n in &[0_u64, 1, 8, 64] {
let pairs: Vec<(Value, Value)> = (0..n)
.map(|i| (Value::String(format!("k{i}").into()), val(i)))
.collect();
let obj: Object = pairs.into_iter().collect();
let json = serde_json::to_string(&obj).expect("ser");
let back: Object = serde_json::from_str(&json).expect("de");
assert_eq!(obj, back, "size {n}");
}
}
/// Equality depends only on contents, not the order keys were inserted.
#[test]
fn object_eq_invariant_to_insertion_order() {
let mut a = Object::new();
let mut b = Object::new();
for i in 0..32_u64 {
a.insert(val(i), val(i.saturating_add(1)));
}
for i in (0..32_u64).rev() {
b.insert(val(i), val(i.saturating_add(1)));
}
assert_eq!(a, b);
}
/// `remove` returns the prior value (or `None`) and `retain` keeps only matching entries.
#[test]
fn object_remove_and_retain() {
let mut obj: Object = make_pairs(16).into_iter().collect();
assert_eq!(obj.remove(&val(0)), Some(val(0)));
assert!(obj.remove(&val(100)).is_none());
obj.retain(|_, v| {
if let Value::Number(ref n) = *v {
n.as_u64().is_some_and(|x| x % 4 == 0)
} else {
false
}
});
for (_, v) in obj.iter_sorted() {
if let Value::Number(ref n) = *v {
assert_eq!(n.as_u64().expect("u64") % 4, 0);
}
}
}
/// `IntoIterator` for `Object` (by value) yields every entry exactly once.
#[test]
fn object_into_iterator_owned() {
let obj: Object = make_pairs(8).into_iter().collect();
let collected: Vec<(Value, Value)> = obj.into_iter().collect();
assert_eq!(collected.len(), 8);
}
// ---- Duplicate-key semantics --------------------------------------------
/// `FromIterator` keeps the last value when the same key appears multiple times.
#[test]
fn object_from_iter_last_wins_on_duplicate_keys() {
let obj = Object::from_iter([(val(0), val(1)), (val(0), val(2))]);
assert_eq!(obj.get(&val(0)), Some(&val(2)));
assert_eq!(obj.len(), 1);
}
/// `From<BTreeMap>` adopts `BTreeMap`'s own last-write-wins semantics for duplicates.
#[test]
fn object_from_btreemap_last_wins_on_duplicate_keys() {
let mut bm: BTreeMap<Value, Value> = BTreeMap::new();
bm.insert(val(0), val(1));
bm.insert(val(0), val(2));
let obj: Object = bm.into();
assert_eq!(obj.get(&val(0)), Some(&val(2)));
assert_eq!(obj.len(), 1);
}
// ---- get_or_insert_with --------------------------------------------------
/// `get_or_insert_with` inserts the default when the key is absent and returns a mutable ref to it.
#[test]
fn object_get_or_insert_with_inserts_when_absent() {
let mut obj = Object::new();
let v = obj.get_or_insert_with(val(7), || val(42));
assert_eq!(*v, val(42));
*v = val(43);
assert_eq!(obj.get(&val(7)), Some(&val(43)));
}
/// `get_or_insert_with` returns the existing value and never invokes the default closure.
#[test]
fn object_get_or_insert_with_returns_existing_when_present() {
let mut obj = Object::new();
obj.insert(val(7), val(1));
let mut closure_called = false;
let v = obj.get_or_insert_with(val(7), || {
closure_called = true;
val(999)
});
assert_eq!(*v, val(1));
assert!(!closure_called, "default closure must not run when present");
}
// ---- Accessor coverage ---------------------------------------------------
/// Smoke-test every accessor: `contains_key`/`get`/`get_mut`/`keys`/`values`/`iter`/`iter_mut`/`append`/`clear`.
#[test]
fn object_accessor_coverage() {
let mut obj: Object = make_pairs(4).into_iter().collect();
assert!(obj.contains_key(&val(0)));
assert!(!obj.contains_key(&val(100)));
assert_eq!(obj.get(&val(2)), Some(&val(4)));
if let Some(v) = obj.get_mut(&val(1)) {
*v = val(999);
}
assert_eq!(obj.get(&val(1)), Some(&val(999)));
let keys: Vec<&Value> = obj.keys().collect();
assert_eq!(keys.len(), 4);
let values: Vec<&Value> = obj.values().collect();
assert_eq!(values.len(), 4);
for (_, v) in obj.iter_mut() {
*v = val(0);
}
for (_, v) in obj.iter() {
assert_eq!(*v, val(0));
}
let mut other = Object::new();
other.insert(val(100), val(200));
obj.append(&mut other);
assert!(other.is_empty());
assert!(obj.contains_key(&val(100)));
obj.clear();
assert!(obj.is_empty());
}
// ---- IntoIterator for references -----------------------------------------
/// `IntoIterator` for `&Object` yields shared refs to every entry.
#[test]
fn object_into_iterator_ref() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut count = 0;
for (_k, _v) in &obj {
count += 1;
}
assert_eq!(count, 4);
}
/// `IntoIterator` for `&mut Object` exposes mutable refs to values; mutations persist.
#[test]
fn object_into_iterator_ref_mut() {
let mut obj: Object = make_pairs(4).into_iter().collect();
for (_k, v) in &mut obj {
*v = val(0);
}
for (_, v) in obj.iter() {
assert_eq!(*v, val(0));
}
}
// ---- Cursor tests --------------------------------------------------------
/// Driving `cursor`+`next` to completion visits each entry exactly once.
#[test]
fn object_cursor_yields_every_entry_once() {
for &n in SIZES {
let pairs = make_pairs(n);
let obj: Object = pairs.clone().into_iter().collect();
let mut cursor = obj.cursor();
let mut collected: Vec<(Value, Value)> = Vec::new();
while let Some((k, v)) = obj.next(&mut cursor) {
collected.push((k.clone(), v.clone()));
}
let mut a = collected;
a.sort();
let mut b = pairs;
b.sort();
assert_eq!(a, b, "size {n}");
}
}
/// A freshly-constructed cursor restarts from the beginning, independent of any prior cursor's state.
#[test]
fn object_cursor_resumable_fresh_cursor_restarts() {
let obj: Object = make_pairs(8).into_iter().collect();
let mut c1 = obj.cursor();
let _ = obj.next(&mut c1);
let _ = obj.next(&mut c1);
let mut c2 = obj.cursor();
let first_again = obj.next(&mut c2);
let first_original = obj.iter().next();
assert_eq!(
first_again.map(|(k, v)| (k.clone(), v.clone())),
first_original.map(|(k, v)| (k.clone(), v.clone()))
);
}
/// When `Object` is shared via `Rc`, `Rc::make_mut` clones — leaving an in-flight cursor on the original snapshot unaffected.
#[test]
fn object_cursor_snapshot_independence_via_rc() {
use crate::Rc;
let mut obj = Object::new();
obj.insert(Value::from("a"), Value::from(1));
obj.insert(Value::from("b"), Value::from(2));
obj.insert(Value::from("c"), Value::from(3));
let rc_obj = Rc::new(obj);
let alias = Rc::clone(&rc_obj);
let mut cursor = rc_obj.cursor();
let _ = rc_obj.next(&mut cursor);
let mut alias_for_mut = alias;
Rc::make_mut(&mut alias_for_mut).insert(Value::from("d"), Value::from(4));
Rc::make_mut(&mut alias_for_mut).remove(&Value::from("a"));
assert_eq!(rc_obj.len(), 3);
let mut remaining = 0;
while rc_obj.next(&mut cursor).is_some() {
remaining += 1;
}
assert_eq!(remaining, 2);
}
/// A cursor over an empty `Object` returns `None` on the first call.
#[test]
fn object_cursor_empty_returns_none_immediately() {
let obj = Object::new();
let mut cursor = obj.cursor();
assert!(obj.next(&mut cursor).is_none());
}
/// Mutating an `Object` between `next()` calls is well-defined: the cursor
/// must not panic and must terminate. The visit order, and whether
/// inserted/removed keys appear, is intentionally unspecified — this test
/// only pins the safety + termination guarantees that callers (e.g. a
/// future RVM iteration frame) may rely on. It must NOT assert any
/// particular order or count, or future backend swaps will be forced to
/// honor an accidental contract.
#[test]
fn object_cursor_mutation_between_steps_is_safe_and_terminates() {
let mut obj: Object = make_pairs(16).into_iter().collect();
let mut cursor = obj.cursor();
// Yield a few entries before mutating.
for _ in 0..3 {
let _ = obj.next(&mut cursor);
}
// Interleave mutations and steps. Each yielded entry must, at the
// moment of yield, be a real entry in the map.
obj.insert(val(100), val(100));
if let Some((k, v)) = obj.next(&mut cursor) {
assert_eq!(obj.get(k), Some(v));
}
obj.remove(&val(2));
if let Some((k, v)) = obj.next(&mut cursor) {
assert_eq!(obj.get(k), Some(v));
}
obj.clear();
// After clear(), draining the cursor must terminate (not panic, not
// loop) within a bounded number of calls.
let mut terminated = false;
for _ in 0..32 {
if obj.next(&mut cursor).is_none() {
terminated = true;
break;
}
}
assert!(terminated, "cursor failed to terminate after clear()");
}
// ---- Hand-written Ord consistency ---------------------------------------
/// `Ord` (built atop `iter_sorted`) is invariant to insertion order.
#[test]
fn object_ord_invariant_to_insertion_order() {
let mut a = Object::new();
let mut b = Object::new();
for i in 0..16_u64 {
a.insert(val(i), val(i.saturating_add(1)));
}
for i in (0..16_u64).rev() {
b.insert(val(i), val(i.saturating_add(1)));
}
use core::cmp::Ordering;
assert_eq!(a.cmp(&b), Ordering::Equal);
}
/// `Ord` agrees with lexicographic comparison of the sorted-entries view.
#[test]
fn object_ord_lexicographic_on_sorted_entries() {
let a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
let b: Object = [(val(0), val(0)), (val(2), val(2))].into_iter().collect();
assert!(a < b);
}
/// `empty < non_empty` and a shorter prefix compares less than its extension.
#[test]
fn object_ord_empty_and_prefix() {
use core::cmp::Ordering;
let empty = Object::new();
let one: Object = [(val(0), val(0))].into_iter().collect();
let two: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
assert_eq!(empty.cmp(&one), Ordering::Less);
assert_eq!(one.cmp(&two), Ordering::Less);
assert_eq!(two.cmp(&empty), Ordering::Greater);
}
/// When keys match, `Ord` falls through to comparing values.
#[test]
fn object_ord_breaks_ties_on_values() {
use core::cmp::Ordering;
let a: Object = [(val(0), val(1))].into_iter().collect();
let b: Object = [(val(0), val(2))].into_iter().collect();
assert_eq!(a.cmp(&b), Ordering::Less);
}
/// `PartialOrd` must agree with `Ord` for every input pair.
#[test]
fn object_partial_cmp_matches_cmp() {
let a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
let b: Object = [(val(0), val(0)), (val(2), val(2))].into_iter().collect();
assert_eq!(a.partial_cmp(&b), Some(a.cmp(&b)));
assert_eq!(b.partial_cmp(&a), Some(b.cmp(&a)));
assert_eq!(a.partial_cmp(&a), Some(core::cmp::Ordering::Equal));
}
// ---- Debug / keys_sorted determinism ------------------------------------
/// `Debug` output is byte-identical for equal Objects regardless of insertion order.
#[test]
fn object_debug_invariant_to_insertion_order() {
let mut a = Object::new();
let mut b = Object::new();
for i in 0..8_u64 {
a.insert(val(i), val(i));
}
for i in (0..8_u64).rev() {
b.insert(val(i), val(i));
}
assert_eq!(format!("{a:?}"), format!("{b:?}"));
}
/// `keys_sorted` yields exactly `iter_sorted().map(|(k,_)| k)`.
#[test]
fn object_keys_sorted_matches_iter_sorted_keys() {
let obj: Object = make_pairs(16).into_iter().collect();
let from_keys: Vec<&Value> = obj.keys_sorted().collect();
let from_iter: Vec<&Value> = obj.iter_sorted().map(|(k, _)| k).collect();
assert_eq!(from_keys, from_iter);
}
// ---- Serde: non-string keys & determinism --------------------------------
/// `Serialize` stringifies non-string keys, and equal Objects produce identical JSON
/// regardless of insertion order.
#[test]
fn object_serialize_non_string_keys_and_deterministic() {
let pairs = [
(Value::from("alpha"), val(1)),
(Value::Bool(true), val(2)),
(val(7), val(3)),
];
let a: Object = pairs.iter().cloned().collect();
let mut b = Object::new();
for (k, v) in pairs.iter().rev().cloned() {
b.insert(k, v);
}
let ja = serde_json::to_string(&a).expect("ser a");
let jb = serde_json::to_string(&b).expect("ser b");
assert_eq!(ja, jb, "serialization must be deterministic");
// Non-string keys appear as quoted strings in the resulting JSON.
let v: serde_json::Value = serde_json::from_str(&ja).expect("parse");
let obj = v.as_object().expect("json object");
assert!(
obj.contains_key("true"),
"bool key was not stringified: {ja}"
);
assert!(
obj.contains_key("7"),
"number key was not stringified: {ja}"
);
assert!(obj.contains_key("alpha"));
}
// ---- Extend / append duplicate-key semantics -----------------------------
/// `extend` overwrites existing entries (last-write-wins) and preserves length when
/// only existing keys are touched.
#[test]
fn object_extend_last_wins_and_empty_noop() {
let mut obj: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
obj.extend([(val(0), val(99))]);
assert_eq!(obj.get(&val(0)), Some(&val(99)));
assert_eq!(obj.len(), 2);
let before = obj.len();
obj.extend(core::iter::empty::<(Value, Value)>());
assert_eq!(obj.len(), before, "empty extend is a no-op");
}
/// `append` drains `other` into `self`, overwriting on overlapping keys.
#[test]
fn object_append_overlapping_keys_drain_and_overwrite() {
let mut a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
let mut b: Object = [(val(1), val(99)), (val(2), val(2))].into_iter().collect();
a.append(&mut b);
assert!(b.is_empty(), "append must drain `other`");
assert_eq!(a.len(), 3);
assert_eq!(a.get(&val(1)), Some(&val(99)));
assert_eq!(a.get(&val(2)), Some(&val(2)));
}
// ---- Iterator trait surface ---------------------------------------------
/// `DoubleEndedIterator`/`ExactSizeIterator`/`FusedIterator` and `size_hint` all
/// behave correctly across partial consumption from both ends.
#[test]
fn object_iter_sorted_double_ended_and_exact_size() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut it = obj.iter_sorted();
assert_eq!(it.len(), 4);
assert_eq!(it.size_hint(), (4, Some(4)));
let first = it.next().expect("front");
let last = it.next_back().expect("back");
assert_eq!(it.len(), 2);
assert_eq!(it.size_hint(), (2, Some(2)));
assert_ne!(first.0, last.0, "front and back must differ for n=4");
// Drain remaining.
while it.next().is_some() {}
assert_eq!(it.len(), 0);
// FusedIterator: stays None after exhaustion.
assert!(it.next().is_none());
assert!(it.next().is_none());
assert!(it.next_back().is_none());
}
/// `IntoIter` also honors `DoubleEndedIterator` and `ExactSizeIterator`.
#[test]
fn object_into_iter_double_ended_and_exact_size() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut it = obj.into_iter();
assert_eq!(it.len(), 4);
let _ = it.next().expect("front");
let _ = it.next_back().expect("back");
assert_eq!(it.len(), 2);
let collected: Vec<_> = it.collect();
assert_eq!(collected.len(), 2);
}
/// `IterMut` decrements its `len()` after consuming from the front.
#[test]
fn object_iter_mut_exact_size() {
let mut obj: Object = make_pairs(3).into_iter().collect();
let mut it = obj.iter_mut();
assert_eq!(it.len(), 3);
let _ = it.next().expect("front");
assert_eq!(it.len(), 2);
}
/// `Iter` is `Clone`; the clone iterates independently from the same point.
#[test]
fn object_iter_sorted_clone_is_independent() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut a = obj.iter_sorted();
let _ = a.next();
let b = a.clone();
let rest_a: Vec<_> = a.collect();
let rest_b: Vec<_> = b.collect();
assert_eq!(rest_a, rest_b);
}
// ---- default / insert ---------------------------------------------------
/// `Object::default()` and `Object::new()` produce equal, empty Objects.
#[test]
fn object_default_equals_new_and_is_empty() {
let a = Object::default();
let b = Object::new();
assert_eq!(a, b);
assert!(a.is_empty());
assert_eq!(a.len(), 0);
}
/// `insert` returns `None` for a fresh key and `Some(old)` when overwriting.
#[test]
fn object_insert_returns_previous_value() {
let mut obj = Object::new();
assert_eq!(obj.insert(val(0), val(1)), None);
assert_eq!(obj.insert(val(0), val(2)), Some(val(1)));
assert_eq!(obj.get(&val(0)), Some(&val(2)));
}
// =========================================================================
// Set tests
// =========================================================================
const SET_SIZES: &[u64] = &[0, 1, 2, 4, 8, 64, 256, 1024];
#[test]
fn set_iter_sorted_matches_btreeset_oracle() {
for &n in SET_SIZES {
let values: Vec<Value> = (0..n).map(val).collect();
let oracle: BTreeSet<Value> = values.iter().cloned().collect();
let s: Set = values.into_iter().collect();
let actual: Vec<&Value> = s.iter_sorted().collect();
let expected: Vec<&Value> = oracle.iter().collect();
assert_eq!(actual, expected, "size {n}");
}
}
#[test]
fn set_iter_multiset_equality_with_oracle() {
for &n in SET_SIZES {
let values: Vec<Value> = (0..n).map(val).collect();
let oracle: BTreeSet<Value> = values.iter().cloned().collect();
let s: Set = values.into_iter().collect();
let mut a: Vec<Value> = s.iter().cloned().collect();
let mut b: Vec<Value> = oracle.iter().cloned().collect();
a.sort();
b.sort();
assert_eq!(a, b);
}
}
#[test]
fn set_algebra_matches_btreeset() {
let a_vals: Vec<Value> = (0..32_u64).map(val).collect();
let b_vals: Vec<Value> = (16..48_u64).map(val).collect();
let a_btree: BTreeSet<Value> = a_vals.iter().cloned().collect();
let b_btree: BTreeSet<Value> = b_vals.iter().cloned().collect();
let a: Set = a_vals.into_iter().collect();
let b: Set = b_vals.into_iter().collect();
fn sorted<'a, I: Iterator<Item = &'a Value>>(it: I) -> Vec<&'a Value> {
let mut v: Vec<&Value> = it.collect();
v.sort();
v
}
let inter_set = a.intersection(&b);
assert_eq!(
sorted(inter_set.iter_sorted()),
sorted(a_btree.intersection(&b_btree))
);
let union_set = a.union(&b);
assert_eq!(
sorted(union_set.iter_sorted()),
sorted(a_btree.union(&b_btree))
);
let diff_set = a.difference(&b);
assert_eq!(
sorted(diff_set.iter_sorted()),
sorted(a_btree.difference(&b_btree))
);
// Subset: trivial + non-trivial cases.
let proper_subset: Set = (0..16_u64).map(val).collect();
let non_subset: Set = (30..50_u64).map(val).collect();
assert!(a.is_subset(&a));
assert!(proper_subset.is_subset(&a));
assert!(!non_subset.is_subset(&a));
}
#[test]
fn set_first_last() {
let s: Set = (0..16_u64).map(val).collect();
assert_eq!(s.first(), Some(&val(0)));
assert_eq!(s.last(), Some(&val(15)));
assert!(Set::new().first().is_none());
}
#[test]
fn set_serde_roundtrip() {
for &n in &[0_u64, 1, 8, 64] {
let s: Set = (0..n).map(val).collect();
let json = serde_json::to_string(&s).expect("ser");
let back: Set = serde_json::from_str(&json).expect("de");
assert_eq!(s, back, "size {n}");
}
}
#[test]
fn set_append_drains_other() {
let mut a: Set = (0..4_u64).map(val).collect();
let mut b: Set = (4..8_u64).map(val).collect();
a.append(&mut b);
assert_eq!(a.len(), 8);
assert!(b.is_empty());
}
#[test]
fn set_value_cow_make_mut_isolates_clones() {
let a = Value::new_set();
let b = a.clone();
let mut b_owned = b;
b_owned.as_set_mut().expect("set").insert(Value::from("x"));
assert_eq!(a.as_set().expect("set").len(), 0);
assert_eq!(b_owned.as_set().expect("set").len(), 1);
}
#[test]
fn set_from_iter_dedups_duplicates() {
let s: Set = [val(1), val(1), val(2), val(2), val(2)]
.into_iter()
.collect();
assert_eq!(s.len(), 2);
assert!(s.contains(&val(1)));
assert!(s.contains(&val(2)));
}
#[test]
fn set_accessor_coverage() {
let mut s: Set = (0..4_u64).map(val).collect();
assert!(s.contains(&val(2)));
assert!(!s.contains(&val(100)));
assert_eq!(s.get(&val(2)), Some(&val(2)));
assert!(s.get(&val(100)).is_none());
assert!(s.remove(&val(2)));
assert!(!s.remove(&val(2)));
assert_eq!(s.len(), 3);
s.retain(|v| v != &val(0));
assert!(!s.contains(&val(0)));
assert_eq!(s.len(), 2);
s.clear();
assert!(s.is_empty());
assert!(!s.contains(&val(1)));
}
#[test]
fn set_into_iterator_ref() {
let s: Set = (0..4_u64).map(val).collect();
let mut count = 0;
for _v in &s {
count += 1;
}
assert_eq!(count, 4);
}
#[test]
fn set_cursor_yields_every_element_once() {
for &n in SET_SIZES {
let vals: Vec<Value> = (0..n).map(val).collect();
let s: Set = vals.clone().into_iter().collect();
let mut cursor = s.cursor();
let mut collected: Vec<Value> = Vec::new();
while let Some(v) = s.next(&mut cursor) {
collected.push(v.clone());
}
let mut a = collected;
a.sort();
let mut b = vals;
b.sort();
assert_eq!(a, b, "size {n}");
}
}
#[test]
fn set_cursor_empty_returns_none_immediately() {
let s = Set::new();
let mut c = s.cursor();
assert!(s.next(&mut c).is_none());
}
#[test]
fn set_ord_invariant_to_insertion_order() {
let mut a = Set::new();
let mut b = Set::new();
for i in 0..16_u64 {
a.insert(val(i));
}
for i in (0..16_u64).rev() {
b.insert(val(i));
}
assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
}
fn top_object_rc(v: &Value) -> crate::Rc<Object> {
match v {
Value::Object(rc) => crate::Rc::clone(rc),
other => panic!("expected object, got {other:?}"),
}
}
/// A no-op deep-merge (every incoming key already present with an equal value) must not clone
/// the target map. `deep_merge` acquires mutable access lazily, so when nothing changes at a
/// level the shared `Rc` is left untouched.
#[test]
fn deep_merge_noop_subset_does_not_clone_object() {
let mut a = Value::from_json_str(r#"{"x": {"deep": 1}, "y": 2}"#).unwrap();
// Keep a second reference so the map's refcount > 1: eager `make_mut` would clone here.
let shared = a.clone();
let before = top_object_rc(&a);
// Strict subset with identical values: no insert, no recurse, no conflict at any level.
a.deep_merge(Value::from_json_str(r#"{"y": 2}"#).unwrap())
.unwrap();
let after = top_object_rc(&a);
assert!(
crate::Rc::ptr_eq(&before, &after),
"no-op merge must not clone the shared object map"
);
assert_eq!(a, shared, "value must be unchanged by a no-op merge");
}
/// An equal nested object under a shared key is a no-op too — the equality short-circuit runs
/// before any mutable access, so the map is not cloned.
#[test]
fn deep_merge_equal_nested_object_does_not_clone() {
let mut a = Value::from_json_str(r#"{"cfg": {"a": 1, "b": 2}, "n": 5}"#).unwrap();
let _shared = a.clone();
let before = top_object_rc(&a);
a.deep_merge(Value::from_json_str(r#"{"cfg": {"a": 1, "b": 2}}"#).unwrap())
.unwrap();
let after = top_object_rc(&a);
assert!(
crate::Rc::ptr_eq(&before, &after),
"merging an equal nested object must not clone the map"
);
}
/// A conflict on the first overlapping key is reported without cloning the target map: the
/// read-only probe detects the conflict before any mutable access is taken.
#[test]
fn deep_merge_conflict_does_not_clone_object() {
let mut a = Value::from_json_str(r#"{"x": 1, "y": 2}"#).unwrap();
let _shared = a.clone();
let before = top_object_rc(&a);
let err = a
.deep_merge(Value::from_json_str(r#"{"x": 999}"#).unwrap())
.unwrap_err();
assert!(format!("{err}").contains("generated multiple times"));
let after = top_object_rc(&a);
assert!(
crate::Rc::ptr_eq(&before, &after),
"a conflict must not clone the shared object map"
);
}
/// Nest `depth` objects `{"k": {"k": ... leaf}}` iteratively, so building the value can't itself
/// overflow and there's no parser to cap depth first.
fn nest(depth: usize, leaf: Value) -> Value {
let mut v = leaf;
for _ in 0..depth {
let mut m = BTreeMap::new();
m.insert(Value::from("k"), v);
v = Value::from(m);
}
v
}
/// Over-deep data must fail with a clean `Err`, not overflow the stack. A `Value` can be built
/// without serde_json's parse-time cap (the native bindings), so `deep_merge` must guard itself.
#[test]
fn deep_merge_rejects_excessive_depth() {
let depth = super::MAX_MERGE_DEPTH + 50;
// Shared key `k` on both sides forces full-depth recursion; distinct leaves keep the trees
// unequal so the equality short-circuit never fires.
let mut a = nest(depth, Value::from_json_str(r#"{"a": 1}"#).unwrap());
let b = nest(depth, Value::from_json_str(r#"{"b": 2}"#).unwrap());
let err = a.deep_merge(b).unwrap_err();
assert!(
format!("{err}").contains("nesting depth"),
"expected a depth-limit error, got: {err}"
);
}
/// The pre-scan carries the same guard, so the default build rejects over-deep input up front
/// (leaving the live document untouched) instead of overflowing during validation.
#[cfg(not(feature = "allocator-memory-limits"))]
#[test]
fn check_mergeable_rejects_excessive_depth() {
let depth = super::MAX_MERGE_DEPTH + 50;
let a = nest(depth, Value::from_json_str(r#"{"a": 1}"#).unwrap());
let b = nest(depth, Value::from_json_str(r#"{"b": 2}"#).unwrap());
let err = a.check_mergeable(&b).unwrap_err();
assert!(
format!("{err}").contains("nesting depth"),
"expected a depth-limit error, got: {err}"
);
}
+5 -3
View File
@@ -544,14 +544,16 @@ fn make_context(case: &TestCase) -> Result<Value> {
let map = ctx.as_object_mut()?;
// Only inject if the caller didn't already provide requestContext
// in the context object, to avoid clobbering custom test setups.
map.get_or_insert_with(Value::from("requestContext"), || rc_val);
map.entry(Value::from("requestContext")).or_insert(rc_val);
} else if let Some(ref api_ver) = case.api_version {
let map = ctx.as_object_mut()?;
if !map.contains_key(&Value::from("requestContext")) {
if let std::collections::btree_map::Entry::Vacant(e) =
map.entry(Value::from("requestContext"))
{
let mut req_ctx = Value::new_object();
let rc_map = req_ctx.as_object_mut()?;
rc_map.insert(Value::from("apiVersion"), Value::from(api_ver.clone()));
map.insert(Value::from("requestContext"), req_ctx);
e.insert(req_ctx);
}
}

Some files were not shown because too many files have changed in this diff Show More