perf: general performance improvements (#500)

* perf: general performance improvements

* remove unsafe usages, further improve perf

* perf: add table indexing
This commit is contained in:
stefan-gorules
2026-08-04 11:12:37 +02:00
committed by GitHub
parent 32a8f12547
commit fe43b4c1d7
75 changed files with 2391 additions and 686 deletions
Generated
+481 -21
View File
@@ -260,6 +260,29 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-lc-rs"
version = "1.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
"pkg-config",
]
[[package]]
name = "base64"
version = "0.22.1"
@@ -551,6 +574,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex 2.0.1",
]
@@ -695,6 +720,15 @@ dependencies = [
"error-code",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "cmov"
version = "0.5.4"
@@ -716,6 +750,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "compact_str"
version = "0.7.1"
@@ -799,6 +843,16 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -1071,6 +1125,12 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.16.0"
@@ -1152,9 +1212,9 @@ dependencies = [
[[package]]
name = "fancy-regex"
version = "0.16.2"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277"
dependencies = [
"bit-set",
"regex-automata",
@@ -1202,9 +1262,9 @@ dependencies = [
[[package]]
name = "fluent-uri"
version = "0.3.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
dependencies = [
"borrow-or-share",
"ref-cast",
@@ -1267,6 +1327,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "funty"
version = "2.0.0"
@@ -1361,6 +1427,21 @@ dependencies = [
"slab",
]
[[package]]
name = "generator"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1428,6 +1509,25 @@ dependencies = [
"scroll",
]
[[package]]
name = "h2"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "half"
version = "2.7.1"
@@ -1483,6 +1583,11 @@ name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
@@ -1508,6 +1613,16 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hipstr"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07a5072958d04f9147e517881d929d3f4706612712f8f4cfcd247f2b716d5262"
dependencies = [
"loom",
"serde",
]
[[package]]
name = "hmac"
version = "0.12.1"
@@ -1613,6 +1728,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
@@ -1954,6 +2070,65 @@ dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys",
"log",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.117",
]
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn 2.0.117",
]
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom 0.4.2",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.100"
@@ -1979,31 +2154,58 @@ dependencies = [
[[package]]
name = "jsonschema"
version = "0.33.0"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5"
checksum = "f8a77951c56b6a0af03c22af6953d6ffcfba9403c765578921f3f1089b999db6"
dependencies = [
"ahash 0.8.12",
"base64",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"jsonschema-regex",
"jsonschema-value",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
"regex-syntax",
"reqwest 0.12.28",
"reqwest 0.13.4",
"rustls",
"serde",
"serde_json",
"strum 0.28.0",
"unicode-general-category",
"uuid-simd",
]
[[package]]
name = "jsonschema-regex"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c2e64f341a1d6a15d2daa3c967e48921cf15b9c7f23a9899e8b63c7f7c4199"
dependencies = [
"regex-syntax",
]
[[package]]
name = "jsonschema-value"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e097778f3e9c6a33862077dbf07aca0360d0b18d2a7abc323da132df49ee83"
dependencies = [
"ahash 0.8.12",
"bytecount",
"fraction",
"num-cmp",
"num-traits",
"serde_json",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -2087,12 +2289,34 @@ version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.8.3"
@@ -2108,6 +2332,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "micromap"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74"
[[package]]
name = "mimalloc"
version = "0.1.52"
@@ -2248,6 +2478,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -2382,6 +2621,12 @@ version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "ordered-multimap"
version = "0.7.3"
@@ -2624,6 +2869,12 @@ dependencies = [
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "plain"
version = "0.2.3"
@@ -3103,13 +3354,16 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.33.0"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d"
checksum = "b772e96f8eb6badd4eb10fbc08b8a98244c09b4af37e72e8fa612c854604b870"
dependencies = [
"ahash 0.8.12",
"fluent-uri",
"once_cell",
"getrandom 0.3.4",
"hashbrown 0.17.1",
"itoa",
"micromap",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -3314,9 +3568,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
@@ -3354,18 +3606,28 @@ checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
@@ -3533,6 +3795,15 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
@@ -3552,6 +3823,7 @@ version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"aws-lc-rs",
"once_cell",
"ring",
"rustls-pki-types",
@@ -3560,6 +3832,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
@@ -3570,12 +3854,40 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -3633,6 +3945,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -3682,6 +4003,35 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.13.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "self_cell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813"
[[package]]
name = "semver"
version = "1.0.28"
@@ -3839,6 +4189,15 @@ dependencies = [
"digest 0.11.3",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -3877,6 +4236,16 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_cesu8"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
@@ -4004,7 +4373,16 @@ version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
"strum_macros 0.27.2",
]
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros 0.28.0",
]
[[package]]
@@ -4019,6 +4397,18 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -4472,6 +4862,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "thread_local"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
dependencies = [
"cfg-if",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@@ -4721,6 +5120,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -4739,6 +5168,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typed-path"
version = "0.12.3"
@@ -4757,6 +5192,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-id-start"
version = "1.4.0"
@@ -4974,10 +5415,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -5143,6 +5589,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"
@@ -5580,11 +6035,12 @@ dependencies = [
"reqwest 0.12.28",
"rquickjs",
"rust_decimal",
"self_cell",
"serde",
"serde_json",
"serde_path_to_error",
"sha2 0.10.9",
"strum",
"strum 0.27.2",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
@@ -5592,6 +6048,7 @@ dependencies = [
"thiserror 1.0.69",
"tokio",
"toml 0.8.23",
"typed-arena",
"zen-expression",
"zen-tmpl",
"zen-types",
@@ -5622,9 +6079,10 @@ dependencies = [
"serde",
"serde_json",
"serde_json5",
"smallvec",
"strsim",
"strum",
"strum_macros",
"strum 0.27.2",
"strum_macros 0.27.2",
"thiserror 1.0.69",
"toml 0.8.23",
"zen-macros",
@@ -5640,7 +6098,7 @@ dependencies = [
"libc",
"serde",
"serde_json",
"strum",
"strum 0.27.2",
"tokio",
"tokio-util",
"zen-engine",
@@ -5711,11 +6169,13 @@ name = "zen-types"
version = "1.0.0-beta.11"
dependencies = [
"ahash 0.8.12",
"hipstr",
"nohash-hasher",
"rust_decimal",
"rust_decimal_macros",
"serde",
"serde_json",
"smallvec",
"thiserror 1.0.69",
]
+1 -1
View File
@@ -24,7 +24,7 @@ impl ZenDecisionContent {
}
};
if let DecisionContent::Graph(g) = &mut decision_content {
g.compile();
Arc::make_mut(g).compile();
}
Ok(Self {
+3 -4
View File
@@ -1,14 +1,13 @@
use std::collections::HashMap;
use std::os::raw::c_char;
use std::ptr;
use std::rc::Rc;
use napi::bindgen_prelude::ToNapiValue;
use napi::sys;
use napi::sys::{napi_env, napi_value};
use serde_json::Value;
use zen_engine::{DecisionGraphResponse, EvaluationTrace, EvaluationTraceKind};
use zen_expression::variable::ToVariable;
use zen_expression::variable::{RcCell, ToVariable};
use zen_expression::Variable;
enum PNode {
@@ -32,8 +31,8 @@ impl PortableArena {
pub fn add(&mut self, var: &Variable, memo: &mut HashMap<usize, u32>) -> u32 {
let addr = match var {
Variable::Array(a) => Some(Rc::as_ptr(a) as *const () as usize),
Variable::Object(o) => Some(Rc::as_ptr(o) as *const () as usize),
Variable::Array(a) => Some(RcCell::as_ptr(a) as *const () as usize),
Variable::Object(o) => Some(RcCell::as_ptr(o) as *const () as usize),
_ => None,
};
+1 -1
View File
@@ -17,7 +17,7 @@ impl PyZenDecisionContent {
let mut content: DecisionContent =
serde_json::from_str(data).context("Failed to parse JSON")?;
if let DecisionContent::Graph(g) = &mut content {
g.compile();
Arc::make_mut(g).compile();
}
Ok(Self(Arc::new(content)))
}
+6 -7
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::rc::Rc;
use pyo3::prelude::{PyDictMethods, PyListMethods};
use pyo3::types::{PyDict, PyList};
@@ -9,7 +8,7 @@ use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde_json::Value;
use zen_engine::{DecisionGraphResponse, EvaluationTrace};
use zen_expression::variable::ToVariable;
use zen_expression::variable::{RcCell, ToVariable};
use zen_expression::Variable;
use crate::value::value_to_object;
@@ -29,8 +28,8 @@ impl<'py> VariableConverter<'py> {
pub fn convert(&mut self, var: &Variable) -> PyResult<Bound<'py, PyAny>> {
let addr = match var {
Variable::Array(a) => Some(Rc::as_ptr(a) as *const () as usize),
Variable::Object(o) => Some(Rc::as_ptr(o) as *const () as usize),
Variable::Array(a) => Some(RcCell::as_ptr(a) as *const () as usize),
Variable::Object(o) => Some(RcCell::as_ptr(o) as *const () as usize),
_ => None,
};
@@ -67,7 +66,7 @@ impl<'py> VariableConverter<'py> {
let dict = PyDict::new(self.py);
let borrowed = o.borrow();
for (key, value) in borrowed.iter() {
dict.set_item(key.as_ref(), self.convert(value)?)?;
dict.set_item(key.as_str(), self.convert(value)?)?;
}
dict.into_bound_py_any(self.py)
@@ -148,8 +147,8 @@ impl PortableResponse {
fn add(nodes: &mut Vec<PNode>, memo: &mut HashMap<usize, u32>, var: &Variable) -> u32 {
let addr = match var {
Variable::Array(a) => Some(Rc::as_ptr(a) as *const () as usize),
Variable::Object(o) => Some(Rc::as_ptr(o) as *const () as usize),
Variable::Array(a) => Some(RcCell::as_ptr(a) as *const () as usize),
Variable::Object(o) => Some(RcCell::as_ptr(o) as *const () as usize),
_ => None,
};
+1 -1
View File
@@ -40,7 +40,7 @@ pub fn variable_to_object<'py>(py: Python<'py>, val: &Variable) -> PyResult<Boun
let dict = PyDict::new(py);
let b = m.borrow();
for (key, value) in b.iter() {
dict.set_item(String::from(key.as_ref()), variable_to_object(py, value)?)?;
dict.set_item(key.as_str().to_owned(), variable_to_object(py, value)?)?;
}
dict.into_bound_py_any(py)
+4 -2
View File
@@ -38,6 +38,8 @@ swc_ts_fast_strip = "53.0.0"
swc_common = "23"
swc_ecma_parser = "41"
swc_ecma_ast = "25"
typed-arena = "2"
self_cell = "1"
[dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
@@ -52,10 +54,10 @@ http = { version = "1.3" }
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
reqsign = { version = "0.20", features = ["aws", "azure", "google", "default-context"] }
sha2 = { version = "0.10" }
jsonschema = { version = "0.33" }
jsonschema = { version = "0.49" }
[target.'cfg(target_family = "wasm")'.dependencies]
jsonschema = { version = "0.33", default-features = false }
jsonschema = { version = "0.49", default-features = false }
rquickjs = { version = "0.10", features = ["macro", "loader", "rust-alloc", "futures", "either", "properties", "bindgen"] }
[[bench]]
+2 -5
View File
@@ -4,7 +4,6 @@ use crate::loader::{DynamicLoader, NoopLoader};
use crate::model::GraphContent;
use crate::nodes::custom::{DynamicCustomNode, NoopCustomNode};
use crate::nodes::function::http_handler::DynamicHttpHandler;
use crate::nodes::validator_cache::ValidatorCache;
use crate::nodes::NodeHandlerExtensions;
use crate::{DecisionGraphValidationError, EvaluationError};
use serde_json::Value;
@@ -19,7 +18,6 @@ pub struct Decision {
loader: DynamicLoader,
adapter: DynamicCustomNode,
http_handler: DynamicHttpHandler,
validator_cache: ValidatorCache,
}
impl From<GraphContent> for Decision {
@@ -29,7 +27,6 @@ impl From<GraphContent> for Decision {
loader: Arc::new(NoopLoader::default()),
adapter: Arc::new(NoopCustomNode::default()),
http_handler: None,
validator_cache: ValidatorCache::default(),
}
}
}
@@ -41,7 +38,6 @@ impl From<Arc<GraphContent>> for Decision {
loader: Arc::new(NoopLoader::default()),
adapter: Arc::new(NoopCustomNode::default()),
http_handler: None,
validator_cache: ValidatorCache::default(),
}
}
}
@@ -86,8 +82,9 @@ impl Decision {
custom_node: self.adapter.clone(),
http_handler: self.http_handler.clone(),
compiled_cache: self.content.compiled_cache.clone(),
dt_indexes: self.content.dt_indexes.clone(),
stripped_functions: self.content.stripped_functions.clone(),
validator_cache: Arc::new(OnceCell::from(self.validator_cache.clone())),
validator_cache: Arc::new(OnceCell::from(self.content.validator_cache.clone())),
..Default::default()
},
})?;
+4 -52
View File
@@ -1,6 +1,5 @@
use ahash::{HashSet, HashSetExt};
use std::ops::Deref;
use std::rc::Rc;
use zen_types::rccell::RcCell;
use zen_types::variable::Variable;
pub(crate) const ZEN_RESERVED_PROPERTIES: &[&str] = &["$nodes"];
@@ -25,7 +24,7 @@ impl VariableCleaner {
| Variable::Dynamic(_) => {}
Variable::Array(arr) => {
let ptr = Rc::as_ptr(arr) as usize;
let ptr = RcCell::as_ptr(arr) as usize;
if !self.visited.insert(ptr) {
return;
}
@@ -37,14 +36,14 @@ impl VariableCleaner {
}
Variable::Object(obj) => {
let ptr = Rc::as_ptr(obj) as usize;
let ptr = RcCell::as_ptr(obj) as usize;
if !self.visited.insert(ptr) {
return;
}
let mut map = obj.borrow_mut();
for key in ZEN_RESERVED_PROPERTIES {
map.remove(*key);
map.remove_str(key);
}
for (_, value) in map.iter() {
@@ -53,51 +52,4 @@ impl VariableCleaner {
}
}
}
pub fn clone_clean(&mut self, var: &Variable) -> Variable {
match var {
Variable::Null
| Variable::Bool(_)
| Variable::Number(_)
| Variable::String(_)
| Variable::Dynamic(_) => var.shallow_clone(),
Variable::Array(arr) => {
let ptr = Rc::as_ptr(&arr) as usize;
if !self.visited.insert(ptr) {
return Variable::Array(arr.clone());
}
let items = arr.borrow();
Variable::from_array(items.iter().map(|v| self.clone_clean(v)).collect())
}
Variable::Object(obj) => {
let ptr = Rc::as_ptr(obj) as usize;
if !self.visited.insert(ptr) {
return Variable::Object(obj.clone());
}
let map = obj.borrow();
let will_remove_key = map
.keys()
.any(|k| ZEN_RESERVED_PROPERTIES.contains(&k.as_ref()));
if !will_remove_key {
return Variable::Object(obj.clone());
}
let mut new_map = map.deref().clone();
for key in ZEN_RESERVED_PROPERTIES {
new_map.remove(*key);
}
let cleaned_map = new_map
.into_iter()
.map(|(k, v)| (k, self.clone_clean(&v)))
.collect();
Variable::from_object(cleaned_map)
}
}
}
}
+52 -14
View File
@@ -24,7 +24,6 @@ use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use zen_expression::variable::{ToVariable, Variable};
@@ -35,6 +34,7 @@ pub struct DecisionGraph {
initial_graph: StableDiDecisionGraph,
graph: StableDiDecisionGraph,
config: DecisionGraphConfig,
parent_nodes: Option<Variable>,
}
#[derive(Debug)]
@@ -53,9 +53,14 @@ impl DecisionGraph {
initial_graph: graph.clone(),
graph,
config,
parent_nodes: None,
})
}
pub(crate) fn set_parent_nodes(&mut self, nodes: Option<Variable>) {
self.parent_nodes = nodes;
}
fn build_graph(
content: &GraphContent,
) -> Result<StableDiDecisionGraph, DecisionGraphValidationError> {
@@ -109,27 +114,39 @@ impl DecisionGraph {
async fn validation_schema(
&self,
node_id: &str,
schema: Option<&serde_json::Value>,
) -> Result<Option<(serde_json::Value, u64)>, String> {
) -> Result<Option<(Arc<serde_json::Value>, u64)>, String> {
let Some(schema) = schema else {
return Ok(None);
};
if let Some(resolved) = &self.config.content.resolved_schemas {
return Ok(resolved.get(node_id).cloned());
}
if !schema_dict::schema_references_dictionary(schema) {
return Ok(None);
}
let dictionaries = schema_dict::load_import_dictionaries(
self.config.extensions.loader(),
&self.config.content.imports,
)
.await?;
schema_dict::resolve_schema(schema, &dictionaries).map(Some)
schema_dict::resolve_schema(schema, &dictionaries)
.map(|resolved| Some((Arc::new(resolved.0), resolved.1)))
}
fn build_node_context(&self, node: &DecisionNode, input: Variable) -> NodeContextBase {
fn build_node_context(
&self,
node: &DecisionNode,
input: Variable,
nodes: Option<Variable>,
) -> NodeContextBase {
NodeContextBase {
id: node.id.clone(),
name: node.name.clone(),
input,
nodes,
extensions: self.config.extensions.clone(),
iteration: self.config.iteration,
trace: match self.config.trace {
@@ -164,33 +181,39 @@ impl DecisionGraph {
}
let node = &self.graph[nid];
let start = Instant::now();
let (input, input_trace) = walker.incoming_node_data(&self.graph, nid, true);
let mut base_ctx = self.build_node_context(node.deref(), input);
let start = self.config.trace.then(Instant::now);
let (input, input_trace) = walker.incoming_node_data(&self.graph, nid);
let mut base_ctx = self.build_node_context(node.deref(), input, walker.nodes_context());
let node_execution = match &node.kind {
DecisionNodeKind::InputNode { content } => {
base_ctx.input = context.clone();
match self.validation_schema(content.schema.as_deref()).await {
match self
.validation_schema(&node.id, content.schema.as_deref())
.await
{
Err(message) => base_ctx.error(message),
Ok(None) => handle_node(base_ctx, content.clone(), InputNodeHandler).await,
Ok(Some((schema, salt))) => {
base_ctx.config.validation_salt = salt;
let resolved = InputNodeContent {
schema: Some(Arc::new(schema)),
schema: Some(schema),
};
handle_node(base_ctx, resolved, InputNodeHandler).await
}
}
}
DecisionNodeKind::OutputNode { content } => {
match self.validation_schema(content.schema.as_deref()).await {
match self
.validation_schema(&node.id, content.schema.as_deref())
.await
{
Err(message) => base_ctx.error(message),
Ok(None) => handle_node(base_ctx, content.clone(), OutputNodeHandler).await,
Ok(Some((schema, salt))) => {
base_ctx.config.validation_salt = salt;
let resolved = OutputNodeContent {
schema: Some(Arc::new(schema)),
schema: Some(schema),
};
handle_node(base_ctx, resolved, OutputNodeHandler).await
}
@@ -217,14 +240,19 @@ impl DecisionGraph {
}
};
tracer.record_execution(node.deref(), input_trace, &node_execution, start.elapsed());
tracer.record_execution(
node.deref(),
input_trace,
&node_execution,
start.map(|s| s.elapsed()).unwrap_or_default(),
);
let output = match node_execution {
Ok(ok) => ok.output,
Err(err) => {
let mut cleaner = VariableCleaner::new();
let trace = tracer.into_traces();
if let Some(t) = &trace {
let mut cleaner = VariableCleaner::new();
t.values().for_each(|v| {
cleaner.clean(&v.input);
cleaner.clean(&v.output);
@@ -242,11 +270,21 @@ impl DecisionGraph {
}
};
let nodes_view = match (&node.kind, &self.parent_nodes) {
(DecisionNodeKind::InputNode { .. }, Some(parent_nodes)) => {
let view = output.depth_clone(1);
view.dot_insert(Variable::nodes_key().as_ref(), parent_nodes.clone());
Some(view)
}
_ => None,
};
walker.set_node_data(
nid,
NodeData {
name: Rc::from(node.name.deref()),
name: zen_types::symbol::Symbol::from(node.name.deref()),
data: output,
nodes_view,
},
);
+1 -1
View File
@@ -1,7 +1,7 @@
pub(crate) mod cleaner;
mod error;
pub(crate) mod graph;
mod schema_dict;
pub(crate) mod schema_dict;
mod tracer;
mod walker;
+26 -20
View File
@@ -6,7 +6,6 @@ use petgraph::prelude::{EdgeIndex, NodeIndex, StableDiGraph};
use petgraph::visit::{EdgeRef, IntoNodeIdentifiers, VisitMap, Visitable};
use petgraph::{Incoming, Outgoing};
use std::ops::Deref;
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Instant;
@@ -22,8 +21,9 @@ use zen_expression::Isolate;
pub(crate) type StableDiDecisionGraph = StableDiGraph<Arc<DecisionNode>, Arc<DecisionEdge>>;
pub(crate) struct NodeData {
pub name: Rc<str>,
pub name: zen_types::symbol::Symbol,
pub data: Variable,
pub nodes_view: Option<Variable>,
}
pub(crate) struct GraphWalker {
@@ -96,7 +96,10 @@ impl GraphWalker {
let node_values = self
.node_data
.iter()
.filter_map(|(_, nd)| Some((nd.name.clone(), nd.data.clone())))
.filter_map(|(_, nd)| {
let value = nd.nodes_view.clone().unwrap_or_else(|| nd.data.clone());
Some((nd.name.clone(), value))
})
.collect();
Variable::from_object(node_values)
@@ -106,34 +109,35 @@ impl GraphWalker {
self.node_data.insert(node_id, value);
}
pub fn nodes_context(&self) -> Option<Variable> {
self.nodes_in_context.then(|| self.get_all_node_data())
}
pub fn incoming_node_data(
&self,
g: &StableDiDecisionGraph,
node_id: NodeIndex,
with_nodes: bool,
) -> (Variable, Variable) {
let value = self.merge_node_data(g.neighbors_directed(node_id, Incoming));
if self.nodes_in_context && with_nodes {
if let Some(object_ref) = value.as_object() {
let mut new_object = object_ref.borrow().clone();
new_object.insert(Rc::from("$nodes"), self.get_all_node_data());
return (Variable::from_object(new_object), value);
}
}
(value.depth_clone(1), value)
(value.clone(), value)
}
pub fn merge_node_data<I>(&self, iter: I) -> Variable
where
I: Iterator<Item = NodeIndex>,
{
iter.filter_map(|nid| self.node_data.get(&nid))
.fold(Variable::empty_object(), |mut prev, nd| {
prev.merge_clone(&nd.data)
})
let mut incoming = iter.filter_map(|nid| self.node_data.get(&nid));
let Some(first) = incoming.next() else {
return Variable::empty_object();
};
let head = match &first.data {
Variable::Object(_) | Variable::Array(_) => first.data.clone(),
_ => Variable::empty_object(),
};
incoming.fold(head, |mut prev, nd| prev.merge_clone(&nd.data))
}
pub fn next<F: FnMut(DecisionGraphTrace)>(
@@ -163,8 +167,11 @@ impl GraphWalker {
let decision_node = g.node_weight(nid)?.clone();
if let DecisionNodeKind::SwitchNode { content } = &decision_node.kind {
if !self.visited_switch_nodes.contains(&nid) {
let (input, input_trace) = self.incoming_node_data(g, nid, true);
let (input, input_trace) = self.incoming_node_data(g, nid);
let mut isolate = Isolate::with_environment(input);
if let Some(nodes) = self.nodes_context() {
isolate.set_local(Variable::nodes_key(), nodes);
}
let mut statement_iter = content.statements.iter();
let valid_statements: Vec<SwitchStatementTraceRow> = match content.hit_policy {
@@ -183,7 +190,6 @@ impl GraphWalker {
if let Some(on_trace) = &mut on_trace {
let output = input_trace.depth_clone(1);
output.dot_remove("$nodes");
on_trace(DecisionGraphTrace {
id: decision_node.id.clone(),
+2 -2
View File
@@ -306,9 +306,9 @@ impl DecisionEngine {
fn decision_from_graph_arc(&self, content: Arc<DecisionContent>) -> Decision {
let graph: Arc<GraphContent> = match Arc::try_unwrap(content) {
Ok(DecisionContent::Graph(g)) => Arc::new(g),
Ok(DecisionContent::Graph(g)) => g,
Err(arc) => match arc.as_ref() {
DecisionContent::Graph(g) => Arc::new(g.clone()),
DecisionContent::Graph(g) => g.clone(),
DecisionContent::Policy(_) => {
panic!("decision_from_graph_arc called with Policy variant")
}
+2
View File
@@ -118,6 +118,8 @@
//! }
//! ```
// `deny` rather than `forbid` so `nodes::variable_json` can opt in: reading a
// `RefCell` without a guard is the only way to hand jsonschema `'a` borrows.
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used)]
#![allow(clippy::module_inception)]
+58 -17
View File
@@ -1,8 +1,7 @@
use ahash::{HashMap, HashMapExt};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::Mutex;
use std::sync::{Arc, RwLock};
use crate::loader::{DecisionLoader, DynamicLoader, LoaderResponse};
use crate::model::DecisionContent;
@@ -10,31 +9,71 @@ use crate::model::DecisionContent;
#[derive(Debug)]
pub struct CachedLoader {
loader: DynamicLoader,
cache: Mutex<HashMap<String, Arc<DecisionContent>>>,
cache: RwLock<HashMap<String, Arc<DecisionContent>>>,
}
impl From<DynamicLoader> for CachedLoader {
fn from(value: DynamicLoader) -> Self {
Self {
loader: value,
cache: Mutex::new(HashMap::new()),
cache: RwLock::new(HashMap::new()),
}
}
}
fn compiled(content: Arc<DecisionContent>) -> Arc<DecisionContent> {
let DecisionContent::Graph(graph) = content.as_ref() else {
return content;
};
if graph.compiled_cache.is_some() {
return content;
}
let mut owned = (**graph).clone();
owned.compile();
Arc::new(DecisionContent::Graph(Arc::new(owned)))
}
async fn prepared(loader: &DynamicLoader, content: Arc<DecisionContent>) -> Arc<DecisionContent> {
let DecisionContent::Graph(graph) = content.as_ref() else {
return content;
};
if graph.compiled_cache.is_some() && graph.resolved_schemas.is_some() {
return content;
}
let mut owned = (**graph).clone();
owned.compile();
let _ = owned.resolve_schemas(loader).await;
Arc::new(DecisionContent::Graph(Arc::new(owned)))
}
impl DecisionLoader for CachedLoader {
fn load<'a>(
&'a self,
key: &'a str,
) -> Pin<Box<dyn Future<Output = LoaderResponse> + 'a + Send>> {
Box::pin(async move {
let mut cache = self.cache.lock().await;
if let Some(content) = cache.get(key) {
return Ok(content.clone());
}
let cached = self
.cache
.read()
.ok()
.and_then(|cache| cache.get(key).cloned());
let decision_content = self.loader.load(key).await?;
cache.insert(key.to_string(), decision_content.clone());
let loaded = match &cached {
Some(content) => content.clone(),
None => self.loader.load(key).await?,
};
let decision_content = prepared(&self.loader, loaded).await;
let unchanged = cached
.as_ref()
.is_some_and(|content| Arc::ptr_eq(content, &decision_content));
if !unchanged {
if let Ok(mut cache) = self.cache.write() {
cache.insert(key.to_string(), decision_content.clone());
}
}
Ok(decision_content)
})
}
@@ -44,15 +83,17 @@ impl DecisionLoader for CachedLoader {
}
fn load_sync(&self, key: &str) -> Option<LoaderResponse> {
let Ok(mut cache) = self.cache.try_lock() else {
return self.loader.load_sync(key);
};
if let Some(content) = cache.get(key) {
return Some(Ok(content.clone()));
if let Ok(cache) = self.cache.read() {
if let Some(content) = cache.get(key) {
return Some(Ok(content.clone()));
}
}
let response = self.loader.load_sync(key)?;
let response = self.loader.load_sync(key)?.map(compiled);
if let Ok(content) = &response {
cache.insert(key.to_string(), content.clone());
if let Ok(mut cache) = self.cache.write() {
cache.insert(key.to_string(), content.clone());
}
}
Some(response)
}
+82 -10
View File
@@ -1,4 +1,8 @@
use crate::decision_graph::schema_dict;
use crate::loader::DynamicLoader;
use crate::nodes::decision_table::index::TableIndex;
use crate::nodes::function::v2::strip::TypeStripper;
use crate::nodes::validator_cache::ValidatorCache;
use crate::policy::PolicyDocument;
use ahash::{HashMap, HashMapExt};
use serde::{Deserialize, Deserializer, Serialize};
@@ -9,7 +13,7 @@ use zen_types::decision::{DecisionEdge, DecisionNode, DecisionNodeKind, Function
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum DecisionContent {
Graph(GraphContent),
Graph(Arc<GraphContent>),
Policy(PolicyContent),
}
@@ -26,7 +30,8 @@ impl<'de> Deserialize<'de> for DecisionContent {
let content = if is_policy {
serde_path_to_error::deserialize::<_, PolicyContent>(value).map(Self::Policy)
} else {
serde_path_to_error::deserialize::<_, GraphContent>(value).map(Self::Graph)
serde_path_to_error::deserialize::<_, GraphContent>(value)
.map(|graph| Self::Graph(Arc::new(graph)))
};
content.map_err(serde::de::Error::custom)
@@ -35,7 +40,7 @@ impl<'de> Deserialize<'de> for DecisionContent {
impl Default for DecisionContent {
fn default() -> Self {
Self::Graph(GraphContent::default())
Self::Graph(Arc::new(GraphContent::default()))
}
}
@@ -62,19 +67,21 @@ impl DecisionContent {
}
pub fn into_graph_arc(self: Arc<Self>) -> Option<Arc<GraphContent>> {
match Arc::try_unwrap(self) {
Ok(Self::Graph(g)) => Some(Arc::new(g)),
Ok(Self::Policy(_)) => None,
Err(arc) => match arc.as_ref() {
Self::Graph(g) => Some(Arc::new(g.clone())),
Self::Policy(_) => None,
},
match self.as_ref() {
Self::Graph(g) => Some(g.clone()),
Self::Policy(_) => None,
}
}
}
impl From<GraphContent> for DecisionContent {
fn from(value: GraphContent) -> Self {
Self::Graph(Arc::new(value))
}
}
impl From<Arc<GraphContent>> for DecisionContent {
fn from(value: Arc<GraphContent>) -> Self {
Self::Graph(value)
}
}
@@ -105,6 +112,15 @@ pub struct GraphContent {
#[serde(skip)]
pub stripped_functions: Option<Arc<HashMap<Arc<str>, Arc<str>>>>,
#[serde(skip)]
pub resolved_schemas: Option<Arc<HashMap<Arc<str>, (Arc<serde_json::Value>, u64)>>>,
#[serde(skip)]
pub(crate) validator_cache: ValidatorCache,
#[serde(skip)]
pub(crate) dt_indexes: Option<Arc<HashMap<Arc<str>, TableIndex>>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -114,6 +130,7 @@ pub struct PolicyContent(pub Arc<PolicyDocument>);
impl GraphContent {
pub fn compile(&mut self) {
self.compile_functions();
self.build_dt_indexes();
if self.compiled_cache.is_some() {
return;
}
@@ -185,6 +202,61 @@ impl GraphContent {
self.compiled_cache.replace(Arc::new(cache));
}
fn build_dt_indexes(&mut self) {
if self.dt_indexes.is_some() {
return;
}
let indexes: HashMap<Arc<str>, TableIndex> = self
.nodes
.iter()
.filter_map(|node| match &node.kind {
DecisionNodeKind::DecisionTableNode { content } => {
TableIndex::build(&content.inputs, &content.rules)
.map(|index| (node.id.clone(), index))
}
_ => None,
})
.collect();
self.dt_indexes = Some(Arc::new(indexes));
}
pub async fn resolve_schemas(&mut self, loader: &DynamicLoader) -> Result<(), String> {
if self.resolved_schemas.is_some() {
return Ok(());
}
let mut referencing: Vec<(Arc<str>, Arc<serde_json::Value>)> = Vec::new();
for node in &self.nodes {
let schema = match &node.kind {
DecisionNodeKind::InputNode { content } => content.schema.as_ref(),
DecisionNodeKind::OutputNode { content } => content.schema.as_ref(),
_ => None,
};
if let Some(schema) = schema {
if schema_dict::schema_references_dictionary(schema) {
referencing.push((node.id.clone(), schema.clone()));
}
}
}
if referencing.is_empty() {
self.resolved_schemas = Some(Arc::new(HashMap::new()));
return Ok(());
}
let dictionaries = schema_dict::load_import_dictionaries(loader, &self.imports).await?;
let mut resolved = HashMap::with_capacity(referencing.len());
for (id, schema) in referencing {
let (value, salt) = schema_dict::resolve_schema(&schema, &dictionaries)?;
resolved.insert(id, (Arc::new(value), salt));
}
self.resolved_schemas = Some(Arc::new(resolved));
Ok(())
}
fn compile_functions(&mut self) {
if self.stripped_functions.is_some() {
return;
+45 -3
View File
@@ -2,6 +2,7 @@ use crate::nodes::definition::{NodeDataType, TraceDataType};
use crate::nodes::extensions::NodeHandlerExtensions;
use crate::nodes::function::v2::function::Function;
use crate::nodes::result::{NodeResponse, NodeResult};
use crate::nodes::variable_json::{Guards, VariableNode};
use crate::nodes::NodeError;
use crate::ZEN_CONFIG;
use ahash::AHasher;
@@ -14,6 +15,7 @@ use std::hash::Hasher;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use thiserror::Error;
use zen_expression::Isolate;
use zen_types::variable::{ToVariable, Variable};
#[derive(Clone)]
@@ -26,6 +28,7 @@ where
pub name: Arc<str>,
pub node: NodeData,
pub input: Variable,
pub nodes: Option<Variable>,
pub trace: Option<RefCell<TraceData>>,
pub extensions: NodeHandlerExtensions,
pub iteration: u8,
@@ -37,11 +40,25 @@ where
NodeData: NodeDataType,
TraceData: TraceDataType,
{
pub fn input_with_nodes(&self) -> Variable {
let Some(nodes) = &self.nodes else {
return self.input.shallow_clone();
};
let Variable::Object(object) = &self.input else {
return self.input.shallow_clone();
};
let mut map = object.borrow().clone();
map.insert(Variable::nodes_key(), nodes.clone());
Variable::from_object(map)
}
pub fn from_base(base: NodeContextBase, data: NodeData) -> Self {
Self {
id: base.id,
name: base.name,
input: base.input,
nodes: base.nodes,
extensions: base.extensions,
iteration: base.iteration,
trace: base.config.trace.then(|| Default::default()),
@@ -50,6 +67,10 @@ where
}
}
pub fn isolate(&self) -> Isolate {
make_isolate(&self.input, self.nodes.as_ref(), &self.extensions)
}
pub fn trace<Function>(&self, mutator: Function)
where
Function: FnOnce(&mut TraceData),
@@ -88,7 +109,7 @@ where
self.extensions.function_runtime().await.node_context(self)
}
pub fn validate(&self, schema: &Value, value: &Value) -> Result<(), NodeError> {
pub fn validate(&self, schema: &Value, value: &Variable) -> Result<(), NodeError> {
let validator_cache = self.extensions.validator_cache();
let hash = self.hash_node();
@@ -96,8 +117,9 @@ where
.get_or_insert(hash, schema)
.node_context(self)?;
let guards = Guards::default();
validator
.validate(value)
.validate(VariableNode::new(value, &guards))
.map_err(|err| ValidationErrorJson::from(err))
.node_context(self)?;
@@ -188,13 +210,32 @@ pub struct NodeContextBase {
pub id: Arc<str>,
pub name: Arc<str>,
pub input: Variable,
pub nodes: Option<Variable>,
pub iteration: u8,
pub extensions: NodeHandlerExtensions,
pub config: NodeContextConfig,
pub trace: Option<RefCell<Variable>>,
}
pub(crate) fn make_isolate(
input: &Variable,
nodes: Option<&Variable>,
extensions: &NodeHandlerExtensions,
) -> Isolate {
let mut isolate =
Isolate::with_environment(input.clone()).with_cache(extensions.compiled_cache.clone());
if let Some(nodes) = nodes {
isolate.set_local(Variable::nodes_key(), nodes.clone());
}
isolate
}
impl NodeContextBase {
pub fn isolate(&self) -> Isolate {
make_isolate(&self.input, self.nodes.as_ref(), &self.extensions)
}
pub fn error<Error>(&self, error: Error) -> NodeResult
where
Error: Into<Box<dyn std::error::Error>>,
@@ -245,6 +286,7 @@ where
id: value.id,
name: value.name,
input: value.input,
nodes: value.nodes,
extensions: value.extensions,
iteration: value.iteration,
config: value.config,
@@ -308,7 +350,7 @@ impl Display for ValidationErrorJson {
impl<'a> From<ValidationError<'a>> for ValidationErrorJson {
fn from(value: ValidationError<'a>) -> Self {
ValidationErrorJson {
path: value.instance_path.to_string(),
path: value.instance_path().to_string(),
message: format!("{}", value),
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ impl NodeHandler for CustomNodeHandler {
async fn handle(&self, ctx: NodeContext<Self::NodeData, Self::TraceData>) -> NodeResult {
let custom_node_request = CustomNodeRequest {
input: ctx.input.clone(),
input: ctx.input_with_nodes(),
node: CustomDecisionNode {
id: ctx.id.clone(),
name: ctx.name.clone(),
+43 -27
View File
@@ -38,37 +38,53 @@ impl NodeHandler for DecisionNodeHandler {
}
async fn handle(&self, ctx: NodeContext<Self::NodeData, Self::TraceData>) -> NodeResult {
let loader = ctx.extensions.loader();
let sub_decision = loader.load(ctx.node.key.deref()).await.node_context(&ctx)?;
let sub_kind = sub_decision.kind();
let Some(sub_graph) = sub_decision.into_graph_arc() else {
return ctx.error(format!(
"sub-decision '{}' is a {sub_kind}, expected graph",
ctx.node.key
));
};
let mut decision_graph_ref = self.decision_graph.borrow_mut();
let decision_graph = match decision_graph_ref.as_mut() {
Some(dg) => dg,
None => {
let dg = DecisionGraph::try_new(DecisionGraphConfig {
content: sub_graph,
extensions: ctx.extensions.clone(),
trace: ctx.config.trace,
iteration: ctx.iteration + 1,
max_depth: ctx.config.max_depth,
})
.node_context(&ctx)?;
*decision_graph_ref = Some(dg);
match decision_graph_ref.as_mut() {
Some(dg) => dg,
None => return ctx.error("Failed to initialize decision graph".to_string()),
}
}
if decision_graph_ref.is_none() {
let loader = ctx.extensions.loader();
let sub_decision = loader.load(ctx.node.key.deref()).await.node_context(&ctx)?;
let sub_kind = sub_decision.kind();
let Some(sub_graph) = sub_decision.into_graph_arc() else {
return ctx.error(format!(
"sub-decision '{}' is a {sub_kind}, expected graph",
ctx.node.key
));
};
let sub_graph =
if sub_graph.compiled_cache.is_some() && sub_graph.resolved_schemas.is_some() {
sub_graph
} else {
let mut owned = (*sub_graph).clone();
owned.compile();
let _ = owned.resolve_schemas(loader).await;
std::sync::Arc::new(owned)
};
let mut extensions = ctx.extensions.clone();
extensions.compiled_cache = sub_graph.compiled_cache.clone();
extensions.dt_indexes = sub_graph.dt_indexes.clone();
extensions.validator_cache =
std::sync::Arc::new(std::cell::OnceCell::from(sub_graph.validator_cache.clone()));
let dg = DecisionGraph::try_new(DecisionGraphConfig {
content: sub_graph,
extensions,
trace: ctx.config.trace,
iteration: ctx.iteration + 1,
max_depth: ctx.config.max_depth,
})
.node_context(&ctx)?;
*decision_graph_ref = Some(dg);
}
let Some(decision_graph) = decision_graph_ref.as_mut() else {
return ctx.error("Failed to initialize decision graph".to_string());
};
decision_graph.set_parent_nodes(ctx.nodes.clone());
let evaluate_result = Box::pin(decision_graph.evaluate(ctx.input.clone())).await;
match evaluate_result {
Ok(result) => {
@@ -0,0 +1,130 @@
use ahash::HashMap;
use fixedbitset::FixedBitSet;
use rust_decimal::Decimal;
use std::sync::Arc;
use zen_expression::intellisense::{ArmTest, IntelliSense};
use zen_types::decision::DecisionTableInputField;
use zen_types::variable::Variable;
pub(crate) const MIN_INDEX_ROWS: usize = 8;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TableIndex {
pub(crate) columns: Vec<Option<ColumnIndex>>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ColumnIndex {
strings: HashMap<Arc<str>, FixedBitSet>,
numbers: HashMap<Decimal, FixedBitSet>,
bools: HashMap<bool, FixedBitSet>,
captured: FixedBitSet,
pub(crate) fallback: FixedBitSet,
}
impl TableIndex {
pub(crate) fn build(
inputs: &[DecisionTableInputField],
rules: &[HashMap<Arc<str>, Arc<str>>],
) -> Option<TableIndex> {
let rows = rules.len();
if rows < MIN_INDEX_ROWS {
return None;
}
let mut intellisense = IntelliSense::new();
let columns: Vec<Option<ColumnIndex>> = inputs
.iter()
.map(|col| ColumnIndex::build(col, rules, rows, &mut intellisense))
.collect();
columns
.iter()
.any(Option::is_some)
.then_some(TableIndex { columns })
}
pub(crate) fn decides(&self, col_idx: usize, row_idx: usize) -> bool {
self.columns
.get(col_idx)
.and_then(Option::as_ref)
.is_some_and(|c| c.captured.contains(row_idx))
}
}
impl ColumnIndex {
fn build(
col: &DecisionTableInputField,
rules: &[HashMap<Arc<str>, Arc<str>>],
rows: usize,
intellisense: &mut IntelliSense,
) -> Option<ColumnIndex> {
if col.field.as_deref().is_none_or(|f| f.is_empty()) {
return None;
}
let mut strings: HashMap<Arc<str>, FixedBitSet> = HashMap::default();
let mut numbers: HashMap<Decimal, FixedBitSet> = HashMap::default();
let mut bools: HashMap<bool, FixedBitSet> = HashMap::default();
let mut captured = FixedBitSet::with_capacity(rows);
let mut fallback = FixedBitSet::with_capacity(rows);
for (row_idx, rule) in rules.iter().enumerate() {
let Some(cell) = rule.get(&col.id).filter(|c| !c.is_empty()) else {
fallback.insert(row_idx);
continue;
};
match intellisense.cell_test(cell) {
ArmTest::Enum { values, .. } => {
for value in values {
strings
.entry(Arc::from(value.as_ref()))
.or_insert_with(|| FixedBitSet::with_capacity(rows))
.insert(row_idx);
}
captured.insert(row_idx);
}
ArmTest::Bool { values, .. } => {
for value in values {
bools
.entry(value)
.or_insert_with(|| FixedBitSet::with_capacity(rows))
.insert(row_idx);
}
captured.insert(row_idx);
}
ArmTest::Number { cover, .. } => match cover.points() {
Some(points) => {
for point in points {
numbers
.entry(point.normalize())
.or_insert_with(|| FixedBitSet::with_capacity(rows))
.insert(row_idx);
}
captured.insert(row_idx);
}
None => {
fallback.insert(row_idx);
}
},
ArmTest::Default | ArmTest::Unrecognized => {
fallback.insert(row_idx);
}
}
}
(captured.count_ones(..) > 0).then_some(ColumnIndex {
strings,
numbers,
bools,
captured,
fallback,
})
}
pub(crate) fn rows_for(&self, value: &Variable) -> Option<&FixedBitSet> {
match value {
Variable::String(s) => self.strings.get(s.as_str()),
Variable::Number(n) => self.numbers.get(&n.normalize()),
Variable::Bool(b) => self.bools.get(b),
_ => None,
}
}
}
+83 -10
View File
@@ -2,14 +2,20 @@ use crate::nodes::definition::NodeHandler;
use crate::nodes::result::NodeResult;
use crate::nodes::{NodeContext, NodeResponse};
use ahash::HashMap;
use fixedbitset::FixedBitSet;
use index::TableIndex;
use serde::Serialize;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::Arc;
use zen_expression::variable::ToVariable;
use zen_expression::Isolate;
use zen_types::decision::{DecisionTableContent, DecisionTableHitPolicy, TransformAttributes};
use zen_types::decision::{
DecisionTableContent, DecisionTableHitPolicy, DecisionTableInputField, TransformAttributes,
};
use zen_types::variable::Variable;
pub(crate) mod index;
#[derive(Debug, Clone)]
pub struct DecisionTableNodeHandler;
@@ -38,12 +44,20 @@ impl NodeHandler for DecisionTableNodeHandler {
impl DecisionTableNodeHandler {
fn handle_first_hit(&self, ctx: DecisionTableContext) -> NodeResult {
let mut isolate = Isolate::with_environment(ctx.input.depth_clone(1))
.with_cache(ctx.extensions.compiled_cache.clone());
let mut isolate = ctx.isolate();
if !ctx.config.trace {
for rule in ctx.node.rules.iter() {
if let Some(RowResult::Output(output)) = self.evaluate_row(&ctx, rule, &mut isolate)
let index = Self::table_index(&ctx);
let candidates =
index.and_then(|ix| Self::candidate_rows(ix, &ctx.node.inputs, &mut isolate));
let pruner = candidates.as_ref().and(index);
for (row_idx, rule) in ctx.node.rules.iter().enumerate() {
if candidates.as_ref().is_some_and(|c| !c.contains(row_idx)) {
continue;
}
let pruned = pruner.map(|ix| (ix, row_idx));
if let Some(RowResult::Output(output)) =
self.evaluate_row(&ctx, rule, &mut isolate, pruned)
{
return ctx.success(output);
}
@@ -55,7 +69,7 @@ impl DecisionTableNodeHandler {
}
let hit = ctx.node.rules.iter().enumerate().find_map(|(index, rule)| {
match self.evaluate_row(&ctx, rule, &mut isolate)? {
match self.evaluate_row(&ctx, rule, &mut isolate, None)? {
RowResult::WithTrace {
output,
reference_map,
@@ -88,11 +102,21 @@ impl DecisionTableNodeHandler {
fn handle_collect(&self, ctx: DecisionTableContext) -> NodeResult {
let mut outputs = Vec::new();
let mut traces = Vec::new();
let mut isolate = Isolate::with_environment(ctx.input.depth_clone(1))
.with_cache(ctx.extensions.compiled_cache.clone());
let mut isolate = ctx.isolate();
let table_index = (!ctx.config.trace)
.then(|| Self::table_index(&ctx))
.flatten();
let candidates =
table_index.and_then(|ix| Self::candidate_rows(ix, &ctx.node.inputs, &mut isolate));
let pruner = candidates.as_ref().and(table_index);
for (index, rule) in ctx.node.rules.iter().enumerate() {
if let Some(result) = self.evaluate_row(&ctx, rule, &mut isolate) {
if candidates.as_ref().is_some_and(|c| !c.contains(index)) {
continue;
}
let pruned = pruner.map(|ix| (ix, index));
if let Some(result) = self.evaluate_row(&ctx, rule, &mut isolate, pruned) {
match result {
RowResult::Output(output) => {
outputs.push(output);
@@ -146,14 +170,63 @@ impl DecisionTableNodeHandler {
}
}
fn table_index(ctx: &DecisionTableContext) -> Option<&TableIndex> {
ctx.extensions.dt_indexes.as_ref()?.get(&ctx.id)
}
fn candidate_rows(
index: &TableIndex,
inputs: &[DecisionTableInputField],
isolate: &mut Isolate,
) -> Option<FixedBitSet> {
let mut acc: Option<FixedBitSet> = None;
for (col_idx, column) in index.columns.iter().enumerate() {
let Some(column) = column else {
continue;
};
let Some(field) = inputs[col_idx].field.as_ref().filter(|f| !f.is_empty()) else {
continue;
};
isolate.set_reference(field).ok()?;
let value = isolate.get_reference(field)?;
if matches!(value, Variable::Dynamic(_)) {
return None;
}
let hit = column.rows_for(&value);
match &mut acc {
None => {
let mut first = column.fallback.clone();
if let Some(hit) = hit {
first.union_with(hit);
}
acc = Some(first);
}
Some(acc) => {
let fallback = column.fallback.as_slice();
let hit = hit.map(FixedBitSet::as_slice).unwrap_or_default();
for (i, word) in acc.as_mut_slice().iter_mut().enumerate() {
let f = fallback.get(i).copied().unwrap_or(0);
let h = hit.get(i).copied().unwrap_or(0);
*word &= f | h;
}
}
}
}
acc
}
fn evaluate_row<'a>(
&self,
ctx: &'a DecisionTableContext,
rule: &'a HashMap<Arc<str>, Arc<str>>,
isolate: &mut Isolate,
pruned: Option<(&TableIndex, usize)>,
) -> Option<RowResult> {
let content = &ctx.node;
for input in content.inputs.iter() {
for (col_idx, input) in content.inputs.iter().enumerate() {
if pruned.is_some_and(|(ix, row_idx)| ix.decides(col_idx, row_idx)) {
continue;
}
let Some(rule_value) = rule.get(&input.id) else {
continue;
};
+2 -11
View File
@@ -6,7 +6,6 @@ use std::rc::Rc;
use crate::nodes::context::{NodeContext, NodeContextExt};
use crate::nodes::definition::NodeHandler;
use zen_expression::variable::{ToVariable, Variable};
use zen_expression::Isolate;
use zen_types::decision::TransformAttributes;
#[derive(Debug, Clone)]
@@ -28,8 +27,7 @@ impl NodeHandler for ExpressionNodeHandler {
async fn handle(&self, ctx: NodeContext<Self::NodeData, Self::TraceData>) -> NodeResult {
let result = Variable::empty_object();
let mut isolate = Isolate::with_environment(ctx.input.depth_clone(1))
.with_cache(ctx.extensions.compiled_cache.clone());
let mut isolate = ctx.isolate();
for expression in ctx.node.expressions.iter() {
if expression.key.is_empty() || expression.value.is_empty() {
@@ -50,14 +48,7 @@ impl NodeHandler for ExpressionNodeHandler {
);
});
isolate.update_environment(|env| {
let Some(environment) = env else {
return;
};
let key = format!("$.{}", &expression.key);
let _ = environment.dot_insert(key.as_str(), value.depth_clone(2));
});
isolate.insert_dollar(&expression.key, value.clone());
result.dot_insert(&expression.key, value);
}
+3
View File
@@ -1,5 +1,6 @@
use crate::loader::{DynamicLoader, NoopLoader};
use crate::nodes::custom::{DynamicCustomNode, NoopCustomNode};
use crate::nodes::decision_table::index::TableIndex;
use crate::nodes::function::http_handler::DynamicHttpHandler;
use crate::nodes::function::v2::function::{Function, FunctionConfig};
use crate::nodes::function::v2::module::console::ConsoleListener;
@@ -21,6 +22,7 @@ pub struct NodeHandlerExtensions {
pub(crate) http_handler: DynamicHttpHandler,
pub(crate) compiled_cache: Option<Arc<OpcodeCache>>,
pub(crate) stripped_functions: Option<Arc<ahash::HashMap<Arc<str>, Arc<str>>>>,
pub(crate) dt_indexes: Option<Arc<ahash::HashMap<Arc<str>, TableIndex>>>,
}
impl Default for NodeHandlerExtensions {
@@ -33,6 +35,7 @@ impl Default for NodeHandlerExtensions {
custom_node: Arc::new(NoopCustomNode::default()),
compiled_cache: None,
stripped_functions: None,
dt_indexes: None,
http_handler: None,
}
}
+2
View File
@@ -29,6 +29,7 @@ impl NodeHandler for FunctionNodeHandler {
id: ctx.id.clone(),
name: ctx.name.clone(),
input: ctx.input.clone(),
nodes: ctx.nodes.clone(),
extensions: ctx.extensions.clone(),
trace: ctx.config.trace.then(|| Default::default()),
iteration: ctx.iteration,
@@ -43,6 +44,7 @@ impl NodeHandler for FunctionNodeHandler {
id: ctx.id.clone(),
name: ctx.name.clone(),
input: ctx.input.clone(),
nodes: ctx.nodes.clone(),
extensions: ctx.extensions.clone(),
trace: ctx.config.trace.then(|| Default::default()),
iteration: ctx.iteration,
+1 -1
View File
@@ -30,7 +30,7 @@ impl NodeHandler for FunctionV1NodeHandler {
runtime.set_interrupt_handler(Some(interrupt_handler));
let mut script = Script::new(runtime.clone());
let result_response = script.call(ctx.node.deref(), &ctx.input).await;
let result_response = script.call(ctx.node.deref(), &ctx.input_with_nodes()).await;
runtime.set_interrupt_handler(None);
+1 -2
View File
@@ -4,7 +4,6 @@ use rquickjs::{Context, Ctx, Error as QError, FromJs, Module, Runtime};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use std::rc::Rc;
use zen_expression::variable::Variable;
#[derive(Debug, Deserialize)]
@@ -59,7 +58,7 @@ impl Script {
fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error {
let error = JsValue::from_js(&ctx, ctx.catch())
.map(|v| v.0)
.unwrap_or(Variable::String(Rc::from(e.to_string().as_str())));
.unwrap_or(Variable::String((e.to_string().as_str()).into()));
anyhow::Error::msg(error.to_string())
}
+2 -4
View File
@@ -65,10 +65,8 @@ impl NodeHandler for FunctionV2NodeHandler {
.function_context(&function_context)
.await?;
let input_omit_nodes = ctx.input.depth_clone(1);
input_omit_nodes.dot_remove("$nodes");
let response_result = function
.call_handler(&module_name, JsValueWithNodes(JsValue(input_omit_nodes)))
.call_handler(&module_name, JsValueWithNodes(JsValue(ctx.input.clone())))
.await;
function.runtime().set_interrupt_handler(None).await;
@@ -97,7 +95,7 @@ impl FunctionV2NodeHandler {
ctx.globals().set("config", config).catch(&ctx)?;
let nodes_data = node_ctx.input.dot("$nodes").unwrap_or_default();
let nodes_data = node_ctx.nodes.clone().unwrap_or_default();
ctx.globals()
.set(
+11 -9
View File
@@ -1,14 +1,13 @@
use crate::decision_graph::cleaner::ZEN_RESERVED_PROPERTIES;
use crate::nodes::function::v2::error::ResultExt;
use ahash::{HashMap, HashMapExt};
use nohash_hasher::BuildNoHashHasher;
use rquickjs::{Ctx, FromJs, IntoJs, Type, Value as QValue};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde_json::json;
use std::collections::HashMap as StdHashMap;
use std::rc::Rc;
use zen_expression::variable::Variable;
use zen_expression::variable::{Variable, VariableMap};
use zen_types::rccell::RcCell;
#[derive(Debug)]
pub(crate) struct JsValue(pub(crate) Variable);
@@ -40,7 +39,7 @@ impl<'js> FromJs<'js> for JsValue {
v.into_string()
.map(|s| s.to_string().ok())
.flatten()
.map(|s| Rc::from(s.as_str()))
.map(|s| zen_types::symbol::Symbol::from(s.as_str()))
.or_throw_msg(ctx, "failed to convert to string")?,
),
Type::Array => {
@@ -60,14 +59,17 @@ impl<'js> FromJs<'js> for JsValue {
.into_object()
.or_throw_msg(ctx, "failed to convert to object")?;
let mut js_object = HashMap::with_capacity(object.len());
let mut js_object = VariableMap::new();
for p in object.props::<String, QValue>() {
let (k, v) = p?;
if ZEN_RESERVED_PROPERTIES.contains(&k.as_str()) {
continue;
}
js_object.insert(Rc::from(k.as_str()), JsValue::from_js(ctx, v)?.0);
js_object.insert(
zen_types::symbol::Symbol::from(k.as_str()),
JsValue::from_js(ctx, v)?.0,
);
}
Variable::from_object(js_object)
@@ -167,7 +169,7 @@ impl<'r, 'js> JsConverter<'r, 'js> {
Variable::Number(n) => n.to_f64().into_js(self.ctx),
Variable::String(str) => str.into_js(self.ctx),
Variable::Array(a) => {
let addr = Rc::as_ptr(&a) as *const () as usize;
let addr = RcCell::as_ptr(&a) as *const () as usize;
if let Some(cached) = self.cache.get(&addr) {
return Ok(cached.clone());
}
@@ -183,7 +185,7 @@ impl<'r, 'js> JsConverter<'r, 'js> {
Ok(val)
}
Variable::Object(o) => {
let addr = Rc::as_ptr(&o) as *const () as usize;
let addr = RcCell::as_ptr(&o) as *const () as usize;
if let Some(cached) = self.cache.get(&addr) {
return Ok(cached.clone());
}
@@ -191,7 +193,7 @@ impl<'r, 'js> JsConverter<'r, 'js> {
let qmap = rquickjs::Object::new(self.ctx.clone())?;
let obj = o.borrow();
for (key, value) in obj.iter() {
qmap.set(key.as_ref(), self.convert_with_cache(value)?)?;
qmap.set(key.as_str(), self.convert_with_cache(value)?)?;
}
let val = qmap.into_value();
+1 -3
View File
@@ -1,4 +1,3 @@
use crate::decision_graph::cleaner::VariableCleaner;
use crate::nodes::definition::NodeHandler;
use crate::nodes::result::NodeResult;
use crate::nodes::NodeContext;
@@ -17,8 +16,7 @@ impl NodeHandler for InputNodeHandler {
async fn handle(&self, ctx: NodeContext<Self::NodeData, Self::TraceData>) -> NodeResult {
if let Some(json_schema) = &ctx.node.schema {
let input_json = VariableCleaner::new().clone_clean(&ctx.input).to_value();
ctx.validate(json_schema, &input_json)?;
ctx.validate(json_schema, &ctx.input)?;
};
ctx.success(ctx.input.clone())
+1
View File
@@ -11,6 +11,7 @@ pub mod output;
mod result;
pub(crate) mod transform_attributes;
pub(crate) mod validator_cache;
pub mod variable_json;
pub use context::{NodeContext, NodeContextBase, NodeContextConfig, NodeContextExt};
pub use definition::NodeHandler;
+1 -3
View File
@@ -1,4 +1,3 @@
use crate::decision_graph::cleaner::VariableCleaner;
use crate::nodes::definition::NodeHandler;
use crate::nodes::result::NodeResult;
use crate::nodes::NodeContext;
@@ -17,8 +16,7 @@ impl NodeHandler for OutputNodeHandler {
async fn handle(&self, ctx: NodeContext<Self::NodeData, Self::TraceData>) -> NodeResult {
if let Some(json_schema) = &ctx.node.schema {
let input_json = VariableCleaner::new().clone_clean(&ctx.input).to_value();
ctx.validate(json_schema, &input_json)?;
ctx.validate(json_schema, &ctx.input)?;
};
ctx.success(ctx.input.clone())
+4 -29
View File
@@ -3,7 +3,7 @@ use crate::nodes::result::NodeResult;
use crate::nodes::{NodeContextBase, NodeContextExt};
use std::future::Future;
use std::ops::Deref;
use zen_expression::{Isolate, Variable};
use zen_expression::Variable;
pub(crate) trait TransformAttributesExecution {
async fn run_with<F, Fut>(&self, ctx: NodeContextBase, evaluate: F) -> NodeResult
@@ -21,33 +21,10 @@ impl TransformAttributesExecution for TransformAttributes {
let input = match &self.input_field {
None => ctx.input.clone(),
Some(input_field) => {
let mut isolate = Isolate::new();
isolate.set_environment(ctx.input.clone());
let calculated_input = isolate
let mut isolate = ctx.isolate();
isolate
.run_standard(input_field.deref())
.node_context_message(&ctx, "Failed to evaluate expression")?;
let nodes = ctx.input.dot("$nodes").unwrap_or(Variable::Null);
match &calculated_input {
Variable::Array(arr) => {
let arr = arr.borrow();
let s: Vec<_> = arr
.iter()
.map(|v| {
let new_v = v.depth_clone(1);
new_v.dot_insert("$nodes", nodes.clone());
new_v
})
.collect();
Variable::from_array(s)
}
_ => {
let new_input = calculated_input.depth_clone(1);
new_input.dot_insert("$nodes", nodes);
new_input
}
}
.node_context_message(&ctx, "Failed to evaluate expression")?
}
};
@@ -61,7 +38,6 @@ impl TransformAttributesExecution for TransformAttributes {
});
}
response.output.dot_remove("$nodes");
response.output
}
TransformExecutionMode::Loop => {
@@ -90,7 +66,6 @@ impl TransformAttributesExecution for TransformAttributes {
response.output = input.clone().merge_clone(&response.output);
}
response.output.dot_remove("$nodes");
output_array.push(response.output);
}
+19 -4
View File
@@ -1,3 +1,4 @@
use crate::nodes::variable_json::VariableJson;
use ahash::HashMap;
use anyhow::Context;
use jsonschema::Validator;
@@ -6,16 +7,26 @@ use std::sync::{Arc, RwLock};
#[derive(Clone, Default, Debug)]
pub struct ValidatorCache {
inner: Arc<RwLock<HashMap<u64, Arc<Validator>>>>,
inner: Arc<RwLock<HashMap<u64, Arc<Validator<VariableJson>>>>>,
}
impl PartialEq for ValidatorCache {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl ValidatorCache {
pub fn get(&self, key: u64) -> Option<Arc<Validator>> {
pub fn get(&self, key: u64) -> Option<Arc<Validator<VariableJson>>> {
let read = self.inner.read().ok()?;
read.get(&key).cloned()
}
pub fn get_or_insert(&self, key: u64, schema: &Value) -> anyhow::Result<Arc<Validator>> {
pub fn get_or_insert(
&self,
key: u64,
schema: &Value,
) -> anyhow::Result<Arc<Validator<VariableJson>>> {
if let Some(v) = self.get(key) {
return Ok(v);
}
@@ -25,7 +36,11 @@ impl ValidatorCache {
.write()
.ok()
.context("Failed to acquire lock on validator cache")?;
let validator = Arc::new(jsonschema::draft7::new(&schema)?);
let validator = Arc::new(
jsonschema::options_for::<VariableJson>()
.with_draft(jsonschema::Draft::Draft7)
.build(schema)?,
);
w_shared.insert(key, validator.clone());
Ok(validator)
+272
View File
@@ -0,0 +1,272 @@
use std::borrow::Cow;
use std::cell::Ref;
use jsonschema::json::{Array, Json, JsonNumber, Node, NodeIdentity, Object};
use jsonschema::JsonType;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use self_cell::self_cell;
use serde_json::Value;
use typed_arena::Arena;
use zen_types::variable::{MapIter, RcCell, Variable, VariableMap};
pub struct VariableJson;
type MapRef<'a> = Ref<'a, VariableMap>;
type VecRef<'a> = Ref<'a, Vec<Variable>>;
self_cell!(
struct MapGuard {
owner: RcCell<VariableMap>,
#[covariant]
dependent: MapRef,
}
);
self_cell!(
struct VecGuard {
owner: RcCell<Vec<Variable>>,
#[covariant]
dependent: VecRef,
}
);
pub struct Guards {
objects: Arena<MapGuard>,
arrays: Arena<VecGuard>,
}
impl Default for Guards {
fn default() -> Self {
Self {
objects: Arena::new(),
arrays: Arena::new(),
}
}
}
#[derive(Clone, Copy)]
pub struct VariableNode<'a> {
var: &'a Variable,
guards: &'a Guards,
}
impl<'a> VariableNode<'a> {
pub fn new(var: &'a Variable, guards: &'a Guards) -> Self {
Self { var, guards }
}
}
impl Json for VariableJson {
type Node<'a> = VariableNode<'a>;
type PreparedKey = Box<str>;
type StringBuffer = Variable;
fn prepare_key(key: &str) -> Box<str> {
Box::from(key)
}
fn with_string_node<T>(
buffer: &mut Variable,
string: &str,
f: impl FnOnce(VariableNode<'_>) -> T,
) -> T {
*buffer = Variable::String((string).into());
let guards = Guards::default();
f(VariableNode::new(buffer, &guards))
}
}
pub struct VariableNumber(Decimal);
impl JsonNumber for VariableNumber {
fn as_u64(&self) -> Option<u64> {
self.0.is_integer().then(|| self.0.to_u64()).flatten()
}
fn as_i64(&self) -> Option<i64> {
self.0.is_integer().then(|| self.0.to_i64()).flatten()
}
fn as_f64(&self) -> Option<f64> {
self.0.to_f64()
}
fn as_str(&self) -> Cow<'_, str> {
Cow::Owned(self.0.normalize().to_string())
}
fn to_number(&self) -> Cow<'_, serde_json::Number> {
let number = self
.0
.to_f64()
.and_then(serde_json::Number::from_f64)
.unwrap_or_else(|| serde_json::Number::from(0));
Cow::Owned(number)
}
}
impl<'a> Node<'a, VariableJson> for VariableNode<'a> {
type Object = ObjectNode<'a>;
type Array = ArrayNode<'a>;
type Number = VariableNumber;
fn as_object(&self) -> Option<ObjectNode<'a>> {
let Variable::Object(cell) = self.var else {
return None;
};
let guard = self
.guards
.objects
.alloc(MapGuard::new(cell.clone(), |cell| cell.borrow()));
Some(ObjectNode {
map: guard.borrow_dependent(),
guards: self.guards,
})
}
fn as_array(&self) -> Option<ArrayNode<'a>> {
let Variable::Array(cell) = self.var else {
return None;
};
let guard = self
.guards
.arrays
.alloc(VecGuard::new(cell.clone(), |cell| cell.borrow()));
Some(ArrayNode {
items: guard.borrow_dependent().as_slice(),
guards: self.guards,
})
}
fn as_string(&self) -> Option<Cow<'a, str>> {
match self.var {
Variable::String(string) => Some(Cow::Borrowed(string.as_str())),
Variable::Dynamic(dynamic) => Some(Cow::Owned(dynamic.to_string())),
_ => None,
}
}
fn as_number(&self) -> Option<VariableNumber> {
match self.var {
Variable::Number(number) => Some(VariableNumber(*number)),
_ => None,
}
}
fn as_boolean(&self) -> Option<bool> {
match self.var {
Variable::Bool(boolean) => Some(*boolean),
_ => None,
}
}
fn is_null(&self) -> bool {
matches!(self.var, Variable::Null)
}
fn json_type(&self) -> JsonType {
match self.var {
Variable::Null => JsonType::Null,
Variable::Bool(_) => JsonType::Boolean,
Variable::Number(_) => JsonType::Number,
Variable::String(_) => JsonType::String,
Variable::Array(_) => JsonType::Array,
Variable::Object(_) => JsonType::Object,
Variable::Dynamic(_) => JsonType::String,
}
}
fn to_value(&self) -> Cow<'a, Value> {
Cow::Owned(Variable::to_value(self.var))
}
fn identity(&self) -> Option<NodeIdentity> {
Some(NodeIdentity::new(
std::ptr::from_ref::<Variable>(self.var) as usize
))
}
}
pub struct ObjectNode<'a> {
map: &'a VariableMap,
guards: &'a Guards,
}
impl<'a> Object<'a, VariableJson> for ObjectNode<'a> {
type Node = VariableNode<'a>;
type MemberName = &'a str;
type MembersIter = VariableMembersIter<'a>;
fn len(&self) -> usize {
self.map.len()
}
fn get(&self, key: &Box<str>) -> Option<VariableNode<'a>> {
self.map
.get_str(key)
.map(|var| VariableNode::new(var, self.guards))
}
fn members(&self) -> VariableMembersIter<'a> {
VariableMembersIter {
iter: self.map.iter(),
guards: self.guards,
}
}
}
pub struct VariableMembersIter<'a> {
iter: MapIter<'a>,
guards: &'a Guards,
}
impl<'a> Iterator for VariableMembersIter<'a> {
type Item = (&'a str, VariableNode<'a>);
fn next(&mut self) -> Option<Self::Item> {
self.iter
.next()
.map(|(name, value)| (name.as_str(), VariableNode::new(value, self.guards)))
}
}
pub struct ArrayNode<'a> {
items: &'a [Variable],
guards: &'a Guards,
}
impl<'a> Array<'a, VariableJson> for ArrayNode<'a> {
type Node = VariableNode<'a>;
type ElementsIter = VariableElementsIter<'a>;
fn len(&self) -> usize {
self.items.len()
}
fn elements(&self) -> VariableElementsIter<'a> {
VariableElementsIter {
iter: self.items.iter(),
guards: self.guards,
}
}
}
pub struct VariableElementsIter<'a> {
iter: std::slice::Iter<'a, Variable>,
guards: &'a Guards,
}
impl<'a> Iterator for VariableElementsIter<'a> {
type Item = VariableNode<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.iter
.next()
.map(|var| VariableNode::new(var, self.guards))
}
}
+3 -1
View File
@@ -451,7 +451,9 @@ impl ExecutionContext<'_> {
};
let segment = &path[..path.find('.').unwrap_or(path.len())];
let store_fields = store_fields.borrow();
let Some((key, value)) = store_fields.get_key_value(segment) else {
let Some((key, value)) =
store_fields.get_key_value(&zen_types::symbol::Symbol::from(segment))
else {
return;
};
env_fields
+4 -123
View File
@@ -2,7 +2,6 @@ use std::sync::{Arc, OnceLock};
use ahash::{HashMap, HashSet};
use fixedbitset::FixedBitSet;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use zen_expression::intellisense::{ArmTest, IntelliSense, NumberCover};
use zen_expression::variable::{Variable, VariableType};
@@ -26,6 +25,7 @@ use super::{
Block, BlockKind, BlockReadPlan, CellReads, ConditionalReads, ExpressionLocation, ParseContext,
ReadFlattenFn, WriteSite, WriteTarget,
};
use crate::nodes::decision_table::index::TableIndex;
pub(crate) struct TableSelection {
pub(crate) matched_rows: Vec<u32>,
@@ -101,127 +101,6 @@ pub struct DecisionTableIr {
index: OnceLock<Option<TableIndex>>,
}
const MIN_INDEX_ROWS: usize = 8;
#[derive(Debug, Clone)]
struct TableIndex {
columns: Vec<Option<ColumnIndex>>,
}
#[derive(Debug, Clone)]
struct ColumnIndex {
strings: HashMap<Arc<str>, FixedBitSet>,
numbers: HashMap<Decimal, FixedBitSet>,
bools: HashMap<bool, FixedBitSet>,
captured: FixedBitSet,
fallback: FixedBitSet,
}
impl TableIndex {
fn build(table: &DecisionTableIr) -> Option<TableIndex> {
let rows = table.rules.len();
if rows < MIN_INDEX_ROWS {
return None;
}
let mut intellisense = IntelliSense::new();
let columns: Vec<Option<ColumnIndex>> = table
.inputs
.iter()
.map(|col| ColumnIndex::build(table, col, rows, &mut intellisense))
.collect();
columns
.iter()
.any(Option::is_some)
.then_some(TableIndex { columns })
}
fn decides(&self, col_idx: usize, row_idx: usize) -> bool {
self.columns
.get(col_idx)
.and_then(Option::as_ref)
.is_some_and(|c| c.captured.contains(row_idx))
}
}
impl ColumnIndex {
fn build(
table: &DecisionTableIr,
col: &DecisionTableInputField,
rows: usize,
intellisense: &mut IntelliSense,
) -> Option<ColumnIndex> {
if col.field.as_deref().is_none_or(|f| f.is_empty()) {
return None;
}
let mut strings: HashMap<Arc<str>, FixedBitSet> = HashMap::default();
let mut numbers: HashMap<Decimal, FixedBitSet> = HashMap::default();
let mut bools: HashMap<bool, FixedBitSet> = HashMap::default();
let mut captured = FixedBitSet::with_capacity(rows);
let mut fallback = FixedBitSet::with_capacity(rows);
for (row_idx, rule) in table.rules.iter().enumerate() {
let Some(cell) = rule.get(&col.id).filter(|c| !c.is_empty()) else {
fallback.insert(row_idx);
continue;
};
match intellisense.cell_test(cell) {
ArmTest::Enum { values, .. } => {
for value in values {
strings
.entry(Arc::from(value.as_ref()))
.or_insert_with(|| FixedBitSet::with_capacity(rows))
.insert(row_idx);
}
captured.insert(row_idx);
}
ArmTest::Bool { values, .. } => {
for value in values {
bools
.entry(value)
.or_insert_with(|| FixedBitSet::with_capacity(rows))
.insert(row_idx);
}
captured.insert(row_idx);
}
ArmTest::Number { cover, .. } => match cover.points() {
Some(points) => {
for point in points {
numbers
.entry(point.normalize())
.or_insert_with(|| FixedBitSet::with_capacity(rows))
.insert(row_idx);
}
captured.insert(row_idx);
}
None => {
fallback.insert(row_idx);
}
},
ArmTest::Default | ArmTest::Unrecognized => {
fallback.insert(row_idx);
}
}
}
(captured.count_ones(..) > 0).then_some(ColumnIndex {
strings,
numbers,
bools,
captured,
fallback,
})
}
fn rows_for(&self, value: &Variable) -> Option<&FixedBitSet> {
match value {
Variable::String(s) => self.strings.get(s.as_ref()),
Variable::Number(n) => self.numbers.get(&n.normalize()),
Variable::Bool(b) => self.bools.get(b),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct OutputColumn {
pub id: Arc<str>,
@@ -1173,7 +1052,9 @@ impl DecisionTableIr {
}
fn table_index(&self) -> Option<&TableIndex> {
self.index.get_or_init(|| TableIndex::build(self)).as_ref()
self.index
.get_or_init(|| TableIndex::build(&self.inputs, &self.rules))
.as_ref()
}
fn candidate_rows(
+17 -13
View File
@@ -2,9 +2,11 @@ use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use zen_types::symbol::Symbol;
use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
use zen_expression::variable::Variable;
use zen_types::rccell::RcCell;
use zen_expression::{Isolate, OpcodeCache};
@@ -302,7 +304,9 @@ impl PhaseScope {
let scoped_fields = self.scoped.as_object()?;
let needs_owner = match (owner_binding, instance.as_object()) {
(Some((name, _)), Some(fields)) => !fields.borrow().contains_key(name.as_str()),
(Some((name, _)), Some(fields)) => {
!fields.borrow().contains_key(&Symbol::from(name.as_str()))
}
_ => false,
};
@@ -310,7 +314,7 @@ impl PhaseScope {
let wrapper = instance.depth_clone(1);
let synthetic_owner = match (owner_binding, wrapper.as_object()) {
(Some((name, owner_var)), Some(wrapper_fields)) => {
let key: Rc<str> = Rc::from(name.as_str());
let key = Symbol::from(name.as_str());
let injected = owner_var.shallow_clone();
wrapper_fields
.borrow_mut()
@@ -333,8 +337,8 @@ impl PhaseScope {
{
let mut fields = scoped_fields.borrow_mut();
fields.remove("$");
fields.insert(self.entity_key.clone(), bound);
fields.remove(&Variable::dollar_key());
fields.insert(Symbol::from(self.entity_key.as_ref()), bound);
}
Some(slot)
}
@@ -344,7 +348,7 @@ enum InstanceSlot {
Direct,
Wrapped {
wrapper: Variable,
synthetic_owner: Option<(Rc<str>, Variable)>,
synthetic_owner: Option<(Symbol, Variable)>,
},
}
@@ -363,7 +367,7 @@ impl InstanceSlot {
let written = written.borrow();
let mut target = target.borrow_mut();
for (key, value) in written.iter() {
if Self::is_injected_owner(synthetic_owner, key, value) {
if Self::is_injected_owner(synthetic_owner, key.clone(), value) {
continue;
}
target.insert(key.clone(), value.shallow_clone());
@@ -371,12 +375,12 @@ impl InstanceSlot {
}
fn is_injected_owner(
synthetic_owner: &Option<(Rc<str>, Variable)>,
key: &Rc<str>,
synthetic_owner: &Option<(Symbol, Variable)>,
key: Symbol,
value: &Variable,
) -> bool {
match synthetic_owner {
Some((owner_key, injected)) if owner_key.as_ref() == key.as_ref() => {
Some((owner_key, injected)) if owner_key.as_str() == key.as_str() => {
Self::same_ref(value, injected)
}
_ => false,
@@ -385,9 +389,9 @@ impl InstanceSlot {
fn same_ref(a: &Variable, b: &Variable) -> bool {
match (a, b) {
(Variable::Object(x), Variable::Object(y)) => Rc::ptr_eq(x, y),
(Variable::Array(x), Variable::Array(y)) => Rc::ptr_eq(x, y),
(Variable::String(x), Variable::String(y)) => Rc::ptr_eq(x, y),
(Variable::Object(x), Variable::Object(y)) => RcCell::ptr_eq(x, y),
(Variable::Array(x), Variable::Array(y)) => RcCell::ptr_eq(x, y),
(Variable::String(x), Variable::String(y)) => x == y,
_ => a == b,
}
}
@@ -419,7 +423,7 @@ impl<'a> Driver<'a> {
fn bind_env(&self, isolate: &RefCell<Isolate>) {
if let Some(fields) = self.env.as_object() {
fields.borrow_mut().remove("$");
fields.borrow_mut().remove(&Variable::dollar_key());
}
isolate
.borrow_mut()
+1 -1
View File
@@ -765,7 +765,7 @@ impl VariableTypeScope for VariableType {
return self.shallow_clone();
};
let mut fields: HashMap<Rc<str>, VariableType> = obj.borrow().clone();
fields.insert(Variable::dollar_key(), field_type.shallow_clone());
fields.insert(Variable::dollar_key_rc(), field_type.shallow_clone());
VariableType::Object(Rc::new(RefCell::new(fields)))
}
+12 -14
View File
@@ -144,21 +144,19 @@ impl CompiledSet {
workspace.set_policy_arc(key.clone(), policy.0.clone());
policy_keys.push(key.clone());
}
DecisionContent::Graph(graph) => {
match Decision::from(Arc::new(graph.clone())).validate() {
Err(error) => failures.push(CompileFailure {
key: key.clone(),
kind: "graph",
diagnostics: Vec::new(),
error: Some(error.to_string()),
}),
Ok(()) => {
let mut compiled = graph.clone();
compiled.compile();
entries.insert(key.clone(), CompiledEntry::Graph(Arc::new(compiled)));
}
DecisionContent::Graph(graph) => match Decision::from(graph.clone()).validate() {
Err(error) => failures.push(CompileFailure {
key: key.clone(),
kind: "graph",
diagnostics: Vec::new(),
error: Some(error.to_string()),
}),
Ok(()) => {
let mut compiled = graph.clone();
Arc::make_mut(&mut compiled).compile();
entries.insert(key.clone(), CompiledEntry::Graph(compiled));
}
}
},
}
}
+1 -1
View File
@@ -153,7 +153,7 @@ impl InputValidator<'_> {
let Some(prop) = dm_props
.properties
.iter()
.find(|p| p.name.as_ref() == key.as_ref())
.find(|p| *p.name == *key.as_str())
else {
continue;
};
+14 -4
View File
@@ -1,4 +1,3 @@
use std::rc::Rc;
use std::sync::Arc;
use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
@@ -42,7 +41,12 @@ impl EnhanceState<'_> {
trace
.values()
.filter(|entry| entry.order < node_trace.order)
.map(|entry| (Rc::from(entry.name.as_ref()), entry.output.clone()))
.map(|entry| {
(
zen_types::symbol::Symbol::from(entry.name.as_ref()),
entry.output.clone(),
)
})
.collect(),
);
let base = node_trace.input.depth_clone(1);
@@ -618,7 +622,10 @@ fn operand_values(
/// those entries — read the map key directly.
fn expression_trace_result(entry: &Variable, key: &str) -> Option<Variable> {
let obj = entry.as_object()?;
let slot = obj.borrow().get(key)?.shallow_clone();
let slot = obj
.borrow()
.get(&zen_types::symbol::Symbol::from(key))?
.shallow_clone();
slot.dot("result")
}
@@ -645,7 +652,10 @@ fn shallow_writes(input: &Variable, output: &Variable) -> Vec<WriteTrace> {
if key.starts_with('$') {
continue;
}
if input.dot(key).is_some_and(|previous| previous == *value) {
if input
.dot(key.as_str())
.is_some_and(|previous| previous == *value)
{
continue;
}
writes.push(WriteTrace {
+91 -6
View File
@@ -209,12 +209,11 @@ async fn engine_function_imports() {
})
.collect::<Vec<_>>();
let function_content = GraphContent {
edges: function_content.edges,
nodes: new_nodes,
imports: Vec::new(),
compiled_cache: None,
stripped_functions: None,
let function_content = {
let mut content = GraphContent::default();
content.edges = function_content.edges;
content.nodes = new_nodes;
content
};
let decision = DecisionEngine::default()
.create_decision(Arc::new(function_content.into()))
@@ -497,3 +496,89 @@ async fn test_nodes_reference() {
})
);
}
#[tokio::test]
async fn decision_table_index_matches_linear_semantics() {
let table_node = json!({
"id": "dt-node",
"type": "decisionTableNode",
"name": "dt",
"content": {
"hitPolicy": "first",
"inputs": [
{"id": "c1", "name": "Tier", "field": "tier"},
{"id": "c2", "name": "Amount", "field": "amount"},
{"id": "c3", "name": "Active", "field": "active"}
],
"outputs": [{"id": "o1", "name": "Rate", "field": "rate"}],
"rules": [
{"_id": "r1", "c1": "'gold'", "c2": "100", "c3": "true", "o1": "1"},
{"_id": "r2", "c1": "'gold'", "c2": "> 500", "c3": "", "o1": "2"},
{"_id": "r3", "c1": "'silver'", "c2": "[100..200]", "c3": "", "o1": "3"},
{"_id": "r4", "c1": "'silver', 'bronze'", "c2": "", "c3": "false", "o1": "4"},
{"_id": "r5", "c1": "", "c2": "42", "c3": "", "o1": "5"},
{"_id": "r6", "c1": "'gold'", "c2": "", "c3": "", "o1": "6"},
{"_id": "r7", "c1": "'bronze'", "c2": "7, 8, 9", "c3": "true", "o1": "7"},
{"_id": "r8", "c1": "", "c2": "", "c3": "", "o1": "8"},
{"_id": "r9", "c1": "'platinum'", "c2": "1000", "c3": "true", "o1": "9"}
]
}
});
let graph = json!({
"nodes": [
{"id": "in", "type": "inputNode", "name": "request"},
table_node,
{"id": "out", "type": "outputNode", "name": "response"}
],
"edges": [
{"id": "e1", "sourceId": "in", "targetId": "dt-node"},
{"id": "e2", "sourceId": "dt-node", "targetId": "out"}
]
});
let probes = [
json!({"tier": "gold", "amount": 100, "active": true}),
json!({"tier": "gold", "amount": 600, "active": false}),
json!({"tier": "silver", "amount": 150, "active": true}),
json!({"tier": "silver", "amount": 50, "active": false}),
json!({"tier": "bronze", "amount": 8, "active": true}),
json!({"tier": "unknown", "amount": 42, "active": false}),
json!({"tier": "unknown", "amount": 0, "active": false}),
json!({"tier": "platinum", "amount": 1000, "active": true}),
json!({"tier": 5, "amount": "x", "active": null}),
];
for hit_policy in ["first", "collect"] {
let mut graph = graph.clone();
graph["nodes"][1]["content"]["hitPolicy"] = json!(hit_policy);
let mut content: GraphContent = serde_json::from_value(graph).unwrap();
content.compile();
let decision = DecisionEngine::default()
.create_decision(Arc::new(content.into()))
.unwrap();
for probe in &probes {
let indexed = decision
.evaluate(Variable::from(probe))
.await
.unwrap()
.result;
let linear = decision
.evaluate_with_opts(
Variable::from(probe),
EvaluationOptions {
trace: true,
max_depth: 5,
},
)
.await
.unwrap()
.result;
assert_eq!(
serde_json::to_value(&indexed).unwrap(),
serde_json::to_value(&linear).unwrap(),
"hit_policy={hit_policy} probe={probe}"
);
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ pub fn load_raw_test_data(key: &str) -> BufReader<File> {
pub fn load_test_data(key: &str) -> GraphContent {
let content: DecisionContent = serde_json::from_reader(load_raw_test_data(key)).unwrap();
match content {
DecisionContent::Graph(g) => g,
DecisionContent::Graph(g) => (*g).clone(),
DecisionContent::Policy(_) => {
panic!("expected graph test fixture, got policy: {key}")
}
+1
View File
@@ -8,6 +8,7 @@ edition = "2021"
repository = "https://github.com/gorules/zen.git"
[dependencies]
smallvec = { version = "1", features = ["union"] }
anyhow = { workspace = true }
ahash = { workspace = true }
bumpalo = { workspace = true, features = ["collections"] }
+1 -1
View File
@@ -17,7 +17,7 @@ pub fn evaluate_unary_expression(
};
let context_object = context_object_ref.borrow();
if !context_object.contains_key("$") {
if !context_object.contains_key(&Variable::dollar_key()) {
return Err(IsolateError::MissingContextReference);
}
+15 -7
View File
@@ -1,4 +1,5 @@
use crate::compiler::Opcode;
use crate::scope::Scope;
use crate::vm::VM;
use crate::{IsolateError, Variable};
use std::marker::PhantomData;
@@ -62,7 +63,15 @@ impl Expression<Standard> {
}
pub fn evaluate_with(&self, context: Variable, vm: &mut VM) -> Result<Variable, IsolateError> {
let output = vm.run(self.bytecode.as_ref(), context)?;
self.evaluate_with_scope(&Scope::new(context), vm)
}
pub fn evaluate_with_scope(
&self,
scope: &Scope,
vm: &mut VM,
) -> Result<Variable, IsolateError> {
let output = vm.run(self.bytecode.as_ref(), scope)?;
Ok(output)
}
}
@@ -85,17 +94,16 @@ impl Expression<Unary> {
}
pub fn evaluate_with(&self, context: Variable, vm: &mut VM) -> Result<bool, IsolateError> {
let Some(context_object_ref) = context.as_object() else {
return Err(IsolateError::MissingContextReference);
};
self.evaluate_with_scope(&Scope::new(context), vm)
}
let context_object = context_object_ref.borrow();
if !context_object.contains_key("$") {
pub fn evaluate_with_scope(&self, scope: &Scope, vm: &mut VM) -> Result<bool, IsolateError> {
if scope.get(&Variable::dollar_key()).is_none() {
return Err(IsolateError::MissingContextReference);
}
let output = vm
.run(self.bytecode.as_ref(), context)?
.run(self.bytecode.as_ref(), scope)?
.as_bool()
.ok_or_else(|| IsolateError::ValueCastError)?;
Ok(output)
+3 -7
View File
@@ -1,16 +1,12 @@
use crate::variable::DynamicVariable;
use crate::Variable;
use ahash::HashMap;
use anyhow::Context;
use rust_decimal::Decimal;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use zen_types::rccell::RcCell;
pub struct Arguments<'a>(pub &'a [Variable]);
type RcCell<T> = Rc<RefCell<T>>;
impl<'a> Deref for Arguments<'a> {
type Target = [Variable];
@@ -92,7 +88,7 @@ impl<'a> Arguments<'a> {
pub fn oobject(
&self,
pos: usize,
) -> anyhow::Result<Option<RcCell<HashMap<Rc<str>, Variable>>>> {
) -> anyhow::Result<Option<RcCell<crate::variable::VariableMap>>> {
match self.ovar(pos) {
Some(v) => v
.as_object()
@@ -102,7 +98,7 @@ impl<'a> Arguments<'a> {
}
}
pub fn object(&self, pos: usize) -> anyhow::Result<RcCell<HashMap<Rc<str>, Variable>>> {
pub fn object(&self, pos: usize) -> anyhow::Result<RcCell<crate::variable::VariableMap>> {
self.oobject(pos)?
.with_context(|| format!("Argument on {pos} position is not a valid object"))
}
+2 -2
View File
@@ -266,7 +266,7 @@ mod imp {
let format = args.ostr(1)?;
let formatted = this.format(format);
Ok(V::String(Rc::from(formatted)))
Ok(V::String((formatted).into()))
}
pub fn start_of(args: Arguments) -> anyhow::Result<V> {
@@ -402,7 +402,7 @@ mod imp {
}
GetterOperation::IsLeapYear => V::Bool(dt.date_naive().leap_year()),
// String
GetterOperation::OffsetName => V::String(Rc::from(dt.timezone().name())),
GetterOperation::OffsetName => V::String((dt.timezone().name()).into()),
})
}),
})
+3 -3
View File
@@ -221,19 +221,19 @@ mod imp {
pub fn month_string(args: Arguments) -> anyhow::Result<V> {
let timestamp = args.var(0)?;
let time = __internal_convert_datetime(&timestamp)?;
Ok(V::String(Rc::from(time.format("%b").to_string())))
Ok(V::String((time.format("%b").to_string()).into()))
}
pub fn weekday_string(args: Arguments) -> anyhow::Result<V> {
let timestamp = args.var(0)?;
let time = __internal_convert_datetime(&timestamp)?;
Ok(V::String(Rc::from(time.weekday().to_string())))
Ok(V::String((time.weekday().to_string()).into()))
}
pub fn date_string(args: Arguments) -> anyhow::Result<V> {
let timestamp = args.var(0)?;
let time = __internal_convert_datetime(&timestamp)?;
Ok(V::String(Rc::from(time.to_string())))
Ok(V::String((time.to_string()).into()))
}
pub fn start_of(args: Arguments) -> anyhow::Result<V> {
+14 -14
View File
@@ -328,7 +328,6 @@ pub(crate) mod imp {
use crate::vm::date::DynamicVariableExt;
use crate::vm::VmDate;
use crate::{Variable as V, Variable};
use ahash::HashMapExt;
use anyhow::{anyhow, Context};
use chrono_tz::Tz;
#[cfg(not(feature = "regex-lite"))]
@@ -436,7 +435,7 @@ pub(crate) mod imp {
.iter()
.map(|c| c.map(|c| c.as_str()))
.filter_map(|c| c)
.map(|s| V::String(Rc::from(s)))
.map(|s| V::String((s).into()))
.collect()
})
.unwrap_or_default();
@@ -508,8 +507,7 @@ pub(crate) mod imp {
Ok(V::from_array(merged))
}
V::Object(_) => {
let mut merged: ahash::HashMap<Rc<str>, V> =
ahash::HashMap::with_capacity(capacity);
let mut merged = zen_types::variable::VariableMap::with_capacity(capacity);
for item in arr.iter() {
match item {
V::Object(obj) => {
@@ -555,8 +553,7 @@ pub(crate) mod imp {
(V::Object(a), V::Object(b)) => {
let a = a.borrow();
let b = b.borrow();
let mut merged: ahash::HashMap<Rc<str>, V> =
ahash::HashMap::with_capacity(a.len() + b.len());
let mut merged = zen_types::variable::VariableMap::with_capacity(a.len() + b.len());
for (key, value) in a.iter() {
merged.insert(key.clone(), value.clone());
@@ -724,7 +721,7 @@ pub(crate) mod imp {
V::Bool(v) => *v,
V::Number(n) => !n.is_zero(),
V::Array(_) | V::Object(_) | V::Dynamic(_) => true,
V::String(s) => match (*s).trim() {
V::String(s) => match s.as_str().trim() {
"true" => true,
"false" => false,
_ => s.is_empty(),
@@ -737,9 +734,9 @@ pub(crate) mod imp {
pub fn to_string(args: Arguments) -> anyhow::Result<V> {
let a = args.var(0)?;
let val = match a {
V::Null => Rc::from("null"),
V::Bool(v) => Rc::from(v.to_string().as_str()),
V::Number(n) => Rc::from(n.to_string().as_str()),
V::Null => "null".into(),
V::Bool(v) => v.to_string().into(),
V::Number(n) => n.to_string().into(),
V::String(s) => s.clone(),
_ => return Err(anyhow!("Cannot convert type {} to string", a.type_name())),
};
@@ -752,7 +749,7 @@ pub(crate) mod imp {
let val = match a {
V::Number(n) => *n,
V::String(str) => {
let s = str.trim();
let s = str.as_str().trim();
Decimal::from_str_exact(s)
.or_else(|_| Decimal::from_scientific(s))
.context("Invalid number")?
@@ -772,7 +769,7 @@ pub(crate) mod imp {
let is_ok = match a {
V::Number(_) => true,
V::String(str) => {
let s = str.trim();
let s = str.as_str().trim();
Decimal::from_str_exact(s)
.or_else(|_| Decimal::from_scientific(s))
.is_ok()
@@ -804,7 +801,7 @@ pub(crate) mod imp {
let b = args.var(1)?;
let val = match (a, b) {
(V::String(a), V::String(b)) => a.contains(b.as_ref()),
(V::String(a), V::String(b)) => a.contains(b.as_str()),
(V::Array(a), _) => {
let arr = a.borrow();
@@ -875,7 +872,10 @@ pub(crate) mod imp {
}
V::Object(a) => {
let obj = a.borrow();
let keys = obj.iter().map(|(key, _)| V::String(key.clone())).collect();
let keys = obj
.iter()
.map(|(key, _)| V::String((key.as_str()).into()))
.collect();
V::from_array(keys)
}
@@ -293,7 +293,7 @@ impl<'a> DependencyResolutionWalker<'a> {
fn extract_path_with_spans(&self, node: &Node) -> Option<(Vec<Rc<str>>, Vec<(u32, u32)>)> {
match node {
Node::Identifier(name) => Some((vec![Rc::from(*name)], vec![self.node_span(node)])),
Node::Root => Some((vec![Variable::root_key()], vec![self.node_span(node)])),
Node::Root => Some((vec![Variable::root_key_rc()], vec![self.node_span(node)])),
Node::Member { node: n, property } => {
let (mut path, mut spans) = self.extract_path_with_spans(n)?;
match property {
@@ -312,7 +312,7 @@ impl<'a> DependencyResolutionWalker<'a> {
fn collection_source_path(node: &Node) -> Option<Vec<Rc<str>>> {
match node {
Node::Identifier(name) => Some(vec![Rc::from(*name)]),
Node::Root => Some(vec![Variable::root_key()]),
Node::Root => Some(vec![Variable::root_key_rc()]),
Node::Member { .. } => Self::extract_read_path(node),
Node::Parenthesized(inner) => Self::collection_source_path(inner),
Node::Binary {
@@ -375,7 +375,7 @@ impl<'a> DependencyResolutionWalker<'a> {
return;
}
Node::Identifier(name) => Rc::from(*name),
Node::Root => Variable::root_key(),
Node::Root => Variable::root_key_rc(),
Node::Pointer => {
self.resolve_pointer_chain(&chain, scope);
return;
@@ -488,7 +488,7 @@ impl<'a> DependencyResolutionWalker<'a> {
}
fn reference_dollar_chain(&mut self, chain: &FlatChain, scope: &mut Scope) {
let mut path: Vec<Rc<str>> = vec![Rc::from("$")];
let mut path: Vec<Rc<str>> = vec![Variable::dollar_key_rc()];
let mut spans = vec![self.node_span(chain.root)];
let mut grouping = true;
for segment in &chain.segments {
@@ -674,7 +674,7 @@ impl<'a> DependencyResolutionWalker<'a> {
let (alias, inner_reads, inner_refs) = match closure_node {
Node::Closure { body, alias } => {
let mut inner_scope = scope.clone();
inner_scope.locals.insert(Variable::dollar_key());
inner_scope.locals.insert(Variable::dollar_key_rc());
inner_scope.pointer_collection = collection_source
.as_ref()
.filter(|source| !scope.is_local(source))
@@ -763,7 +763,7 @@ impl<'a> DependencyResolutionWalker<'a> {
Node::Closure { body, alias: _ } => {
let mut inner = scope.clone();
inner.locals.insert(Variable::dollar_key());
inner.locals.insert(Variable::dollar_key_rc());
inner.pointer_collection = None;
self.resolve(body, &mut inner);
}
@@ -837,7 +837,7 @@ impl<'a> DependencyResolutionWalker<'a> {
}
Node::Root => {
let path: Vec<Rc<str>> = vec![Variable::root_key()];
let path: Vec<Rc<str>> = vec![Variable::root_key_rc()];
let span = self.node_span(node);
self.reads.push(ReadDependency::Direct {
path: path.clone(),
@@ -866,7 +866,7 @@ impl<'a> DependencyResolutionWalker<'a> {
fn extract_read_path(node: &Node) -> Option<Vec<Rc<str>>> {
match node {
Node::Identifier(name) => Some(vec![Rc::from(*name)]),
Node::Root => Some(vec![Variable::root_key()]),
Node::Root => Some(vec![Variable::root_key_rc()]),
Node::Member { node, property } => {
let mut path = Self::extract_read_path(node)?;
match property {
+30 -30
View File
@@ -8,10 +8,12 @@ use crate::compiler::{Compiler, CompilerError, Opcode};
use crate::expression::{OpcodeCache, Standard, Unary};
use crate::lexer::{Lexer, LexerError};
use crate::parser::{Parser, ParserError};
use crate::scope::Scope;
use crate::variable::Variable;
use crate::vm::{VMError, VM};
use crate::{Expression, ExpressionKind};
use bumpalo::Bump;
use zen_types::symbol::Symbol;
/// Isolate is a component that encapsulates an isolated environment for executing expressions.
///
@@ -26,7 +28,7 @@ pub struct Isolate {
bump: Bump,
environment: Option<Variable>,
scope: Scope,
references: HashMap<String, Variable>,
cache: Option<Arc<OpcodeCache>>,
}
@@ -40,7 +42,7 @@ impl Isolate {
bump: Bump::new(),
environment: None,
scope: Scope::default(),
references: Default::default(),
cache: None,
}
@@ -59,7 +61,7 @@ impl Isolate {
}
pub fn set_environment(&mut self, variable: Variable) {
self.environment.replace(variable);
self.scope.set_base(variable);
self.references.clear();
}
@@ -67,11 +69,26 @@ impl Isolate {
self.cache = Some(cache);
}
pub fn update_environment<F>(&mut self, mut updater: F)
where
F: FnMut(Option<&mut Variable>),
{
updater(self.environment.as_mut());
pub fn scope(&self) -> &Scope {
&self.scope
}
pub fn set_local(&mut self, name: Symbol, value: Variable) {
self.scope.set_local(name, value);
}
pub fn insert_dollar(&mut self, path: &str, value: Variable) {
let dollar = match self.scope.local(&Variable::dollar_key()) {
Some(existing @ Variable::Object(_)) => existing.shallow_clone(),
_ => {
let created = Variable::empty_object();
self.scope
.set_local(Variable::dollar_key(), created.clone());
created
}
};
let _ = dollar.dot_insert(path, value);
}
pub fn set_reference(&mut self, reference: &str) -> Result<(), IsolateError> {
@@ -89,16 +106,7 @@ impl Isolate {
}
pub fn set_reference_value(&mut self, value: Variable) -> Result<(), IsolateError> {
if !matches!(&mut self.environment, Some(Variable::Object(_))) {
self.environment.replace(Variable::empty_object());
}
let Some(Variable::Object(environment_object_ref)) = &self.environment else {
return Err(IsolateError::ReferenceError);
};
let mut environment_object = environment_object_ref.borrow_mut();
environment_object.insert(Variable::dollar_key(), value);
self.scope.set_local(Variable::dollar_key(), value);
Ok(())
}
@@ -145,16 +153,12 @@ impl Isolate {
self.run_internal(source, ExpressionKind::Standard)?;
let bytecode = self.compiler.get_bytecode();
let result = self
.vm
.run(bytecode, self.environment.clone().unwrap_or(Variable::Null))?;
let result = self.vm.run(bytecode, &self.scope)?;
Ok(result)
}
pub fn run_compiled(&mut self, source: &[Opcode]) -> Result<Variable, IsolateError> {
let result = self
.vm
.run(source, self.environment.clone().unwrap_or(Variable::Null))?;
let result = self.vm.run(source, &self.scope)?;
Ok(result)
}
@@ -178,17 +182,13 @@ impl Isolate {
self.run_internal(source, ExpressionKind::Unary)?;
let bytecode = self.compiler.get_bytecode();
let result = self
.vm
.run(bytecode, self.environment.clone().unwrap_or(Variable::Null))?;
let result = self.vm.run(bytecode, &self.scope)?;
result.as_bool().ok_or_else(|| IsolateError::ValueCastError)
}
pub fn run_unary_compiled(&mut self, code: &[Opcode]) -> Result<bool, IsolateError> {
let result = self
.vm
.run(code, self.environment.clone().unwrap_or(Variable::Null))?;
let result = self.vm.run(code, &self.scope)?;
result.as_bool().ok_or_else(|| IsolateError::ValueCastError)
}
+2
View File
@@ -67,6 +67,7 @@ pub mod intellisense;
pub mod lexer;
pub mod nl;
pub mod parser;
pub mod scope;
pub mod validate;
pub mod variable;
pub mod vm;
@@ -76,4 +77,5 @@ pub use exports::{
};
pub use expression::{Expression, ExpressionKind, OpcodeCache};
pub use isolate::{Isolate, IsolateError};
pub use scope::Scope;
pub use variable::Variable;
+115
View File
@@ -0,0 +1,115 @@
use crate::variable::Variable;
use smallvec::SmallVec;
use zen_types::symbol::Symbol;
#[derive(Debug, Clone)]
pub struct Scope {
base: Variable,
locals: SmallVec<[(Symbol, Variable); 4]>,
}
impl Default for Scope {
fn default() -> Self {
Self::new(Variable::Null)
}
}
impl Scope {
pub fn new(base: Variable) -> Self {
Self {
base,
locals: SmallVec::new(),
}
}
pub fn base(&self) -> &Variable {
&self.base
}
pub fn base_mut(&mut self) -> &mut Variable {
&mut self.base
}
pub fn set_base(&mut self, base: Variable) {
self.base = base;
self.locals.clear();
}
pub fn locals(&self) -> &[(Symbol, Variable)] {
&self.locals
}
pub fn clear_locals(&mut self) {
self.locals.clear();
}
pub fn set_local(&mut self, name: Symbol, value: Variable) {
match self
.locals
.iter_mut()
.find(|(key, _)| key.as_str() == name.as_str())
{
Some(slot) => slot.1 = value,
None => self.locals.push((name, value)),
}
}
pub fn local(&self, name: &Symbol) -> Option<&Variable> {
self.locals
.iter()
.find(|(key, _)| key.as_str() == name.as_str())
.map(|(_, value)| value)
}
pub fn local_str(&self, name: &str) -> Option<&Variable> {
self.locals
.iter()
.find(|(key, _)| key.as_str() == name)
.map(|(_, value)| value)
}
pub fn get_str(&self, name: &str) -> Option<Variable> {
if let Some(value) = self.local_str(name) {
return Some(value.clone());
}
match &self.base {
Variable::Object(object) => object.borrow().get_str(name).cloned(),
_ => None,
}
}
pub fn get(&self, name: &Symbol) -> Option<Variable> {
if let Some(value) = self.local(name) {
return Some(value.clone());
}
match &self.base {
Variable::Object(object) => object.borrow().get(name).cloned(),
_ => None,
}
}
pub fn materialize(&self) -> Variable {
if self.locals.is_empty() {
return self.base.shallow_clone();
}
let Variable::Object(base) = &self.base else {
return self.base.shallow_clone();
};
let mut map = base.borrow().clone();
for (key, value) in &self.locals {
map.insert(key.clone(), value.clone());
}
Variable::from_object(map)
}
}
impl From<Variable> for Scope {
fn from(value: Variable) -> Self {
Scope::new(value)
}
}
+1
View File
@@ -1,4 +1,5 @@
pub use zen_types::rcvalue::*;
pub use zen_types::symbol::Symbol;
pub use zen_types::variable::*;
pub use zen_types::variable_type::*;
+76 -43
View File
@@ -2,20 +2,21 @@ use crate::compiler::{Compare, FetchFastTarget, Jump, Opcode};
use crate::functions::arguments::Arguments;
use crate::functions::registry::FunctionRegistry;
use crate::functions::{internal, MethodRegistry};
use crate::scope::Scope;
use crate::variable::Variable;
use crate::variable::Variable::*;
use crate::vm::date::DynamicVariableExt;
use crate::vm::error::VMError::*;
use crate::vm::error::VMResult;
use crate::vm::interval::{VmInterval, VmIntervalData};
use ahash::{HashMap, HashMapExt};
use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
use rust_decimal::{Decimal, MathematicalOps};
use std::rc::Rc;
use std::string::String as StdString;
use zen_types::symbol::Symbol;
#[derive(Debug)]
pub struct Scope {
pub struct LoopScope {
array: Variable,
len: usize,
iter: usize,
@@ -24,7 +25,7 @@ pub struct Scope {
#[derive(Debug)]
pub struct VM {
scopes: Vec<Scope>,
scopes: Vec<LoopScope>,
stack: Vec<Variable>,
}
@@ -36,17 +37,17 @@ impl VM {
}
}
pub fn run(&mut self, bytecode: &[Opcode], env: Variable) -> VMResult<Variable> {
pub fn run(&mut self, bytecode: &[Opcode], scope: &Scope) -> VMResult<Variable> {
self.stack.clear();
self.scopes.clear();
let s = VMInner::new(bytecode, &mut self.stack, &mut self.scopes).run(env);
let s = VMInner::new(bytecode, &mut self.stack, &mut self.scopes).run(scope);
Ok(s?)
}
}
struct VMInner<'parent_ref, 'bytecode_ref> {
scopes: &'parent_ref mut Vec<Scope>,
scopes: &'parent_ref mut Vec<LoopScope>,
stack: &'parent_ref mut Vec<Variable>,
bytecode: &'bytecode_ref [Opcode],
ip: u32,
@@ -56,7 +57,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
pub fn new(
bytecode: &'bytecode_ref [Opcode],
stack: &'parent_ref mut Vec<Variable>,
scopes: &'parent_ref mut Vec<Scope>,
scopes: &'parent_ref mut Vec<LoopScope>,
) -> Self {
Self {
ip: 0,
@@ -76,8 +77,8 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
})
}
pub fn run(&mut self, root_env: Variable) -> VMResult<Variable> {
let mut env = root_env.clone();
pub fn run(&mut self, root_scope: &Scope) -> VMResult<Variable> {
let mut env = root_scope.clone();
let mut assigned_object: Option<Variable> = None;
if self.ip != 0 {
self.ip = 0;
@@ -98,7 +99,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
Opcode::PushNull => self.push(Null),
Opcode::PushBool(b) => self.push(Bool(*b)),
Opcode::PushNumber(n) => self.push(Number(*n)),
Opcode::PushString(s) => self.push(String(Rc::from(s.as_ref()))),
Opcode::PushString(s) => self.push(String((s.as_ref()).into())),
Opcode::Pop => {
self.pop()?;
}
@@ -109,7 +110,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
match (a, b) {
(Object(o), String(s)) => {
let obj = o.borrow();
self.push(obj.get(s.as_ref()).cloned().unwrap_or(Null));
self.push(obj.get_str(s.as_str()).cloned().unwrap_or(Null));
}
(Array(a), Number(n)) => {
let arr = a.borrow();
@@ -129,7 +130,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
})?;
if let Some(slice) = str.get(index..index + 1) {
self.push(String(Rc::from(slice)));
self.push(String((slice).into()));
} else {
self.push(Null)
};
@@ -138,13 +139,26 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
}
}
Opcode::FetchFast(path) => {
let variable = path.iter().fold(Null, |v, p| match p {
FetchFastTarget::Root => root_env.clone(),
FetchFastTarget::Begin => env.clone(),
let mut steps = path.iter();
let mut variable = match steps.next() {
Some(FetchFastTarget::Root) => root_scope.materialize(),
Some(FetchFastTarget::Begin) => match steps.clone().next() {
Some(FetchFastTarget::String(key)) => {
steps.next();
env.get_str(key).unwrap_or(Null)
}
_ => env.materialize(),
},
_ => Null,
};
variable = steps.fold(variable, |v, p| match p {
FetchFastTarget::Root => root_scope.materialize(),
FetchFastTarget::Begin => env.materialize(),
FetchFastTarget::String(key) => match v {
Object(obj) => {
let obj_ref = obj.borrow();
obj_ref.get(key.as_ref()).cloned().unwrap_or(Null)
obj_ref.get_str(key).cloned().unwrap_or(Null)
}
_ => Null,
},
@@ -156,7 +170,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
String(str) => {
let index = *num as usize;
str.get(index..index + 1)
.map(|slice| String(Rc::from(slice)))
.map(|slice| String((slice).into()))
.unwrap_or(Null)
}
_ => Null,
@@ -165,24 +179,27 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
self.push(variable);
}
Opcode::FetchEnv(f) => match &env {
Object(o) => {
let obj = o.borrow();
match obj.get(f.as_ref()) {
None => self.push(Null),
Some(v) => self.push(v.clone()),
Opcode::FetchEnv(f) => match env.local_str(f) {
Some(v) => self.push(v.clone()),
None => match env.base() {
Object(o) => {
let obj = o.borrow();
match obj.get_str(f) {
None => self.push(Null),
Some(v) => self.push(v.clone()),
}
}
}
Null => self.push(Null),
_ => {
return Err(OpcodeErr {
opcode: "FetchEnv".into(),
message: "Unsupported type".into(),
});
}
Null => self.push(Null),
_ => {
return Err(OpcodeErr {
opcode: "FetchEnv".into(),
message: "Unsupported type".into(),
});
}
},
},
Opcode::FetchRootEnv => {
self.push(env.clone());
self.push(env.materialize());
}
Opcode::Negate => {
let a = self.pop()?;
@@ -381,7 +398,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
}
(String(a), Object(b)) => {
let obj = b.borrow();
self.push(Bool(obj.contains_key(a.as_ref())));
self.push(Bool(obj.contains_key_str(a.as_str())));
}
(Bool(a), Array(b)) => {
let arr = b.borrow();
@@ -457,7 +474,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
c.push_str(a.as_ref());
c.push_str(b.as_ref());
self.push(String(Rc::from(c.as_str())));
self.push(String((c.as_str()).into()));
}
_ => {
return Err(OpcodeErr {
@@ -634,7 +651,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
}
}
self.push(String(Rc::from(s)));
self.push(String((s).into()));
}
Opcode::Slice => {
let from_var = self.pop()?;
@@ -668,7 +685,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
message: "Index out of range".into(),
})?;
self.push(String(Rc::from(slice)));
self.push(String((slice).into()));
}
_ => {
return Err(OpcodeErr {
@@ -722,7 +739,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
message: "Failed to extract argument".into(),
})?;
let mut map = HashMap::with_capacity(to);
let mut map = crate::variable::VariableMap::with_capacity(to);
for _ in 0..to {
let value = self.pop()?;
let String(key) = self.pop()? else {
@@ -732,7 +749,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
});
};
map.insert(key.clone(), value);
map.insert(Symbol::from(key.as_str()), value);
}
self.push(Variable::from_object(map));
@@ -756,14 +773,30 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
});
};
let Some(new_env) = env.dot_insert_detached(key.as_ref(), value.clone()) else {
if !matches!(env.base(), Object(_)) {
return Err(OpcodeErr {
opcode: "AssignedObjectStep".into(),
message: "Failed to mutate existing env".to_owned(),
});
};
}
match key.contains('.') {
false => env.set_local(Symbol::from(key.as_str()), value.clone()),
true => {
let Some(new_env) = env
.materialize()
.dot_insert_detached(key.as_ref(), value.clone())
else {
return Err(OpcodeErr {
opcode: "AssignedObjectStep".into(),
message: "Failed to mutate existing env".to_owned(),
});
};
env = Scope::new(new_env);
}
}
env = new_env;
assigned_object.dot_insert(key.as_ref(), value);
}
Opcode::AssignedObjectEnd { with_return } => {
@@ -891,7 +924,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
let maybe_scope = match &var {
Array(a) => {
let arr = a.borrow();
Some(Scope {
Some(LoopScope {
len: arr.len(),
array: var.clone(),
count: 0,
@@ -900,7 +933,7 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
}
_ => match var.dynamic::<VmInterval>().map(|s| s.to_array()).flatten() {
None => None,
Some(arr) => Some(Scope {
Some(arr) => Some(LoopScope {
len: arr.len(),
array: Variable::from_array(arr),
count: 0,
+56
View File
@@ -807,6 +807,62 @@ fn isolate_unary_tests() {
}
}
#[test]
fn isolate_does_not_mutate_environment() {
let environment: Variable = json!({ "a": 1, "nested": { "b": 2 } }).into();
let mut isolate = Isolate::new();
isolate.set_environment(environment.clone());
isolate.set_reference("a").unwrap();
isolate.set_local(Variable::nodes_key(), json!({ "some": "node" }).into());
isolate.run_standard("x = 1; y = x + a; y").unwrap();
isolate.insert_dollar("computed", Variable::Number(42.into()));
assert_eq!(
environment.to_value(),
json!({ "a": 1, "nested": { "b": 2 } }),
"the caller's environment must come back exactly as it went in"
);
assert_eq!(
isolate.run_standard("$nodes.some").unwrap().to_value(),
json!("node")
);
assert_eq!(
isolate.run_standard("$.computed").unwrap().to_value(),
json!(42)
);
assert_eq!(isolate.run_standard("a").unwrap().to_value(), json!(1));
let mut root_keys = isolate.run_standard("keys($root)").unwrap().to_value();
root_keys
.as_array_mut()
.unwrap()
.sort_by_key(|k| k.to_string());
assert_eq!(
root_keys,
json!(["$", "$nodes", "a", "nested"]),
"`$root` still sees the input and the bindings as one object"
);
}
#[test]
fn assignments_do_not_leak_between_runs() {
let environment: Variable = json!({ "a": 1 }).into();
let mut isolate = Isolate::with_environment(environment.clone());
assert_eq!(
isolate.run_standard("b = a + 1; b").unwrap().to_value(),
json!(2)
);
assert_eq!(
isolate.run_standard("b").unwrap(),
Variable::Null,
"an assignment lives for one run only"
);
assert_eq!(environment.to_value(), json!({ "a": 1 }));
}
#[cfg(test)]
mod test {
use anyhow::Context;
+12 -13
View File
@@ -47,8 +47,7 @@ pub fn to_variable_impl(input: TokenStream) -> TokenStream {
const _: () = {
extern crate zen_expression as _zen_expression;
use _zen_expression::variable::{Variable as _Variable, VariableMap as _VariableMap, VariableMapExt, ToVariable as _ToVariable};
use ::std::rc::Rc as _Rc;
use _zen_expression::variable::{Variable as _Variable, VariableMap as _VariableMap, VariableMapExt, ToVariable as _ToVariable, Symbol as _Symbol};
#impl_block
};
@@ -73,7 +72,7 @@ fn generate_struct_body(fields: &[serde_derive_internals::ast::Field]) -> proc_m
quote! {
map.insert(
_Rc::from(#serialized_name),
_Symbol::from(#serialized_name),
self.#field_ident.to_variable()
);
}
@@ -187,7 +186,7 @@ fn generate_untagged_variant_arm(
let field_name = field.attrs.name().serialize_name();
quote! {
map.insert(_Rc::from(#field_name), #field_ident.to_variable());
map.insert(_Symbol::from(#field_name), #field_ident.to_variable());
}
});
@@ -224,7 +223,7 @@ fn generate_variant_arm(
serde_derive_internals::ast::Style::Unit => {
quote! {
#enum_ident::#variant_ident => {
_Variable::String(_Rc::from(#variant_name))
_Variable::String(_Symbol::from_static(#variant_name))
}
}
}
@@ -233,8 +232,8 @@ fn generate_variant_arm(
quote! {
#enum_ident::#variant_ident(value) => {
let mut map = _VariableMap::with_capacity(2);
map.insert(_Rc::from(#type_key), _Variable::String(_Rc::from(#variant_name)));
map.insert(_Rc::from(#value_key), value.to_variable());
map.insert(_Symbol::from(#type_key), _Variable::String(_Symbol::from_static(#variant_name)));
map.insert(_Symbol::from(#value_key), value.to_variable());
_Variable::from_object(map)
}
}
@@ -250,8 +249,8 @@ fn generate_variant_arm(
quote! {
#enum_ident::#variant_ident(#(#field_patterns),*) => {
let mut map = _VariableMap::with_capacity(2);
map.insert(_Rc::from(#type_key), _Variable::String(_Rc::from(#variant_name)));
map.insert(_Rc::from(#value_key), (#(#field_patterns)*).to_variable());
map.insert(_Symbol::from(#type_key), _Variable::String(_Symbol::from_static(#variant_name)));
map.insert(_Symbol::from(#value_key), (#(#field_patterns)*).to_variable());
_Variable::from_object(map)
}
}
@@ -259,14 +258,14 @@ fn generate_variant_arm(
let field_mappings = field_patterns.iter().enumerate().map(|(i, pattern)| {
let field_key = rename_rule.apply_to_field(&format!("field_{}", i));
quote! {
map.insert(_Rc::from(#field_key), (#pattern).to_variable());
map.insert(_Symbol::from(#field_key), (#pattern).to_variable());
}
});
quote! {
#enum_ident::#variant_ident(#(#field_patterns),*) => {
let mut map = _VariableMap::with_capacity(#field_count + 1);
map.insert(_Rc::from(#type_key), _Variable::String(_Rc::from(#variant_name)));
map.insert(_Symbol::from(#type_key), _Variable::String(_Symbol::from_static(#variant_name)));
#(#field_mappings)*
_Variable::from_object(map)
}
@@ -290,7 +289,7 @@ fn generate_variant_arm(
let field_name = field.attrs.name().serialize_name();
quote! {
map.insert(_Rc::from(#field_name), #field_ident.to_variable());
map.insert(_Symbol::from(#field_name), #field_ident.to_variable());
}
});
@@ -303,7 +302,7 @@ fn generate_variant_arm(
quote! {
#enum_ident::#variant_ident { #(#field_patterns),* } => {
let mut map = _VariableMap::with_capacity(#field_count);
map.insert(_Rc::from(#type_key), _Variable::String(_Rc::from(#variant_name)));
map.insert(_Symbol::from(#type_key), _Variable::String(_Symbol::from_static(#variant_name)));
#(#field_mappings)*
_Variable::from_object(map)
}
+2 -3
View File
@@ -1,5 +1,4 @@
use std::iter::Peekable;
use std::rc::Rc;
use std::slice::Iter;
use crate::error::TemplateRenderError;
@@ -56,7 +55,7 @@ impl<'source, 'nodes> Interpreter<'source, 'nodes> {
let item = self.results.remove(0);
match item {
InterpreterResult::Variable(val) => Ok(val),
InterpreterResult::String(str) => Ok(Variable::String(Rc::from(str))),
InterpreterResult::String(str) => Ok(Variable::String((str).into())),
}
}
_ => {
@@ -69,7 +68,7 @@ impl<'source, 'nodes> Interpreter<'source, 'nodes> {
})
.collect::<String>();
Ok(Variable::String(Rc::from(string_data.as_str())))
Ok(Variable::String((string_data.as_str()).into()))
}
}
}
+2
View File
@@ -9,6 +9,8 @@ license = "MIT"
ahash = { workspace = true }
serde = { workspace = true, features = ["rc", "derive"] }
serde_json = { workspace = true, features = [] }
smallvec = { version = "1", features = ["union"] }
hipstr = { version = "0.8", features = ["serde"] }
rust_decimal = { workspace = true, features = ["maths-nopanic"] }
rust_decimal_macros = { workspace = true }
thiserror = { workspace = true }
+2
View File
@@ -2,6 +2,8 @@
mod constant;
pub mod decision;
pub mod rccell;
pub mod rcvalue;
pub mod symbol;
pub mod variable;
pub mod variable_type;
+4
View File
@@ -0,0 +1,4 @@
use std::cell::RefCell;
use std::rc::Rc;
pub type RcCell<T> = Rc<RefCell<T>>;
+5 -5
View File
@@ -12,13 +12,13 @@ impl ToVariable for RcValue {
RcValue::Null => Variable::Null,
RcValue::Bool(b) => Variable::Bool(*b),
RcValue::Number(n) => Variable::Number(*n),
RcValue::String(s) => Variable::String(Rc::from(s.as_ref())),
RcValue::String(s) => Variable::String((s.as_ref()).into()),
RcValue::Array(arr) => {
Variable::from_array(arr.iter().map(|v| v.to_variable()).collect())
}
RcValue::Object(obj) => Variable::from_object(
obj.iter()
.map(|(k, v)| (Rc::from(k.as_ref()), v.to_variable()))
.map(|(k, v)| (crate::symbol::Symbol::from(k.as_ref()), v.to_variable()))
.collect(),
),
}
@@ -31,7 +31,7 @@ impl From<&Variable> for RcValue {
Variable::Null => RcValue::Null,
Variable::Bool(b) => RcValue::Bool(*b),
Variable::Number(n) => RcValue::Number(*n),
Variable::String(s) => RcValue::String(s.clone()),
Variable::String(s) => RcValue::String((s.as_str()).into()),
Variable::Array(arr) => {
let arr = arr.borrow();
RcValue::Array(arr.iter().map(RcValue::from).collect())
@@ -40,7 +40,7 @@ impl From<&Variable> for RcValue {
let obj = obj.borrow();
RcValue::Object(
obj.iter()
.map(|(k, v)| (k.clone(), RcValue::from(v)))
.map(|(k, v)| (Rc::from(k.as_str()), RcValue::from(v)))
.collect(),
)
}
@@ -89,7 +89,7 @@ impl From<&Value> for RcValue {
);
}
}
Value::String(s) => RcValue::String(Rc::from(s.as_str())),
Value::String(s) => RcValue::String((s.as_str()).into()),
Value::Array(arr) => RcValue::Array(arr.iter().map(RcValue::from).collect()),
Value::Object(obj) => RcValue::Object(
obj.iter()
+1 -1
View File
@@ -59,7 +59,7 @@ impl<'de> Visitor<'de> for RcValueVisitor {
where
E: Error,
{
Ok(RcValue::String(Rc::from(v)))
Ok(RcValue::String((v).into()))
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
+1
View File
@@ -0,0 +1 @@
pub type Symbol = hipstr::LocalHipStr<'static>;
+9 -9
View File
@@ -1,9 +1,9 @@
use crate::rccell::RcCell;
use crate::variable::Variable;
use rust_decimal::Decimal;
#[cfg(not(feature = "arbitrary_precision"))]
use rust_decimal::prelude::FromPrimitive;
use serde_json::{Number, Value};
use std::rc::Rc;
#[cfg(not(feature = "arbitrary_precision"))]
use std::str::FromStr;
@@ -41,13 +41,13 @@ impl From<Value> for Variable {
)
}
}
Value::String(s) => Variable::String(Rc::from(s.as_str())),
Value::String(s) => Variable::String((s.as_str()).into()),
Value::Array(arr) => {
Variable::from_array(arr.into_iter().map(Variable::from).collect())
}
Value::Object(obj) => Variable::from_object(
obj.into_iter()
.map(|(k, v)| (Rc::from(k.as_str()), Variable::from(v)))
.map(|(k, v)| (crate::symbol::Symbol::from(k.as_str()), Variable::from(v)))
.collect(),
),
}
@@ -88,11 +88,11 @@ impl From<&Value> for Variable {
);
}
}
Value::String(s) => Variable::String(Rc::from(s.as_str())),
Value::String(s) => Variable::String((s.as_str()).into()),
Value::Array(arr) => Variable::from_array(arr.iter().map(Variable::from).collect()),
Value::Object(obj) => Variable::from_object(
obj.iter()
.map(|(k, v)| (Rc::from(k.as_str()), Variable::from(v)))
.map(|(k, v)| (crate::symbol::Symbol::from(k.as_str()), Variable::from(v)))
.collect(),
),
}
@@ -119,8 +119,8 @@ impl From<Variable> for Value {
}
Variable::String(s) => Value::String(s.to_string()),
Variable::Array(arr) => {
let vec = Rc::try_unwrap(arr)
.map(|a| a.into_inner())
let vec = RcCell::try_unwrap(arr)
.map(|cell| cell.into_inner())
.unwrap_or_else(|s| {
let borrowed = s.borrow();
borrowed.clone()
@@ -129,8 +129,8 @@ impl From<Variable> for Value {
Value::Array(vec.into_iter().map(Value::from).collect())
}
Variable::Object(obj) => {
let hmap = Rc::try_unwrap(obj)
.map(|a| a.into_inner())
let hmap = RcCell::try_unwrap(obj)
.map(|cell| cell.into_inner())
.unwrap_or_else(|s| {
let borrowed = s.borrow();
borrowed.clone()
+4 -4
View File
@@ -1,7 +1,7 @@
#[cfg(feature = "arbitrary_precision")]
use crate::constant::NUMBER_TOKEN;
use crate::variable::Variable;
use ahash::{HashMap, HashMapExt};
use crate::variable::VariableMap;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
use serde::de::{DeserializeSeed, Error, MapAccess, SeqAccess, Unexpected, Visitor};
@@ -59,7 +59,7 @@ impl<'de> Visitor<'de> for VariableVisitor {
where
E: Error,
{
Ok(Variable::String(Rc::from(v)))
Ok(Variable::String((v).into()))
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
@@ -85,7 +85,7 @@ impl<'de> Visitor<'de> for VariableVisitor {
where
A: MapAccess<'de>,
{
let mut m = HashMap::with_capacity(map.size_hint().unwrap_or_default());
let mut m = VariableMap::with_capacity(map.size_hint().unwrap_or_default());
#[cfg(feature = "arbitrary_precision")]
let mut first = true;
@@ -105,7 +105,7 @@ impl<'de> Visitor<'de> for VariableVisitor {
));
}
m.insert(key, value);
m.insert(crate::symbol::Symbol::from(key.as_ref()), value);
#[cfg(feature = "arbitrary_precision")]
{
first = false;
+5 -5
View File
@@ -13,13 +13,13 @@ pub trait ToVariable {
impl ToVariable for String {
fn to_variable(&self) -> Variable {
Variable::String(Rc::from(self.as_str()))
Variable::String((self.as_str()).into())
}
}
impl ToVariable for str {
fn to_variable(&self) -> Variable {
Variable::String(Rc::from(self))
Variable::String((self).into())
}
}
@@ -92,7 +92,7 @@ where
fn to_variable(&self) -> Variable {
Variable::from_object(
self.iter()
.map(|(k, v)| (k.clone(), v.to_variable()))
.map(|(k, v)| (crate::symbol::Symbol::from(k.as_ref()), v.to_variable()))
.collect(),
)
}
@@ -106,7 +106,7 @@ where
fn to_variable(&self) -> Variable {
Variable::from_object(
self.iter()
.map(|(k, v)| (Rc::<str>::from(k.deref()), v.to_variable()))
.map(|(k, v)| (crate::symbol::Symbol::from(k.deref()), v.to_variable()))
.collect(),
)
}
@@ -120,7 +120,7 @@ where
fn to_variable(&self) -> Variable {
Variable::from_object(
self.iter()
.map(|(k, v)| (Rc::from(k.as_str()), v.to_variable()))
.map(|(k, v)| (crate::symbol::Symbol::from(k.as_str()), v.to_variable()))
.collect(),
)
}
+385
View File
@@ -0,0 +1,385 @@
use crate::symbol::Symbol;
use crate::variable::Variable;
use ahash::{HashMap, HashMapExt};
use smallvec::SmallVec;
use std::fmt::{Debug, Formatter};
const INLINE: usize = 8;
const SPILL_AT: usize = 32;
type Entries = SmallVec<[(Symbol, Variable); INLINE]>;
#[derive(Clone)]
enum Repr {
Small(Entries),
Large(HashMap<Symbol, Variable>),
}
#[derive(Clone)]
pub struct VariableMap(Repr);
impl VariableMap {
pub fn new() -> Self {
Self(Repr::Small(SmallVec::new()))
}
pub fn with_capacity(capacity: usize) -> Self {
match capacity > SPILL_AT {
true => Self(Repr::Large(HashMap::with_capacity(capacity))),
false => Self(Repr::Small(SmallVec::with_capacity(capacity))),
}
}
pub fn len(&self) -> usize {
match &self.0 {
Repr::Small(entries) => entries.len(),
Repr::Large(map) => map.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&mut self) {
match &mut self.0 {
Repr::Small(entries) => entries.clear(),
Repr::Large(map) => map.clear(),
}
}
#[inline]
pub fn get(&self, key: &Symbol) -> Option<&Variable> {
match &self.0 {
Repr::Small(entries) => entries
.iter()
.find(|(k, _)| k.as_str() == key.as_str())
.map(|(_, v)| v),
Repr::Large(map) => map.get(key),
}
}
#[inline]
pub fn get_str(&self, key: &str) -> Option<&Variable> {
match &self.0 {
Repr::Small(entries) => entries
.iter()
.find(|(k, _)| k.as_str() == key)
.map(|(_, v)| v),
Repr::Large(map) => map.get(key),
}
}
pub fn get_mut(&mut self, key: &Symbol) -> Option<&mut Variable> {
match &mut self.0 {
Repr::Small(entries) => entries
.iter_mut()
.find(|(k, _)| k.as_str() == key.as_str())
.map(|(_, v)| v),
Repr::Large(map) => map.get_mut(key),
}
}
pub fn get_key_value(&self, key: &Symbol) -> Option<(&Symbol, &Variable)> {
match &self.0 {
Repr::Small(entries) => entries
.iter()
.find(|(k, _)| k.as_str() == key.as_str())
.map(|(k, v)| (k, v)),
Repr::Large(map) => map.get_key_value(key),
}
}
pub fn contains_key(&self, key: &Symbol) -> bool {
self.get(key).is_some()
}
pub fn contains_key_str(&self, key: &str) -> bool {
self.get_str(key).is_some()
}
pub fn remove_str(&mut self, key: &str) -> Option<Variable> {
match &mut self.0 {
Repr::Small(entries) => entries
.iter()
.position(|(k, _)| k.as_str() == key)
.map(|index| entries.remove(index).1),
Repr::Large(map) => map.remove(key),
}
}
pub fn insert(&mut self, key: Symbol, value: Variable) -> Option<Variable> {
match &mut self.0 {
Repr::Small(entries) => {
if let Some(slot) = entries.iter_mut().find(|(k, _)| k.as_str() == key.as_str()) {
return Some(std::mem::replace(&mut slot.1, value));
}
if entries.len() >= SPILL_AT {
self.spill();
let Repr::Large(map) = &mut self.0 else {
unreachable!("just spilled")
};
return map.insert(key, value);
}
entries.push((key, value));
None
}
Repr::Large(map) => map.insert(key, value),
}
}
pub fn remove(&mut self, key: &Symbol) -> Option<Variable> {
match &mut self.0 {
Repr::Small(entries) => entries
.iter()
.position(|(k, _)| k.as_str() == key.as_str())
.map(|index| entries.remove(index).1),
Repr::Large(map) => map.remove(key),
}
}
fn spill(&mut self) {
let Repr::Small(entries) = &mut self.0 else {
return;
};
let mut map = HashMap::with_capacity(entries.len() * 2);
for (key, value) in entries.drain(..) {
map.insert(key, value);
}
self.0 = Repr::Large(map);
}
pub fn entry(&mut self, key: Symbol) -> Entry<'_> {
if matches!(&self.0, Repr::Small(entries)
if entries.len() >= SPILL_AT && self.get(&key).is_none())
{
self.spill();
}
match self.contains_key(&key) {
true => Entry::Occupied(OccupiedEntry { map: self, key }),
false => Entry::Vacant(VacantEntry { map: self, key }),
}
}
pub fn iter(&self) -> Iter<'_> {
match &self.0 {
Repr::Small(entries) => Iter::Small(entries.iter()),
Repr::Large(map) => Iter::Large(map.iter()),
}
}
pub fn iter_mut(&mut self) -> IterMut<'_> {
match &mut self.0 {
Repr::Small(entries) => IterMut::Small(entries.iter_mut()),
Repr::Large(map) => IterMut::Large(map.iter_mut()),
}
}
pub fn keys(&self) -> impl Iterator<Item = &Symbol> + '_ {
self.iter().map(|(key, _)| key)
}
pub fn values(&self) -> impl Iterator<Item = &Variable> {
self.iter().map(|(_, value)| value)
}
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Variable> {
self.iter_mut().map(|(_, value)| value)
}
}
pub enum Entry<'a> {
Occupied(OccupiedEntry<'a>),
Vacant(VacantEntry<'a>),
}
pub struct OccupiedEntry<'a> {
map: &'a mut VariableMap,
key: Symbol,
}
pub struct VacantEntry<'a> {
map: &'a mut VariableMap,
key: Symbol,
}
impl<'a> Entry<'a> {
pub fn or_insert(self, default: Variable) -> &'a mut Variable {
match self {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => entry.insert(default),
}
}
pub fn or_insert_with<F: FnOnce() -> Variable>(self, default: F) -> &'a mut Variable {
match self {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => entry.insert(default()),
}
}
}
impl<'a> OccupiedEntry<'a> {
pub fn get(&self) -> &Variable {
self.map.get(&self.key).expect("occupied")
}
pub fn get_mut(&mut self) -> &mut Variable {
self.map.get_mut(&self.key).expect("occupied")
}
pub fn into_mut(self) -> &'a mut Variable {
let key = self.key;
self.map.get_mut(&key).expect("occupied")
}
pub fn insert(&mut self, value: Variable) -> Variable {
std::mem::replace(self.get_mut(), value)
}
}
impl<'a> VacantEntry<'a> {
pub fn insert(self, value: Variable) -> &'a mut Variable {
let key = self.key;
self.map.insert(key.clone(), value);
self.map.get_mut(&key).expect("just inserted")
}
}
pub enum Iter<'a> {
Small(std::slice::Iter<'a, (Symbol, Variable)>),
Large(std::collections::hash_map::Iter<'a, Symbol, Variable>),
}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a Symbol, &'a Variable);
fn next(&mut self) -> Option<Self::Item> {
match self {
Iter::Small(iter) => iter.next().map(|(key, value)| (key, value)),
Iter::Large(iter) => iter.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
Iter::Small(iter) => iter.size_hint(),
Iter::Large(iter) => iter.size_hint(),
}
}
}
pub enum IterMut<'a> {
Small(std::slice::IterMut<'a, (Symbol, Variable)>),
Large(std::collections::hash_map::IterMut<'a, Symbol, Variable>),
}
impl<'a> Iterator for IterMut<'a> {
type Item = (&'a Symbol, &'a mut Variable);
fn next(&mut self) -> Option<Self::Item> {
match self {
IterMut::Small(iter) => iter.next().map(|(key, value)| (&*key, value)),
IterMut::Large(iter) => iter.next(),
}
}
}
pub enum IntoIter {
Small(smallvec::IntoIter<[(Symbol, Variable); INLINE]>),
Large(std::collections::hash_map::IntoIter<Symbol, Variable>),
}
impl Iterator for IntoIter {
type Item = (Symbol, Variable);
fn next(&mut self) -> Option<Self::Item> {
match self {
IntoIter::Small(iter) => iter.next(),
IntoIter::Large(iter) => iter.next(),
}
}
}
impl IntoIterator for VariableMap {
type Item = (Symbol, Variable);
type IntoIter = IntoIter;
fn into_iter(self) -> IntoIter {
match self.0 {
Repr::Small(entries) => IntoIter::Small(entries.into_iter()),
Repr::Large(map) => IntoIter::Large(map.into_iter()),
}
}
}
impl<'a> IntoIterator for &'a VariableMap {
type Item = (&'a Symbol, &'a Variable);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Iter<'a> {
self.iter()
}
}
impl Default for VariableMap {
fn default() -> Self {
Self::new()
}
}
impl FromIterator<(Symbol, Variable)> for VariableMap {
fn from_iter<T: IntoIterator<Item = (Symbol, Variable)>>(iter: T) -> Self {
let iter = iter.into_iter();
let mut map = VariableMap::with_capacity(iter.size_hint().0);
for (key, value) in iter {
map.insert(key, value);
}
map
}
}
impl Extend<(Symbol, Variable)> for VariableMap {
fn extend<T: IntoIterator<Item = (Symbol, Variable)>>(&mut self, iter: T) {
for (key, value) in iter {
self.insert(key, value);
}
}
}
impl PartialEq for VariableMap {
fn eq(&self, other: &Self) -> bool {
self.len() == other.len()
&& self
.iter()
.all(|(key, value)| other.get(key).is_some_and(|o| o == value))
}
}
impl VariableMap {
pub fn insert_str(&mut self, key: &str, value: Variable) -> Option<Variable> {
self.insert(Symbol::from(key), value)
}
}
impl Debug for VariableMap {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_map()
.entries(self.iter().map(|(k, v)| (k.as_str(), v)))
.finish()
}
}
impl serde::Serialize for VariableMap {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.len()))?;
for (key, value) in self.iter() {
map.serialize_entry(key.as_str(), value)?;
}
map.end()
}
}
+48 -24
View File
@@ -1,11 +1,11 @@
use crate::symbol::Symbol;
use crate::variable::map::Entry;
use crate::variable::ref_ser::RefSerializer;
use ahash::HashMap;
use rust_decimal::Decimal;
use rust_decimal::prelude::Zero;
use serde_json::Value;
use std::any::Any;
use std::cell::RefCell;
use std::collections::hash_map::Entry;
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref;
use std::rc::Rc;
@@ -19,24 +19,25 @@ pub use impls::ToVariable;
mod conv;
mod de;
mod impls;
mod map;
mod ref_deser;
mod ref_ser;
mod ser;
pub(crate) type RcCell<T> = Rc<RefCell<T>>;
pub use crate::rccell::RcCell;
pub type VariableMap = HashMap<Rc<str>, Variable>;
pub use crate::variable::map::{Iter as MapIter, VariableMap};
thread_local! {
static DOLLAR_KEY: Rc<str> = Rc::from("$");
static ROOT_KEY: Rc<str> = Rc::from("$root");
static DOLLAR_KEY_RC: Rc<str> = Rc::from("$");
static ROOT_KEY_RC: Rc<str> = Rc::from("$root");
}
pub enum Variable {
Null,
Bool(bool),
Number(Decimal),
String(Rc<str>),
String(Symbol),
Array(RcCell<Vec<Variable>>),
Object(RcCell<VariableMap>),
Dynamic(Rc<dyn DynamicVariable>),
@@ -51,12 +52,28 @@ pub trait DynamicVariable: Display {
}
impl Variable {
pub fn dollar_key() -> Rc<str> {
DOLLAR_KEY.with(Rc::clone)
pub fn dollar_key() -> Symbol {
Symbol::from_static("$")
}
pub fn root_key() -> Rc<str> {
ROOT_KEY.with(Rc::clone)
pub fn root_key() -> Symbol {
Symbol::from_static("$root")
}
pub fn nodes_key() -> Symbol {
Symbol::from_static("$nodes")
}
pub fn key(name: &str) -> Symbol {
Symbol::from(name)
}
pub fn dollar_key_rc() -> Rc<str> {
DOLLAR_KEY_RC.with(Rc::clone)
}
pub fn root_key_rc() -> Rc<str> {
ROOT_KEY_RC.with(Rc::clone)
}
pub fn from_array(arr: Vec<Self>) -> Self {
@@ -71,7 +88,7 @@ impl Variable {
RefDeserializer::new().deserialize(serialized)
}
pub fn from_object(obj: HashMap<Rc<str>, Self>) -> Self {
pub fn from_object(obj: VariableMap) -> Self {
Self::Object(Rc::new(RefCell::new(obj)))
}
@@ -92,7 +109,14 @@ impl Variable {
pub fn as_rc_str(&self) -> Option<Rc<str>> {
match self {
Variable::String(s) => Some(s.clone()),
Variable::String(s) => Some(Rc::from(s.as_str())),
_ => None,
}
}
pub fn as_sym(&self) -> Option<&Symbol> {
match self {
Variable::String(s) => Some(s),
_ => None,
}
}
@@ -111,7 +135,7 @@ impl Variable {
}
}
pub fn as_object(&self) -> Option<RcCell<HashMap<Rc<str>, Variable>>> {
pub fn as_object(&self) -> Option<RcCell<VariableMap>> {
match self {
Variable::Object(obj) => Some(obj.clone()),
_ => None,
@@ -167,7 +191,7 @@ impl Variable {
.try_fold(self.shallow_clone(), |var, part| match var {
Variable::Object(obj) => {
let reference = obj.borrow();
reference.get(part).map(|v| v.shallow_clone())
reference.get_str(part).map(|v| v.shallow_clone())
}
_ => None,
})
@@ -183,7 +207,7 @@ impl Variable {
.try_fold(cloned_self.shallow_clone(), |var, part| match var {
Variable::Object(obj) => {
let mut obj_ref = obj.borrow_mut();
Some(match obj_ref.entry(Rc::from(*part)) {
Some(match obj_ref.entry(Symbol::from(*part)) {
Entry::Occupied(mut occ) => {
let var = occ.get();
let new_obj = match var {
@@ -209,7 +233,7 @@ impl Variable {
let head = parts.try_fold(self.shallow_clone(), |var, part| match var {
Variable::Object(obj) => {
let mut obj_ref = obj.borrow_mut();
Some(match obj_ref.entry(Rc::from(part)) {
Some(match obj_ref.entry(Symbol::from(part)) {
Entry::Occupied(occ) => occ.get().shallow_clone(),
Entry::Vacant(vac) => vac.insert(Self::empty_object()).shallow_clone(),
})
@@ -221,7 +245,7 @@ impl Variable {
};
let mut object = object_ref.borrow_mut();
object.remove(last_part)
object.remove_str(last_part)
}
pub fn dot_insert(&self, key: &str, variable: Variable) -> Option<Variable> {
@@ -230,7 +254,7 @@ impl Variable {
let head = parts.try_fold(self.shallow_clone(), |var, part| match var {
Variable::Object(obj) => {
let mut obj_ref = obj.borrow_mut();
Some(match obj_ref.entry(Rc::from(part)) {
Some(match obj_ref.entry(Symbol::from(part)) {
Entry::Occupied(occ) => occ.get().shallow_clone(),
Entry::Vacant(vac) => vac.insert(Self::empty_object()).shallow_clone(),
})
@@ -242,7 +266,7 @@ impl Variable {
};
let mut object = object_ref.borrow_mut();
object.insert(Rc::from(last_part), variable)
object.insert(Symbol::from(last_part), variable)
}
pub fn dot_insert_detached(&self, key: &str, variable: Variable) -> Option<Variable> {
@@ -254,7 +278,7 @@ impl Variable {
};
let mut object = object_ref.borrow_mut();
object.insert(Rc::from(last_part), variable);
object.insert(Symbol::from(last_part), variable);
Some(new_var)
}
@@ -353,7 +377,7 @@ fn merge_variables(
if doc.is_object() && patch.is_object() {
let doc_ref = doc.as_object().unwrap();
let patch_ref = patch.as_object().unwrap();
if Rc::ptr_eq(&doc_ref, &patch_ref) {
if RcCell::ptr_eq(&doc_ref, &patch_ref) {
return false;
}
@@ -374,7 +398,7 @@ fn merge_variables(
}
MergeStrategy::CloneOnWrite => {
let mut changed = false;
let mut new_map = None;
let mut new_map: Option<VariableMap> = None;
for (key, value) in patch.deref() {
// Get or create the new map if we haven't yet
@@ -474,7 +498,7 @@ impl PartialEq for Variable {
(Variable::Null, Variable::Null) => true,
(Variable::Bool(b1), Variable::Bool(b2)) => b1 == b2,
(Variable::Number(n1), Variable::Number(n2)) => n1 == n2,
(Variable::String(s1), Variable::String(s2)) => s1 == s2,
(Variable::String(s1), Variable::String(s2)) => s1.as_str() == s2.as_str(),
(Variable::Array(a1), Variable::Array(a2)) => a1 == a2,
(Variable::Object(obj1), Variable::Object(obj2)) => obj1 == obj2,
(Variable::Dynamic(d1), Variable::Dynamic(d2)) => Rc::ptr_eq(d1, d2),
+13 -16
View File
@@ -1,7 +1,6 @@
use crate::rcvalue::RcValue;
use crate::variable::Variable;
use ahash::{HashMap, HashMapExt};
use std::cell::RefCell;
use crate::variable::VariableMap;
use std::rc::Rc;
use thiserror::Error;
@@ -27,11 +26,10 @@ impl RefDeserializer {
for (i, _) in refs_array.iter().enumerate() {
match &refs_array[i] {
RcValue::Array(_) => {
self.refs[i] = Some(Variable::Array(Rc::new(RefCell::new(Vec::new()))));
self.refs[i] = Some(Variable::from_array(Vec::new()));
}
RcValue::Object(_) => {
self.refs[i] =
Some(Variable::Object(Rc::new(RefCell::new(HashMap::default()))));
self.refs[i] = Some(Variable::from_object(VariableMap::default()));
}
_ => {
self.refs[i] = Some(self.deserialize_value(&refs_array[i])?);
@@ -52,7 +50,7 @@ impl RefDeserializer {
}
RcValue::Object(obj) => {
if let Some(Variable::Object(target)) = &self.refs[i] {
let mut map = HashMap::with_capacity(obj.len());
let mut map = VariableMap::with_capacity(obj.len());
for (key, value) in obj {
let key_var = self.deserialize_key(key)?;
let value_var = self.deserialize_value(value)?;
@@ -67,13 +65,13 @@ impl RefDeserializer {
}
let root_value = root_obj
.remove(&Variable::root_key())
.remove("$root")
.ok_or_else(|| RefDeserializeError::InvalidFormat("Missing $root".into()))?;
self.deserialize_value(&root_value)
}
fn deserialize_key(&self, key: &Rc<str>) -> Result<Rc<str>, RefDeserializeError> {
fn deserialize_key(&self, key: &Rc<str>) -> Result<crate::symbol::Symbol, RefDeserializeError> {
if let Some(ref_id) = parse_ref_id(key) {
if ref_id >= self.refs.len() {
return Err(RefDeserializeError::InvalidReference(ref_id));
@@ -114,16 +112,16 @@ impl RefDeserializer {
for item in arr {
items.push(self.deserialize_value(item)?);
}
Ok(Variable::Array(Rc::new(RefCell::new(items))))
Ok(Variable::from_array(items))
}
RcValue::Object(obj) => {
let mut map = HashMap::with_capacity(obj.len());
let mut map = VariableMap::with_capacity(obj.len());
for (key, value) in obj {
let key_var = self.deserialize_key(key)?;
let value_var = self.deserialize_value(value)?;
map.insert(key_var, value_var);
}
Ok(Variable::Object(Rc::new(RefCell::new(map))))
Ok(Variable::from_object(map))
}
}
}
@@ -139,11 +137,10 @@ pub enum RefDeserializeError {
UnresolvedReference(usize),
}
fn unescape_at_string(s: &Rc<str>) -> Rc<str> {
if s.starts_with("@@") {
Rc::from(&s[1..])
} else {
s.clone()
fn unescape_at_string(s: &Rc<str>) -> crate::symbol::Symbol {
match s.starts_with("@@") {
true => crate::symbol::Symbol::from(&s[1..]),
false => crate::symbol::Symbol::from(s.as_ref()),
}
}
+20 -24
View File
@@ -1,3 +1,4 @@
use crate::rccell::RcCell;
use crate::rcvalue::RcValue;
use crate::variable::Variable;
use ahash::AHashMap;
@@ -26,25 +27,20 @@ impl RefSerializer {
}
}
fn escape_at_string(s: &Rc<str>) -> Rc<str> {
if s.starts_with('@') {
let string = format!("@{s}");
Rc::from(string.as_str())
} else {
s.clone()
fn escape_at_string(s: &crate::symbol::Symbol) -> Rc<str> {
match s.starts_with('@') {
true => Rc::from(format!("@{s}").as_str()),
false => Rc::from(s.as_str()),
}
}
fn intern_string_addr(&mut self, s: &Rc<str>) -> usize {
let reference = match self.string_intern.get(s) {
Some(interned) => interned,
None => {
self.string_intern.insert(s.clone(), s.clone());
s
}
};
fn intern_string_addr(&mut self, s: &str) -> usize {
if !self.string_intern.contains_key(s) {
let owned: Rc<str> = Rc::from(s);
self.string_intern.insert(owned.clone(), owned);
}
Rc::as_ptr(&reference) as *const () as usize
Rc::as_ptr(self.string_intern.get(s).expect("interned")) as *const () as usize
}
pub fn serialize(mut self, var: &Variable) -> RcValue {
@@ -58,7 +54,7 @@ impl RefSerializer {
result.insert(Rc::from("$refs"), RcValue::Array(self.ref_data));
}
result.insert(Variable::root_key(), data);
result.insert(Variable::root_key_rc(), data);
RcValue::Object(result)
}
@@ -69,11 +65,11 @@ impl RefSerializer {
return;
}
let addr = self.intern_string_addr(s);
let addr = self.intern_string_addr(s.as_str());
*self.ref_counts.entry(addr).or_insert(0) += 1;
}
Variable::Array(arr) => {
let addr = Rc::as_ptr(arr) as *const () as usize;
let addr = RcCell::as_ptr(arr) as *const () as usize;
*self.ref_counts.entry(addr).or_insert(0) += 1;
let borrowed = arr.borrow();
@@ -82,12 +78,12 @@ impl RefSerializer {
}
}
Variable::Object(obj) => {
let addr = Rc::as_ptr(obj) as *const () as usize;
let addr = RcCell::as_ptr(obj) as *const () as usize;
*self.ref_counts.entry(addr).or_insert(0) += 1;
let borrowed = obj.borrow();
for (key, value) in borrowed.iter() {
let key_addr = self.intern_string_addr(key);
let key_addr = self.intern_string_addr(key.as_str());
*self.ref_counts.entry(key_addr).or_insert(0) += 1;
self.count_refs(value);
}
@@ -121,7 +117,7 @@ impl RefSerializer {
fn serialize_with_refs(&mut self, var: &Variable) -> RcValue {
match var {
Variable::String(s) => {
let addr = self.intern_string_addr(s);
let addr = self.intern_string_addr(s.as_str());
let Some((id, id_str)) = self.refs.get(&addr) else {
return RcValue::String(Self::escape_at_string(s));
};
@@ -134,7 +130,7 @@ impl RefSerializer {
}
Variable::Array(arr) => {
let addr = Rc::as_ptr(arr) as *const () as usize;
let addr = RcCell::as_ptr(arr) as *const () as usize;
let data = {
let borrowed = arr.borrow();
let items: Vec<_> = borrowed
@@ -157,7 +153,7 @@ impl RefSerializer {
}
Variable::Object(obj) => {
let addr = Rc::as_ptr(obj) as *const () as usize;
let addr = RcCell::as_ptr(obj) as *const () as usize;
let data = {
let borrowed = obj.borrow();
let mut map = HashMap::with_capacity_and_hasher(
@@ -166,7 +162,7 @@ impl RefSerializer {
);
for (key, value) in borrowed.iter() {
let key_addr = self.intern_string_addr(key);
let key_addr = self.intern_string_addr(key.as_str());
let key_str = if let Some((key_id, key_id_str)) = self.refs.get(&key_addr) {
if self.ref_data[*key_id] == RcValue::Null {
self.ref_data[*key_id] =
+3 -1
View File
@@ -1,14 +1,16 @@
mod conv;
mod util;
use crate::variable::RcCell;
use ahash::HashMap;
pub use ahash::HashMapExt as VariableMapExt;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::fmt::{Display, Write};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
type RcCell<T> = Rc<RefCell<T>>;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum VariableType {
Any,
+42 -23
View File
@@ -1,4 +1,3 @@
use ahash::{HashMap, HashMapExt};
use rust_decimal_macros::dec;
use serde_json::json;
use std::cell::RefCell;
@@ -6,6 +5,7 @@ use std::error::Error;
use std::rc::Rc;
use zen_types::rcvalue::RcValue;
use zen_types::variable::Variable;
use zen_types::variable::VariableMap as HashMap;
type TestResult = Result<(), Box<dyn Error>>;
@@ -46,8 +46,8 @@ fn serialize_deserialize_with_refs() -> TestResult {
// Check that refs were created
if let RcValue::Object(ref obj) = serialized {
assert!(obj.contains_key(&Rc::from("$refs")));
assert!(obj.contains_key(&Rc::from("$root")));
assert!(obj.contains_key("$refs"));
assert!(obj.contains_key("$root"));
} else {
panic!("Expected object");
}
@@ -133,7 +133,7 @@ fn no_refs_when_below_threshold() -> TestResult {
// Should not have refs section
if let RcValue::Object(ref obj) = serialized {
assert!(!obj.contains_key(&Rc::from("$refs")));
assert!(!obj.contains_key("$refs"));
}
let deserialized = Variable::deserialize_ref(serialized)?;
@@ -148,18 +148,30 @@ fn serialize_circular_references() -> TestResult {
let shared_obj = Rc::new(RefCell::new({
let mut map = HashMap::new();
map.insert(
Rc::from("shared_data"),
Variable::String(Rc::from("important_value")),
zen_types::symbol::Symbol::from("shared_data"),
Variable::String(("important_value").into()),
);
map.insert(
zen_types::symbol::Symbol::from("id"),
Variable::Number(dec!(42.0)),
);
map.insert(Rc::from("id"), Variable::Number(dec!(42.0)));
map
}));
// Create a structure where the same object appears in multiple places
let mut root_map = HashMap::new();
root_map.insert(Rc::from("first_ref"), Variable::Object(shared_obj.clone()));
root_map.insert(Rc::from("second_ref"), Variable::Object(shared_obj.clone()));
root_map.insert(Rc::from("third_ref"), Variable::Object(shared_obj));
root_map.insert(
zen_types::symbol::Symbol::from("first_ref"),
Variable::Object(shared_obj.clone()),
);
root_map.insert(
zen_types::symbol::Symbol::from("second_ref"),
Variable::Object(shared_obj.clone()),
);
root_map.insert(
zen_types::symbol::Symbol::from("third_ref"),
Variable::Object(shared_obj),
);
let var = Variable::Object(Rc::new(RefCell::new(root_map)));
@@ -173,12 +185,10 @@ fn serialize_circular_references() -> TestResult {
#[test]
fn serialize_same_array_multiple_locations() -> TestResult {
use std::cell::RefCell;
// Create a shared array
let shared_array = Rc::new(RefCell::new(vec![
Variable::String(Rc::from("item_one")),
Variable::String(Rc::from("item_two")),
Variable::String(("item_one").into()),
Variable::String(("item_two").into()),
Variable::Number(dec!(123.0)),
]));
@@ -205,12 +215,12 @@ fn serialize_mixed_shared_references() -> TestResult {
// Create an object where the same string is used as both key and value
let mut obj_map = HashMap::new();
obj_map.insert(
shared_string.clone(),
Variable::String(shared_string.clone()),
zen_types::symbol::Symbol::from(shared_string.as_ref()),
Variable::String(shared_string.as_ref().into()),
);
obj_map.insert(
Rc::from("other_key"),
Variable::String(shared_string.clone()),
zen_types::symbol::Symbol::from("other_key"),
Variable::String(shared_string.as_ref().into()),
);
let shared_obj = Rc::new(RefCell::new(obj_map));
@@ -238,16 +248,25 @@ fn serialize_shared_array_with_shared_strings() -> TestResult {
// Create a shared array containing the shared string
let shared_array = Rc::new(RefCell::new(vec![
Variable::String(shared_string.clone()),
Variable::String(shared_string.as_ref().into()),
Variable::Number(dec!(42.0)),
Variable::String(shared_string.clone()),
Variable::String(shared_string.as_ref().into()),
]));
// Use the shared array in multiple places
let mut root_map = HashMap::new();
root_map.insert(Rc::from("array1"), Variable::Array(shared_array.clone()));
root_map.insert(Rc::from("array2"), Variable::Array(shared_array.clone()));
root_map.insert(Rc::from("array3"), Variable::Array(shared_array));
root_map.insert(
zen_types::symbol::Symbol::from("array1"),
Variable::Array(shared_array.clone()),
);
root_map.insert(
zen_types::symbol::Symbol::from("array2"),
Variable::Array(shared_array.clone()),
);
root_map.insert(
zen_types::symbol::Symbol::from("array3"),
Variable::Array(shared_array),
);
let var = Variable::Object(Rc::new(RefCell::new(root_map)));
+14 -14
View File
@@ -1,7 +1,7 @@
use rust_decimal_macros::dec;
use serde_json::json;
use std::error::Error;
use std::rc::Rc;
use zen_types::rccell::RcCell;
use zen_types::variable::Variable;
type TestResult = Result<(), Box<dyn Error>>;
@@ -19,7 +19,7 @@ fn dot_operations() -> TestResult {
// Test dot get
assert_eq!(
var.dot("user.profile.name"),
Some(Variable::String(Rc::from("Alice")))
Some(Variable::String(("Alice").into()))
);
assert_eq!(var.dot("user.profile.nonexistent"), None);
assert_eq!(var.dot("nonexistent.path"), None);
@@ -27,21 +27,21 @@ fn dot_operations() -> TestResult {
// Test dot insert
let updated = var.dot_insert(
"user.profile.email",
Variable::String(Rc::from("alice@example.com")),
Variable::String(("alice@example.com").into()),
);
assert!(updated.is_none()); // Returns previous value (none)
assert_eq!(
var.dot("user.profile.email"),
Some(Variable::String(Rc::from("alice@example.com")))
Some(Variable::String(("alice@example.com").into()))
);
// Test dot insert detached
let new_var = var
.dot_insert_detached("settings.theme", Variable::String(Rc::from("dark")))
.dot_insert_detached("settings.theme", Variable::String(("dark").into()))
.ok_or_else(|| "Failed to insert detached path".to_string())?;
assert_eq!(
new_var.dot("settings.theme"),
Some(Variable::String(Rc::from("dark")))
Some(Variable::String(("dark").into()))
);
assert_eq!(var.dot("settings.theme"), None); // Original unchanged
@@ -63,33 +63,33 @@ fn clone_operations() -> TestResult {
// Test shallow clone - shares references
let shallow = original.shallow_clone();
if let (Variable::Array(orig_arr), Variable::Array(shallow_arr)) = (&original, &shallow) {
assert!(Rc::ptr_eq(orig_arr, shallow_arr));
assert!(RcCell::ptr_eq(orig_arr, shallow_arr));
}
// Test depth clone
let depth1 = original.depth_clone(1);
if let (Variable::Array(orig_arr), Variable::Array(depth_arr)) = (&original, &depth1) {
assert!(!Rc::ptr_eq(orig_arr, depth_arr)); // Different array refs
assert!(!RcCell::ptr_eq(orig_arr, depth_arr)); // Different array refs
let orig_nested = &orig_arr.borrow()[2];
let depth_nested = &depth_arr.borrow()[2];
if let (Variable::Object(orig_obj), Variable::Object(depth_obj)) =
(orig_nested, depth_nested)
{
assert!(Rc::ptr_eq(orig_obj, depth_obj)); // Nested still shared at depth 1
assert!(RcCell::ptr_eq(orig_obj, depth_obj)); // Nested still shared at depth 1
}
}
// Test deep clone - everything separate
let deep = original.deep_clone();
if let (Variable::Array(orig_arr), Variable::Array(deep_arr)) = (&original, &deep) {
assert!(!Rc::ptr_eq(orig_arr, deep_arr));
assert!(!RcCell::ptr_eq(orig_arr, deep_arr));
let orig_nested = &orig_arr.borrow()[2];
let deep_nested = &deep_arr.borrow()[2];
if let (Variable::Object(orig_obj), Variable::Object(deep_obj)) = (orig_nested, deep_nested)
{
assert!(!Rc::ptr_eq(orig_obj, deep_obj)); // Nested also separate
assert!(!RcCell::ptr_eq(orig_obj, deep_obj)); // Nested also separate
}
}
@@ -115,11 +115,11 @@ fn merge_operations() -> TestResult {
assert_eq!(merged.dot("user.age"), Some(Variable::Number(dec!(31)))); // Merged updated
assert_eq!(
merged.dot("user.email"),
Some(Variable::String(Rc::from("alice@example.com")))
Some(Variable::String(("alice@example.com").into()))
);
assert_eq!(
merged.dot("new_field"),
Some(Variable::String(Rc::from("value")))
Some(Variable::String(("value").into()))
);
// Test in-place merge
@@ -127,7 +127,7 @@ fn merge_operations() -> TestResult {
assert_eq!(doc.dot("user.age"), Some(Variable::Number(dec!(31)))); // Original now changed
assert_eq!(
doc.dot("user.name"),
Some(Variable::String(Rc::from("Alice")))
Some(Variable::String(("Alice").into()))
); // Preserved
assert_eq!(
doc.dot("settings.notifications"),