Merge commit '802db7e483c2b83175f9591be919d7ceb1eae7d7' into adr009/zset-ffi

# Conflicts:
#	Cargo.nix
#	Cargo.toml
This commit is contained in:
Claude (dbsp-circuit)
2026-10-10 13:17:02 -07:00
committed by Edward Langley
19 changed files with 1635 additions and 153 deletions
Generated
+100 -55
View File
@@ -205,7 +205,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -243,9 +243,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.4.7"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -319,7 +319,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -499,7 +499,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -510,7 +510,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
"darling_core",
"quote",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -585,9 +585,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "enum_dispatch"
@@ -655,9 +655,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "finl_unicode"
@@ -952,6 +952,15 @@ dependencies = [
"improvise-formula",
]
[[package]]
name = "improvise-incremental"
version = "0.1.0"
dependencies = [
"improvise-core",
"improvise-io",
"proptest",
]
[[package]]
name = "improvise-io"
version = "0.1.0-rc2"
@@ -1043,9 +1052,9 @@ dependencies = [
[[package]]
name = "indoc"
version = "2.0.7"
version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1"
dependencies = [
"rustversion",
]
@@ -1060,7 +1069,7 @@ dependencies = [
"indoc",
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -1114,15 +1123,15 @@ checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f"
[[package]]
name = "lazy_static"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]]
name = "libc"
version = "0.2.189"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "libloading"
@@ -1187,12 +1196,12 @@ dependencies = [
[[package]]
name = "mac_address"
version = "1.1.8"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a"
dependencies = [
"nix",
"winapi",
"nix 0.30.1",
"windows-sys",
]
[[package]]
@@ -1234,9 +1243,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.2.3"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
dependencies = [
"libc",
"log",
@@ -1249,6 +1258,18 @@ name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.13.2",
"cfg-if",
@@ -1423,19 +1444,21 @@ dependencies = [
[[package]]
name = "pest"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8"
checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa"
dependencies = [
"memchr",
"psm",
"stacker",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf"
checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f"
dependencies = [
"pest",
"pest_generator",
@@ -1443,22 +1466,22 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109"
checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.7",
]
[[package]]
name = "pest_meta"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb"
checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1"
dependencies = [
"pest",
]
@@ -1529,9 +1552,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "powerfmt"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
[[package]]
name = "ppv-lite86"
@@ -1579,6 +1602,15 @@ dependencies = [
"unarray",
]
[[package]]
name = "psm"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
dependencies = [
"cc",
]
[[package]]
name = "quick-error"
version = "1.2.3"
@@ -1932,7 +1964,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -2015,9 +2047,9 @@ checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "siphasher"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
[[package]]
name = "slab"
@@ -2027,9 +2059,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.16.2"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
[[package]]
name = "socket2"
@@ -2041,6 +2073,19 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "stacker"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -2098,9 +2143,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.6"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
dependencies = [
"proc-macro2",
"quote",
@@ -2172,7 +2217,7 @@ dependencies = [
"libc",
"log",
"memmem",
"nix",
"nix 0.29.0",
"num-derive",
"num-traits",
"ordered-float",
@@ -2233,7 +2278,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -2259,9 +2304,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "tokio"
version = "1.53.1"
version = "1.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
dependencies = [
"bytes",
"libc",
@@ -2280,7 +2325,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.7",
]
[[package]]
@@ -2361,9 +2406,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicase"
version = "2.9.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f"
[[package]]
name = "unicode-ident"
@@ -2402,9 +2447,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.26.1"
version = "1.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
dependencies = [
"atomic",
"getrandom 0.4.3",
@@ -2494,7 +2539,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.7",
"wasm-bindgen-shared",
]
@@ -2706,18 +2751,18 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "zerocopy"
version = "0.8.58"
version = "0.8.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44"
checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.58"
version = "0.8.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"
checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a"
dependencies = [
"proc-macro2",
"quote",
+215 -64
View File
@@ -103,6 +103,16 @@ rec {
# File a bug if you depend on any for non-debug work!
debug = internal.debugCrate { inherit packageId; };
};
"improvise-incremental" = rec {
packageId = "improvise-incremental";
build = internal.buildRustCrateWithFeatures {
packageId = "improvise-incremental";
};
# Debug support which might change between releases.
# File a bug if you depend on any for non-debug work!
debug = internal.debugCrate { inherit packageId; };
};
"improvise-io" = rec {
packageId = "improvise-io";
build = internal.buildRustCrateWithFeatures {
@@ -721,14 +731,14 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
features = [ "full" "fold" ];
}
];
devDependencies = [
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
features = [ "full" "fold" "parsing" ];
}
];
@@ -817,9 +827,9 @@ rec {
};
"cc" = rec {
crateName = "cc";
version = "1.4.7";
version = "1.7.0";
edition = "2021";
sha256 = "04z3q2wqsg4qgx4vsqsd01s27svz0bgdymjiyccgbnn24gg3whal";
sha256 = "12xvzyiwincbr6djn8ykbdlxv87ilva4pjxhkz156xc2iaplk9jh";
dependencies = [
{
name = "find-msvc-tools";
@@ -1062,7 +1072,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
features = [ "full" ];
}
];
@@ -1577,7 +1587,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
features = [ "full" "extra-traits" ];
}
];
@@ -1608,7 +1618,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
}
];
@@ -1855,9 +1865,9 @@ rec {
};
"either" = rec {
crateName = "either";
version = "1.18.0";
version = "1.19.0";
edition = "2021";
sha256 = "0d7dx31sf8rakcgp63070ngb2vkjynrni866pnx879pawndgnai5";
sha256 = "1gjq21g0sgk5ylpj85zafcinwhh3jj91i6drhb4278vw2v17370f";
features = {
"default" = [ "std" ];
"serde" = [ "dep:serde" ];
@@ -2047,9 +2057,9 @@ rec {
};
"find-msvc-tools" = rec {
crateName = "find-msvc-tools";
version = "0.1.13";
version = "0.1.14";
edition = "2021";
sha256 = "16ykhz2icc0xx8i3vr8fpp6h3djpik2zw5bcxbff9bxba5g909gg";
sha256 = "112ljldlv150fpl8xr2jl5czg51k3kdfn6cy5fqdsvkl14sgpp5f";
libName = "find_msvc_tools";
};
@@ -3047,6 +3057,30 @@ rec {
}
];
};
"improvise-incremental" = rec {
crateName = "improvise-incremental";
version = "0.1.0";
edition = "2024";
src = lib.cleanSourceWith { filter = sourceFilter; src = ./crates/improvise-incremental; };
libName = "improvise_incremental";
dependencies = [
{
name = "improvise-core";
packageId = "improvise-core";
}
];
devDependencies = [
{
name = "improvise-io";
packageId = "improvise-io";
}
{
name = "proptest";
packageId = "proptest";
}
];
};
"improvise-io" = rec {
crateName = "improvise-io";
@@ -3379,9 +3413,9 @@ rec {
};
"indoc" = rec {
crateName = "indoc";
version = "2.0.7";
version = "2.0.8";
edition = "2021";
sha256 = "01np60qdq6lvgh8ww2caajn9j4dibx9n58rvzf7cya1jz69mrkvr";
sha256 = "1lc4agds9bqf3lin6kkz2jgmahaqwrkaq1fkij5fpzvcg68jcyx3";
procMacro = true;
authors = [
"David Tolnay <dtolnay@gmail.com>"
@@ -3429,7 +3463,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
features = [ "derive" "full" ];
}
];
@@ -3557,9 +3591,9 @@ rec {
};
"lazy_static" = rec {
crateName = "lazy_static";
version = "1.5.0";
version = "1.5.1";
edition = "2015";
sha256 = "1zk6dqqni0193xg6iijh7i3i44sryglwgvx20spdvwk3r6sbrlmv";
sha256 = "1yqaqmp510xw2ldpw88mx9b5s5qj8flb4rw0wd9ks1zpk9j0z1r0";
authors = [
"Marvin Löbel <loebel.marvin@gmail.com>"
];
@@ -3570,9 +3604,9 @@ rec {
};
"libc" = rec {
crateName = "libc";
version = "0.2.189";
version = "0.2.190";
edition = "2021";
sha256 = "1whjfs375vlng2q6yrbzs73cvp5lm3w1n2gfqajb2vgf7zg3xbry";
sha256 = "0y5yap4bfp7rfsldcbk9pb5alcgygca5xn1n2pmh181zdpf3spff";
features = {
"default" = [ "std" ];
"rustc-dep-of-std" = [ "align" "rustc-std-workspace-core" ];
@@ -3738,24 +3772,24 @@ rec {
};
"mac_address" = rec {
crateName = "mac_address";
version = "1.1.8";
version = "1.2.0";
edition = "2018";
sha256 = "00r3n18mxglq1dzshnm0vxk1fgsp3c2hd08w6hfcqdp8ymmv5bn0";
sha256 = "06pbqk1hqiraavi2wxc9gxj1g73bbm7af7ccf6sx79g9kc5barxk";
authors = [
"rep-nop <repnop@outlook.com>"
];
dependencies = [
{
name = "nix";
packageId = "nix";
packageId = "nix 0.30.1";
target = { target, features }: (("linux" == target."os" or null) || ("macos" == target."os" or null) || ("freebsd" == target."os" or null) || ("netbsd" == target."os" or null) || ("openbsd" == target."os" or null) || ("android" == target."os" or null) || ("illumos" == target."os" or null));
features = [ "net" ];
}
{
name = "winapi";
packageId = "winapi";
name = "windows-sys";
packageId = "windows-sys";
target = { target, features }: (target."windows" or false);
features = [ "winerror" "ws2def" "iphlpapi" ];
features = [ "Win32_Networking_WinSock" "Win32_NetworkManagement_IpHelper" "Win32_NetworkManagement_Ndis" ];
}
];
features = {
@@ -3860,9 +3894,9 @@ rec {
};
"mio" = rec {
crateName = "mio";
version = "1.2.3";
version = "1.2.4";
edition = "2021";
sha256 = "1n5ryp7j5fga38z7php5yy9k7ia24rp6cl9gm27zwar6khz4862b";
sha256 = "1vl6px9zy0pwhlwnw3cknsqyzkg8bcpiwiq4cgicf2fwgywfv20p";
authors = [
"Carl Lerche <me@carllerche.com>"
"Thomas de Zeeuw <thomasdezeeuw@gmail.com>"
@@ -3898,7 +3932,7 @@ rec {
};
resolvedDefaultFeatures = [ "default" "log" "net" "os-ext" "os-poll" ];
};
"nix" = rec {
"nix 0.29.0" = rec {
crateName = "nix";
version = "0.29.0";
edition = "2021";
@@ -3906,6 +3940,53 @@ rec {
authors = [
"The nix-rust Project Developers"
];
dependencies = [
{
name = "bitflags";
packageId = "bitflags 2.13.2";
}
{
name = "cfg-if";
packageId = "cfg-if";
}
{
name = "libc";
packageId = "libc";
features = [ "extra_traits" ];
}
];
buildDependencies = [
{
name = "cfg_aliases";
packageId = "cfg_aliases";
}
];
features = {
"aio" = [ "pin-utils" ];
"dir" = [ "fs" ];
"memoffset" = [ "dep:memoffset" ];
"mount" = [ "uio" ];
"mqueue" = [ "fs" ];
"net" = [ "socket" ];
"pin-utils" = [ "dep:pin-utils" ];
"ptrace" = [ "process" ];
"sched" = [ "process" ];
"signal" = [ "process" ];
"socket" = [ "memoffset" ];
"ucontext" = [ "signal" ];
"user" = [ "feature" ];
"zerocopy" = [ "fs" "uio" ];
};
resolvedDefaultFeatures = [ "default" "fs" "mman" ];
};
"nix 0.30.1" = rec {
crateName = "nix";
version = "0.30.1";
edition = "2021";
sha256 = "1dixahq9hk191g0c2ydc0h1ppxj0xw536y6rl63vlnp06lx3ylkl";
authors = [
"The nix-rust Project Developers"
];
dependencies = [
{
name = "bitflags";
@@ -3935,6 +4016,7 @@ rec {
features = {
"aio" = [ "pin-utils" ];
"dir" = [ "fs" ];
"event" = [ "poll" ];
"memoffset" = [ "dep:memoffset" ];
"mount" = [ "uio" ];
"mqueue" = [ "fs" ];
@@ -3948,7 +4030,7 @@ rec {
"user" = [ "feature" ];
"zerocopy" = [ "fs" "uio" ];
};
resolvedDefaultFeatures = [ "default" "fs" "memoffset" "mman" "net" "socket" ];
resolvedDefaultFeatures = [ "default" "memoffset" "net" "socket" ];
};
"nom" = rec {
crateName = "nom";
@@ -4436,9 +4518,9 @@ rec {
};
"pest" = rec {
crateName = "pest";
version = "2.9.2";
version = "2.9.3";
edition = "2021";
sha256 = "1a11k95dyrpfrn2ql5lprdh4k31k4hksg83w67vfdlps62iarls5";
sha256 = "1yhr35ds3c5ww7nvh9nvs842p40nmwgqj7ql4z3accwbld5kfs5m";
authors = [
"Dragoș Tiselice <dragostiselice@gmail.com>"
];
@@ -4448,6 +4530,16 @@ rec {
packageId = "memchr";
optional = true;
}
{
name = "psm";
packageId = "psm";
optional = true;
}
{
name = "stacker";
packageId = "stacker";
optional = true;
}
{
name = "ucd-trie";
packageId = "ucd-trie";
@@ -4459,15 +4551,15 @@ rec {
"memchr" = [ "dep:memchr" ];
"miette-error" = [ "std" "pretty-print" "dep:miette" ];
"pretty-print" = [ "dep:serde" "dep:serde_json" ];
"std" = [ "ucd-trie/std" ];
"std" = [ "ucd-trie/std" "dep:stacker" "dep:psm" ];
};
resolvedDefaultFeatures = [ "default" "memchr" "std" ];
};
"pest_derive" = rec {
crateName = "pest_derive";
version = "2.9.2";
version = "2.9.3";
edition = "2021";
sha256 = "1ky22vlsbqha1jhx5l31d95gm8l54va38b9zmmr2wjicgxan0jr8";
sha256 = "0zygma96zndmadjsg6m353ak48ziab55c8mb43zsmsscj95ihvmn";
procMacro = true;
authors = [
"Dragoș Tiselice <dragostiselice@gmail.com>"
@@ -4493,9 +4585,9 @@ rec {
};
"pest_generator" = rec {
crateName = "pest_generator";
version = "2.9.2";
version = "2.9.3";
edition = "2021";
sha256 = "029igkh6nsw62bn4325gkjm5c3lf4bjqbkv8jblrwc0ml441z78x";
sha256 = "150nhpb5mh4vx1gmgl3fmix953pmy3r595xg4sk59pp4cz97hx58";
authors = [
"Dragoș Tiselice <dragostiselice@gmail.com>"
];
@@ -4519,7 +4611,7 @@ rec {
}
{
name = "syn";
packageId = "syn 2.0.119";
packageId = "syn 3.0.7";
}
];
features = {
@@ -4531,9 +4623,9 @@ rec {
};
"pest_meta" = rec {
crateName = "pest_meta";
version = "2.9.2";
version = "2.9.3";
edition = "2021";
sha256 = "1jxd8gbm328lsl3m0z2b1pq9ri60h4f7122i189k5jpwkwdbm4zd";
sha256 = "1qb9r2akz8q1wbvwmdipiaq06km3m8wn27w3kcssqha2icj6z62g";
authors = [
"Dragoș Tiselice <dragostiselice@gmail.com>"
];
@@ -4706,9 +4798,9 @@ rec {
};
"powerfmt" = rec {
crateName = "powerfmt";
version = "0.2.0";
version = "0.2.1";
edition = "2021";
sha256 = "14ckj2xdpkhv3h6l5sdmb9f1d57z8hbfpdldjc2vl5givq2y77j3";
sha256 = "0n293rvnpisjmxdqhq6zmrvcdql7j1c4zrcy5053vrv5x6wr8qsa";
authors = [
"Jacob Pratt <jacob@jhpratt.dev>"
];
@@ -4863,6 +4955,22 @@ rec {
};
resolvedDefaultFeatures = [ "bit-set" "default" "fork" "regex-syntax" "rusty-fork" "std" "tempfile" "timeout" ];
};
"psm" = rec {
crateName = "psm";
version = "0.1.24";
edition = "2015";
sha256 = "0428cnwx8i9dhkcjwzap0amg9cjk8nhj0xr5hkhm6zl543r9y2r0";
authors = [
"Simonas Kazlauskas <psm@kazlauskas.me>"
];
buildDependencies = [
{
name = "cc";
packageId = "cc";
}
];
};
"quick-error" = rec {
crateName = "quick-error";
version = "1.2.3";
@@ -6145,7 +6253,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
usesDefaultFeatures = false;
features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ];
}
@@ -6407,9 +6515,9 @@ rec {
};
"siphasher" = rec {
crateName = "siphasher";
version = "1.0.3";
version = "1.0.4";
edition = "2018";
sha256 = "0jg6l9xyzca5vy4h6gf8r6p4kk84g98fk95pzig1kq6cr4z8grcf";
sha256 = "0mn28y43123jdpskdn6r9wibmn066f7h30zkkqn88bd6hj8zxx1k";
authors = [
"Frank Denis <github@pureftpd.org>"
];
@@ -6438,9 +6546,9 @@ rec {
};
"smallvec" = rec {
crateName = "smallvec";
version = "1.16.2";
version = "1.16.3";
edition = "2018";
sha256 = "13iai5hhwyp8z0pbn8r11q4j5956jaxhcbvvf2drm17f1q7myfgr";
sha256 = "06z0akmcq2zc9qg22dxda298p50gg3di5ggl3y2nllag8ypwhgav";
authors = [
"The Servo Project Developers"
];
@@ -6481,6 +6589,49 @@ rec {
};
resolvedDefaultFeatures = [ "all" ];
};
"stacker" = rec {
crateName = "stacker";
version = "0.1.25";
edition = "2021";
sha256 = "0rwrws4iyh8cay7pghzf2dy55pprwfn1vm805f5czfh6cza4jzvh";
authors = [
"Alex Crichton <alex@alexcrichton.com>"
"Simonas Kazlauskas <stacker@kazlauskas.me>"
];
dependencies = [
{
name = "cfg-if";
packageId = "cfg-if";
}
{
name = "libc";
packageId = "libc";
}
{
name = "psm";
packageId = "psm";
}
{
name = "windows-sys";
packageId = "windows-sys";
target = { target, features }: (target.name == "arm64ec-pc-windows-msvc");
features = [ "Win32_System_Memory" "Win32_System_Threading" "Win32_Foundation" ];
}
{
name = "windows-sys";
packageId = "windows-sys";
target = { target, features }: ((target."windows" or false) && (!("arm64ec" == target."arch" or null)));
features = [ "Win32_System_Memory" "Win32_System_Threading" "Win32_Foundation" ];
}
];
buildDependencies = [
{
name = "cc";
packageId = "cc";
}
];
};
"static_assertions" = rec {
crateName = "static_assertions";
version = "1.1.0";
@@ -6623,11 +6774,11 @@ rec {
};
resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "extra-traits" "full" "parsing" "printing" "proc-macro" ];
};
"syn 3.0.6" = rec {
"syn 3.0.7" = rec {
crateName = "syn";
version = "3.0.6";
version = "3.0.7";
edition = "2021";
sha256 = "1vmw7s58rzrs926nv5m06x7qbgswm1aa9iw3s1bj5var47kyi4w5";
sha256 = "1p5agw0rj3f8spsnyqgj3c569nzyv5zz430xasi2qgskc42jwann";
authors = [
"David Tolnay <dtolnay@gmail.com>"
];
@@ -6854,7 +7005,7 @@ rec {
}
{
name = "nix";
packageId = "nix";
packageId = "nix 0.29.0";
target = { target, features }: (target."unix" or false);
features = [ "mman" "fs" ];
}
@@ -7040,7 +7191,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
}
];
@@ -7133,9 +7284,9 @@ rec {
};
"tokio" = rec {
crateName = "tokio";
version = "1.53.1";
version = "1.53.2";
edition = "2021";
sha256 = "1v8b3b45pkpbibls75yniqbvx5dlks2708141ljni5mnf6lawb10";
sha256 = "0i202ksji8q2asvii0adzgi8m0z93z3j7apn62qfh8d6qzy92pz9";
authors = [
"Tokio Contributors <team@tokio.rs>"
];
@@ -7262,7 +7413,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
features = [ "full" ];
}
];
@@ -7513,9 +7664,9 @@ rec {
};
"unicase" = rec {
crateName = "unicase";
version = "2.9.0";
version = "2.10.0";
edition = "2018";
sha256 = "0hh1wrfd7807mfph2q67jsxqgw8hm82xg2fb8ln8cvblkwxbri6v";
sha256 = "13x2h9s4jhch7rknqkqb1pb35irpj00fswy9ssgh0dm0qsnc6z1m";
authors = [
"Sean McArthur <sean@seanmonstar.com>"
];
@@ -7609,9 +7760,9 @@ rec {
};
"uuid" = rec {
crateName = "uuid";
version = "1.26.1";
version = "1.28.0";
edition = "2021";
sha256 = "1kl5nb7r3gpmkc43d6nbayvzqhcp2grczk6c7bxv80b6x70xmxif";
sha256 = "01mnlhz09hwdjaxwzjd6bql3fjb5zl9n8dzaxn7dvdxyhiiiihbw";
authors = [
"Ashley Mannix<ashleymannix@live.com.au>"
"Dylan DPC<dylan.dpc@gmail.com>"
@@ -7888,7 +8039,7 @@ rec {
}
{
name = "syn";
packageId = "syn 3.0.6";
packageId = "syn 3.0.7";
features = [ "visit" "visit-mut" "full" "extra-traits" ];
}
{
@@ -8644,7 +8795,7 @@ rec {
features = {
"debug" = [ "impl-debug" ];
};
resolvedDefaultFeatures = [ "consoleapi" "fileapi" "handleapi" "impl-default" "iphlpapi" "memoryapi" "namedpipeapi" "processenv" "processthreadsapi" "synchapi" "winbase" "winerror" "winnls" "winnt" "winsock2" "winuser" "ws2def" ];
resolvedDefaultFeatures = [ "consoleapi" "fileapi" "handleapi" "impl-default" "memoryapi" "namedpipeapi" "processenv" "processthreadsapi" "synchapi" "winbase" "winerror" "winnls" "winnt" "winsock2" "winuser" ];
};
"winapi-i686-pc-windows-gnu" = rec {
crateName = "winapi-i686-pc-windows-gnu";
@@ -9065,7 +9216,7 @@ rec {
"Win32_Web" = [ "Win32" ];
"Win32_Web_InternetExplorer" = [ "Win32_Web" ];
};
resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_Pipes" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_WindowsProgramming" "default" ];
resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_NetworkManagement" "Win32_NetworkManagement_IpHelper" "Win32_NetworkManagement_Ndis" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_Memory" "Win32_System_Pipes" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_WindowsProgramming" "default" ];
};
"winnow" = rec {
crateName = "winnow";
@@ -9114,9 +9265,9 @@ rec {
};
"zerocopy" = rec {
crateName = "zerocopy";
version = "0.8.58";
version = "0.8.62";
edition = "2021";
sha256 = "0i6vsbj4qfqj62b8qxl7wdcin8rnqz6pw88p6zzl5dc2mnpqyzn1";
sha256 = "17asjqfdkqmxsx42myw1x5jlb28m5axlf9ifldqpbiy7dbsjnl46";
dependencies = [
{
name = "zerocopy-derive";
@@ -9146,9 +9297,9 @@ rec {
};
"zerocopy-derive" = rec {
crateName = "zerocopy-derive";
version = "0.8.58";
version = "0.8.62";
edition = "2021";
sha256 = "1whbf7cjcqyp5nhvma78yy489swr7mfgc9m6r6h4ckyz8kh5cpsr";
sha256 = "06lx0gigf0j5pvp4dhlrv636b4mib37ijzlcc3i62rdhailj0msl";
procMacro = true;
libName = "zerocopy_derive";
dependencies = [
+4 -4
View File
@@ -1,5 +1,5 @@
[workspace]
members = [".", "crates/improvise-formula", "crates/improvise-core", "crates/improvise-protocol", "crates/improvise-io", "crates/improvise-command", "crates/improvise-wasm-client", "crates/improvise-ws-server", "crates/improvise-worker-server", "crates/gp-tree", "crates/improvise-gp", "crates/numtower", "crates/zset", "crates/zset-ffi"]
members = [".", "crates/improvise-formula", "crates/improvise-core", "crates/improvise-protocol", "crates/improvise-io", "crates/improvise-command", "crates/improvise-wasm-client", "crates/improvise-ws-server", "crates/improvise-worker-server", "crates/gp-tree", "crates/improvise-gp", "crates/numtower", "crates/zset", "crates/improvise-incremental", "crates/zset-ffi"]
[package]
name = "improvise"
@@ -70,9 +70,9 @@ improvise-wasm-client = { path = "crates/improvise-wasm-client" }
improvise-protocol = { path = "crates/improvise-protocol", features = ["test-support"] }
# `tests/headless_cli.rs` detaches the binary from any terminal with `setsid`.
libc = "0.2"
pest = "2.9.2"
pest_derive = "2.9.2"
pest_meta = "2.9.2"
pest = "2.9.3"
pest_derive = "2.9.3"
pest_meta = "2.9.3"
proptest = "1"
tempfile = "3"
+91
View File
@@ -0,0 +1,91 @@
# `improvise-incremental`: the reasoning
`crates/improvise-incremental` is ADR 009 step 2 (improvise-t8ow, epic improvise-bw4p): the
plug. It holds the `Derived` trait, `Eager` (today's evaluator behind it) and the
differential harness every later backend is checked with. There is no incremental backend
here, the evaluator is untouched and no existing crate changed. The decisions are the ADR's
(`context/adr/009-incremental-evaluation.md`, clause 2). The evidence for each test is in
`context/test-evidence/improvise-incremental.md`.
## The shape
| item | what it is |
|---|---|
| `Derived` | the two things the evaluator reads: `value(key, agg)` and `raw_fold(partial)` |
| `Eager` | an owned `Model` plus its None categories; every answer is a call into `Model` |
| `Maintained` | a `Derived` that is told each `Edit`; `Eager` is one |
| `Edit` | one model mutation as data (set, clear, add or remove a formula, set a measure's aggregation) |
| `probes`, `Probe` | every question worth asking of a model, and a way to put it to any backend |
| `differential` | runs one edit sequence on a candidate and on `Eager`, comparing every probe at every step |
| `Divergence`, `Report` | what a failure names (step, probe, both answers) and what a pass compared |
## Interpretations
Clause 2 names the trait and says what it carries ("the formula result at a key, and the raw
fold of the cells that match a partial key") and nothing else. These are the readings taken.
1. **`Derived` lives in this crate, not in improvise-core.** The ADR says improvise-core
"gains a trait". Putting it there changes an existing crate's source, which the brief for
this step ruled out, and the brief was ambiguous about it. This departs from the accepted
ADR's text, so it is flagged to the user and not decided here: improvise-bw4p.2 moves it
(one file and a `pub use`, since the trait names only core types). While it sits
downstream, core cannot read through it, which is the clause's point.
2. **Two methods, the minimum.** `value(key, agg)` is the formula result at a key, or the cell
as stored or folded; `raw_fold(partial)` is the fold of matching stored cells. `agg` is
`Aggregation`, the evaluator's own type. Hidden-item `covers`, totals and occupancy are not
in the trait, so the differential cannot probe them, and a hidden item (view state) cannot
be exercised at all. The ADR's differential compares "every drawn cell, total and occupancy
count"; improvise-bw4p.3 adds them. A group is exercised: a stored key with one coordinate
replaced by its item's group is a probe.
3. **`raw_fold` is never a formula, and `Eager` enforces it.** For a partial naming a formula
target `Eager::raw_fold` is `None`; otherwise it is `evaluate_aggregated(partial, OverHidden)`,
because the evaluator's own raw fold (`aggregate_raw`, `aggregate_cell_values`) is private to
core. Calling it through the public path is exact for any partial that names no formula
target. improvise-bw4p.2 exposes the raw fold from core so `Eager` stops going round.
4. **`Eager` recomputes formulas after every edit.** Production does it in `rebuild_layout`
(`Model::recompute_formulas(none_cats)`), and an over-hidden read of a formula cell reads
the cache it fills. So `Eager` carries the None categories of the view it stands for and
recomputes under them after each edit, as a layout rebuild does.
5. **`Edit` is the vocabulary of the harness, not of the wire.** Five variants, each one
public `Model` call. A later step adds variants for what it maintains (an item added, a
category removed); the harness does not care which. An `Edit` fault reaches both sides of a
differential alike, so it is pinned only where a direct `Eager` test reads the result:
`ClearCell`, `AddFormula` and `RemoveFormula`, and `SetMeasureAgg` through the fold tests.
`SetCell` is read by nearly every test.
6. **Values agree by float rules with every NaN one value** (`same_value`), the rule clause 3
states for cell deltas. Signed zero compares equal, so a later `ExactSum` cannot be told
from eager's sum by the sign of a zero here; clause 8 has its own property test for that.
7. **No `zset` dependency yet.** Nothing in this step uses a Z-set. It arrives with the first
backend, as the cell delta of step 3.
## What the harness checks
`differential(model, none_cats, candidate, edits)` builds `Eager` over a clone of the model,
applies each edit to both, and after step 0 and after every edit asks both every probe. The
probe set **accumulates**: probes from the model at every earlier step stay asked, so a backend
that goes on answering for a removed formula diverges. A pass returns a `Report` of how many
states and answers were compared.
`probes(model)` takes the base keys (each stored key, and each with one coordinate replaced by
its item's group), every sub-partial of a base key dropping at most `MAX_DROPPED = 3`
coordinates plus the empty key, and each formula's target over each of those. Each key is asked
as an exact value, an over-hidden value and a raw fold. A fold dropping more than three
coordinates from a stored key is not asked about. `edit_probes`
adds the key an edit names, so a cleared cell is asked about after it is gone. A divergence
reads `divergence at step N: <probe>: eager <a> != candidate <b>`.
**A limit, stated plainly (improvise-bw4p.4).** The probe set is every stored key's
sub-partials, not the keys a view draws. A view with four or more categories on None draws a cell
that drops four or more coordinates of a stored key, and the harness checks none of those cells:
a rival wrong only at `{C0=a, _Measure=Revenue}` over a five-category model passes. Neither
shipped demo reaches the limit, so the corpus does not show it. Raising `MAX_DROPPED` is not the
answer, because `sub_partials` walks all 2^n subsets of a key whatever the limit is (93ms at 20
categories and 225ms at 22 in a debug build, per call). The fix is to probe the keys each view
draws, with steps 3 and 4; until then a candidate on such a view is under-checked.
The corpus the tests use is `examples/demo.improv` and `web/demo.improv`, loaded through
`improvise_io::persistence::load`, and hand-built models (formulas, text, mixed values, a group).
`eager_matches_the_evaluator_on_the_shipped_documents` takes every view's None categories; the
differential runs take the first per document, and the largest document only its first four
edits, to keep the binary under two seconds in a debug build. A proptest (256 cases) runs random
edit sequences against a candidate that rebuilds an `Eager` from scratch on each edit.
@@ -0,0 +1,194 @@
# Test evidence: `improvise-incremental`
What each test guards, which rival it was run against, and where that rival failed. The
reasoning is in `context/code/improvise-incremental.md`; the decisions are ADR 009 clause 2's.
**How the rivals are kept.** The `the_harness_catches_*` tests run `differential` on a
`Rival`, an `Eager` told every edit and wrong in one way, and assert the step at which the
harness reports the divergence. They run on every build, so each rival is watched failing on
every run and not once by hand.
**Injected in the source.** Eighteen faults were put into `src/eager.rs`, `src/harness.rs`
and `src/edit.rs` one at a time, each run against the whole suite and each reverted and
followed by a full rerun. The failing tests, by name (a fault no test names survived, and
none did):
- `Eager::value` ignores its `Aggregation`: `eager_follows_an_edit_to_the_cell_a_fold_reads`,
`eager_matches_the_evaluator_on_the_shipped_documents`, `eager_raw_fold_is_never_a_formula`,
`eager_value_reads_a_formula_exactly_and_folded`,
`the_harness_catches_a_backend_that_ignores_the_aggregation`,
`the_harness_catches_a_wrong_folded_formula_value`;
- `Eager::raw_fold` reads `Exact`: `eager_raw_fold_sums_by_default`,
`eager_raw_fold_follows_the_measures_aggregation`, `eager_raw_fold_of_text_is_unanimous_or_mixed`,
`eager_numbers_among_text_are_mixed`, `eager_follows_an_edit_to_the_cell_a_fold_reads`,
`eager_matches_the_evaluator_on_the_shipped_documents`,
`the_harness_catches_a_raw_fold_that_returns_a_formula`;
- `Eager::raw_fold` returns a formula's value: `eager_raw_fold_is_never_a_formula`,
`eager_matches_the_evaluator_on_the_shipped_documents`,
`the_harness_catches_a_raw_fold_that_returns_a_formula`;
- `Eager::apply` does not recompute formulas: `eager_follows_an_edit_to_the_cell_a_fold_reads`,
`a_maintained_eager_equals_a_fresh_one_over_random_edit_sequences`;
- the harness compares only the first edit: `the_harness_catches_a_backend_that_ignores_a_clear`,
`…_ignores_the_measure_aggregation`, `…_goes_stale_after_two_edits`,
`the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula`;
- the harness never reports a divergence: all nine `the_harness_catches_*` tests;
- `same_value` always true: `same_value_follows_the_float_rules_and_is_exact_elsewhere` and four
`the_harness_catches_*` tests;
- `same_value` NaN unequal, signed zero by bits, any two text equal: each fails
`same_value_follows_the_float_rules_and_is_exact_elsewhere` and nothing else;
- `probes` omit formula cells: `probes_cover_folds_formulas_measures_and_groups` and four
`the_harness_catches_*` tests;
- `probes` drop at most one coordinate: `probes_cover_folds_formulas_measures_and_groups`,
`the_harness_catches_a_fault_that_only_shows_after_two_coordinates_are_dropped`;
- `probes` omit raw folds: `probes_cover_folds_formulas_measures_and_groups`,
`an_edit_names_its_key_as_two_value_probes_and_a_fold`,
`the_harness_catches_a_raw_fold_that_returns_a_formula`;
- `probes` omit group keys: `probes_cover_folds_formulas_measures_and_groups`,
`the_harness_catches_a_wrong_group_fold`;
- the probe history is forgotten between steps: `…_goes_stale_after_two_edits`,
`…_ignores_a_clear`, `the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula`;
- `edit_probes` names nothing: `an_edit_names_its_key_as_two_value_probes_and_a_fold`;
- `Edit::AddFormula` as a no-op: `eager_add_and_remove_formula_change_what_a_key_reads`;
- `Edit::ClearCell` applied as `SetCell(0.0)`, which both sides of a differential share and so
only a direct `Eager` test can see: `eager_clear_cell_leaves_the_key_absent`.
## eager_raw_fold_sums_by_default
Hand-computed: Revenue under Product A is 10 + 5 = 15. Fails on the `raw_fold`-reads-`Exact`
rival (a partial key names no stored cell, so the answer is `None`).
## eager_raw_fold_follows_the_measures_aggregation
After `measure_agg[Revenue] = Max` the same fold is 10, and Cost, unchanged, still sums to 6.
A fold that always sums fails on the first; one that always takes the maximum fails on the second.
## eager_raw_fold_of_text_is_unanimous_or_mixed
Note under A is two equal texts and reads as that text; under B it is two different texts and
reads `Error("mixed")`. A fold that drops text, which the evaluator used to do (gotcha 7),
returns `None` for both.
## eager_numbers_among_text_are_mixed
A text among numbers is `Error("mixed")`, not the sum of the numbers.
## eager_value_reads_a_formula_exactly_and_folded
`Profit = Revenue - Cost` reads 6 at East/A exactly, and 9 at Product A with Region over
hidden: (10 + 5) - (4 + 2), the fold taken before the subtraction. Fails on the
`value`-ignores-`Aggregation` rival.
## eager_follows_an_edit_to_the_cell_a_fold_reads
After East/A Revenue becomes 20 the fold is 25 and the folded Profit 19. The Profit half is
what fails when `apply` forgets to recompute formulas: the folded cache is then stale.
## eager_matches_the_evaluator_on_the_shipped_documents
Both shipped documents, every view's None categories, and the hand model: every probe of
`Eager` equals a direct `evaluate_aggregated` on a model recomputed under the same None
categories, and the probe set is non-empty. Fails on both aggregation rivals.
## eager_agrees_with_itself_over_every_corpus_edit_sequence
`differential` over a generated edit sequence on each corpus model returns a `Report` with
`edits + 1` steps and a non-zero probe count. The count assertion is what a harness that
compared nothing would fail; the step count is what one that stopped early would fail.
## the_harness_passes_a_backend_that_is_right
The control for the `catches` tests: `Eager` as the candidate over the hand-built edits is a
pass, so a harness that rejects everything is seen.
## the_harness_catches_a_backend_that_ignores_the_aggregation
A candidate that reads every `value` as `Exact` diverges at step 0, first at the full-key
formula cell `{East, A, Profit}` over hidden (eager `None`, candidate `6`).
## the_harness_catches_a_backend_that_ignores_the_measure_aggregation
A candidate that drops `SetMeasureAgg` diverges at step 2, the edit that sets it, on a Revenue key.
## the_harness_catches_a_backend_that_goes_stale_after_two_edits
A candidate that stops applying edits after the second diverges at step 3, on a Revenue key.
A prefix of steps cannot see a backend that goes wrong late.
## the_harness_catches_a_backend_that_ignores_a_clear
A candidate that keeps a cleared cell diverges at step 3 and the message names it.
## the_harness_catches_a_wrong_folded_formula_value
Review of PR 343 (Codex P1): a rival wrong only at `value({Product=A, _Measure=Profit}, OverHidden)`,
the Profit cell a view with Region on None draws, passed 128 probes, because formula cells were
built over full keys only. Now the divergence is at step 0 on exactly that probe, eager `9`,
candidate `-999`.
## the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula
Review of PR 343: probes were rebuilt from the post-edit model, so a candidate that ignores
`RemoveFormula` was never asked about Profit again. Probes now accumulate over every step. The
divergence is at the last step, on a Profit key, eager `None`, candidate `Some`.
## the_harness_catches_a_fault_that_only_shows_after_two_coordinates_are_dropped
Review of PR 343 (Copilot): only one-coordinate partials and the empty key were probed. A rival
wrong only at `{_Measure=Revenue}` now diverges at step 0 on `Value({_Measure=Revenue}, Exact)`.
The bound is `MAX_DROPPED = 3` coordinates; a fold dropping more is not asked about.
## the_harness_catches_a_raw_fold_that_returns_a_formula
Review of PR 343 (Copilot): `raw_fold` is documented never to be a formula. A rival whose
`raw_fold` is `value(partial, OverHidden)` diverges at step 0 on a `RawFold` of a Profit partial
(eager `None`, candidate `Some`). It could not be seen while `Eager` itself returned the formula.
## the_harness_catches_a_wrong_group_fold
A group name stands in for an item in the probe keys. A rival wrong only at
`{Region=Coastal, Product=A, _Measure=Revenue}` diverges at step 0, eager `15`, candidate `-999`.
## eager_folds_a_group_it_is_asked_about
The group fold in the grouped model is East + West = 15 exactly, so the rival test above has a
true answer to differ from.
## eager_raw_fold_is_never_a_formula
At `{Product=A, _Measure=Profit}` the folded value is `9` and the raw fold is `None`. Fails on the
fault that lets `raw_fold` read the formula cache.
## eager_add_and_remove_formula_change_what_a_key_reads
Review of PR 343: `Edit::AddFormula` as a no-op left all 25 tests green, because an `Edit` fault
reaches both sides of a differential alike. `Double = Revenue * 2` reads `None` before the edit
and `20` at East/A after it, and `None` again after `RemoveFormula`. Fails on `AddFormula` as a
no-op.
## eager_clear_cell_leaves_the_key_absent
After `ClearCell` the key reads `None` and the model holds no cell. A `ClearCell` applied as
`SetCell(0.0)` hits both sides of a differential identically, so only this direct test fails.
## same_value_follows_the_float_rules_and_is_exact_elsewhere
NaN equals NaN, `-0.0` equals `0.0`, `1.0` differs from `1.0000000000000002`, texts and errors
compare exactly, and `None` equals only `None`. Each of three one-line faults in `same_value`
(NaN unequal, signed zero by bits, any two texts equal) survived the whole suite until this test
existed.
## probes_cover_folds_formulas_measures_and_groups
`probes` includes a raw fold, a full key, a folded formula cell under both aggregations, the
two-dropped key `{_Measure=Revenue}`, the empty key and a group key. Review of PR 343 showed
`probes` omitting formula cells, `_Measure` keys or raw folds left every test green.
## an_edit_names_its_key_as_two_value_probes_and_a_fold
`edit_probes` of a `ClearCell` names the cleared key three ways, and of an edit naming no key
names nothing.
## a_maintained_eager_equals_a_fresh_one_over_random_edit_sequences
Clause 2 asks for a property test over a random edit sequence. A proptest (256 cases, up to four
edits of five kinds over the hand-built model) runs `differential` with a candidate that rebuilds
an `Eager` from scratch on every edit. It fails on the `Eager::apply`-skips-recompute fault, which
is the property: maintaining in place equals building afresh.
+1 -1
View File
@@ -17,7 +17,7 @@ serde = { version = "1", features = ["derive"] }
# wasm-clean.
siphasher = "1"
# Unicode full case folding for slugify (ADR 007 answer 26). no_std, no dependencies.
unicase = "2.9"
unicase = "2.10"
[features]
# Exposes the fold-evaluation counter to other crates' tests (improvise-0jw5).
+3 -3
View File
@@ -8,11 +8,11 @@ repository = "https://github.com/fiddlerwoaroof/improvise"
[dependencies]
anyhow = "1"
pest = "2.9.2"
pest_derive = "2.9.2"
pest = "2.9.3"
pest_derive = "2.9.3"
# For FormulaId alone, which crosses the wire (improvise-km71); no AST derive.
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
pest_meta = "2.9.2"
pest_meta = "2.9.3"
proptest = "1"
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "improvise-incremental"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
description = "The seam between improvise-core and the incremental circuit: the Derived trait, the Eager oracle and the differential harness (ADR 009 clause 2)."
# ADR 009 clause 2: depends on improvise-core, never the other way round. The
# `zset` edge arrives with the first backend that uses it (step 3), not before.
[dependencies]
improvise-core = { path = "../improvise-core" }
[dev-dependencies]
improvise-io = { path = "../improvise-io" }
proptest = "1"
+46
View File
@@ -0,0 +1,46 @@
//! Today's evaluator behind the trait. Temporary (ADR 009 clause 2).
use crate::{Derived, Edit, Maintained};
use improvise_core::model::cell::{CellKey, CellValue};
use improvise_core::model::{Aggregation, Model};
/// A model plus the None categories its formulas were last recomputed under.
/// It adds no logic: every answer is a call into `Model`.
#[derive(Debug, Clone)]
pub struct Eager {
model: Model,
none_cats: Vec<String>,
}
impl Eager {
pub fn new(mut model: Model, none_cats: Vec<String>) -> Self {
model.recompute_formulas(&none_cats);
Eager { model, none_cats }
}
pub fn model(&self) -> &Model {
&self.model
}
}
impl Derived for Eager {
fn value(&self, key: &CellKey, agg: Aggregation) -> Option<CellValue> {
self.model.evaluate_aggregated(key, agg)
}
fn raw_fold(&self, partial: &CellKey) -> Option<CellValue> {
match self.model.formula_for(partial) {
Some(_) => None,
None => self
.model
.evaluate_aggregated(partial, Aggregation::OverHidden),
}
}
}
impl Maintained for Eager {
fn apply(&mut self, edit: &Edit) {
edit.apply(&mut self.model);
self.model.recompute_formulas(&self.none_cats);
}
}
+46
View File
@@ -0,0 +1,46 @@
//! One mutation of a model, as data, so a sequence can be replayed on two backends.
use improvise_core::formula::ast::{AggFunc, Formula};
use improvise_core::model::Model;
use improvise_core::model::cell::{CellKey, CellValue};
#[derive(Debug, Clone)]
pub enum Edit {
SetCell(CellKey, CellValue),
ClearCell(CellKey),
AddFormula(Formula),
RemoveFormula {
target: String,
target_category: String,
},
SetMeasureAgg {
measure: String,
func: AggFunc,
},
}
impl Edit {
/// The one place an edit becomes a call on the model.
pub fn apply(&self, model: &mut Model) {
match self {
Edit::SetCell(key, value) => model.set_cell(key.clone(), value.clone()),
Edit::ClearCell(key) => model.clear_cell(key),
Edit::AddFormula(formula) => model.add_formula(formula.clone()),
Edit::RemoveFormula {
target,
target_category,
} => model.remove_formula(target, target_category),
Edit::SetMeasureAgg { measure, func } => {
model.measure_agg.insert(measure.clone(), func.clone());
}
}
}
/// The key an edit names, if it names one: always probed after it.
pub fn key(&self) -> Option<&CellKey> {
match self {
Edit::SetCell(key, _) | Edit::ClearCell(key) => Some(key),
_ => None,
}
}
}
+195
View File
@@ -0,0 +1,195 @@
//! The differential harness: one edit sequence, two backends, every probe after every step.
use crate::{Derived, Eager, Edit};
use improvise_core::model::cell::{CellKey, CellValue};
use improvise_core::model::{Aggregation, Model};
use std::collections::HashSet;
use std::fmt;
/// A backend that holds state of its own and is told each edit.
pub trait Maintained: Derived {
fn apply(&mut self, edit: &Edit);
}
/// One question put to both backends.
#[derive(Debug, Clone, PartialEq)]
pub enum Probe {
Value(CellKey, Aggregation),
RawFold(CellKey),
}
impl Probe {
pub fn ask(&self, backend: &dyn Derived) -> Option<CellValue> {
match self {
Probe::Value(key, agg) => backend.value(key, *agg),
Probe::RawFold(partial) => backend.raw_fold(partial),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Divergence {
/// 0 is the model as built, `n` the state after edit `n`.
pub step: usize,
pub probe: Probe,
pub eager: Option<CellValue>,
pub candidate: Option<CellValue>,
}
impl fmt::Display for Divergence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"divergence at step {}: {:?}: eager {:?} != candidate {:?}",
self.step, self.probe, self.eager, self.candidate
)
}
}
/// How many states and how many answers were compared: a run that compared
/// nothing must be visible as such.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Report {
pub steps: usize,
pub probes: usize,
}
/// Values agree when equal by float rules with every NaN one value, so a NaN
/// answer is not a divergence from itself.
pub fn same_value(a: &Option<CellValue>, b: &Option<CellValue>) -> bool {
match (a, b) {
(Some(CellValue::Number(x)), Some(CellValue::Number(y))) => {
x == y || (x.is_nan() && y.is_nan())
}
_ => a == b,
}
}
/// The most coordinates a probed partial key drops from a stored key; the
/// empty key is probed as well. A fold dropping more is not asked about.
pub const MAX_DROPPED: usize = 3;
/// The questions asked of a backend, once each, in the order first added.
#[derive(Default)]
struct Asked {
seen: HashSet<(CellKey, u8)>,
order: Vec<Probe>,
}
impl Asked {
fn extend(&mut self, probes: impl IntoIterator<Item = Probe>) {
for p in probes {
let tag = match &p {
Probe::Value(k, Aggregation::Exact) => (k.clone(), 0),
Probe::Value(k, Aggregation::OverHidden) => (k.clone(), 1),
Probe::RawFold(k) => (k.clone(), 2),
};
if self.seen.insert(tag) {
self.order.push(p);
}
}
}
}
fn all_three(key: CellKey) -> [Probe; 3] {
[
Probe::Value(key.clone(), Aggregation::Exact),
Probe::Value(key.clone(), Aggregation::OverHidden),
Probe::RawFold(key),
]
}
/// Every key `key` keeps after dropping at most `MAX_DROPPED` coordinates.
fn sub_partials(key: &CellKey) -> impl Iterator<Item = CellKey> + '_ {
let n = key.0.len();
(0..1u64 << n)
.filter(|mask| mask.count_ones() as usize <= MAX_DROPPED)
.map(move |mask| {
let kept = (0..n)
.filter(|i| mask >> i & 1 == 0)
.map(|i| key.0[i].clone());
CellKey::new(kept.collect())
})
}
/// Every question worth asking of `model`; the bound and the key set are
/// in `context/code/improvise-incremental.md`.
pub fn probes(model: &Model) -> Vec<Probe> {
let stored: Vec<CellKey> = model.data.iter_cells().map(|(k, _)| k).collect();
let grouped = stored.iter().flat_map(|k| {
k.0.iter().filter_map(|(cat, item)| {
let group = model.category(cat)?.items.get(item)?.group.clone()?;
Some(k.clone().with(cat, group))
})
});
let base: Vec<CellKey> = stored.iter().cloned().chain(grouped).collect();
let partials = base
.iter()
.flat_map(sub_partials)
.chain(std::iter::once(CellKey::new(vec![])));
let mut asked = Asked::default();
for key in partials {
let formula_cells = model
.formulas()
.iter()
.map(|f| key.clone().with(&f.target_category, &f.target))
.collect::<Vec<_>>();
asked.extend(all_three(key));
asked.extend(formula_cells.into_iter().flat_map(all_three));
}
asked.order
}
/// What an edit names: its key, asked all three ways, after the edit.
pub fn edit_probes(edit: &Edit) -> Vec<Probe> {
edit.key()
.cloned()
.into_iter()
.flat_map(all_three)
.collect()
}
fn compare(
step: usize,
eager: &Eager,
candidate: &dyn Derived,
asked: &Asked,
) -> Result<usize, Divergence> {
for probe in &asked.order {
let (want, got) = (probe.ask(eager), probe.ask(candidate));
if !same_value(&want, &got) {
return Err(Divergence {
step,
probe: probe.clone(),
eager: want,
candidate: got,
});
}
}
Ok(asked.order.len())
}
/// Runs `edits` on `candidate` and on `Eager`, comparing every probe ever
/// asked (so a removed formula stays asked about) at each step. `candidate` is built from `model` with the same `none_cats`.
pub fn differential<C: Maintained>(
model: &Model,
none_cats: &[String],
mut candidate: C,
edits: &[Edit],
) -> Result<Report, Divergence> {
let mut eager = Eager::new(model.clone(), none_cats.to_vec());
let mut asked = Asked::default();
asked.extend(probes(eager.model()));
let mut probes_run = compare(0, &eager, &candidate, &asked)?;
for (n, edit) in edits.iter().enumerate() {
eager.apply(edit);
candidate.apply(edit);
asked.extend(probes(eager.model()));
asked.extend(edit_probes(edit));
probes_run += compare(n + 1, &eager, &candidate, &asked)?;
}
Ok(Report {
steps: edits.len() + 1,
probes: probes_run,
})
}
+27
View File
@@ -0,0 +1,27 @@
//! The plug: what the evaluator reads, and the oracle that answers it today.
//! Reasoning: `context/code/improvise-incremental.md`.
mod eager;
mod edit;
mod harness;
pub use eager::Eager;
pub use edit::Edit;
pub use harness::{
Divergence, MAX_DROPPED, Maintained, Probe, Report, differential, edit_probes, probes,
same_value,
};
use improvise_core::model::Aggregation;
use improvise_core::model::cell::{CellKey, CellValue};
/// The two things the evaluator reads (ADR 009 clause 2).
pub trait Derived {
/// The value at `key`: the formula result when a formula owns it, else the
/// cell as stored or folded over what `agg` hides. `Model::evaluate_aggregated`.
fn value(&self, key: &CellKey, agg: Aggregation) -> Option<CellValue>;
/// The raw fold of the stored cells that match `partial`, by the measure's
/// aggregation. Never a formula: `None` for a partial naming a formula target.
fn raw_fold(&self, partial: &CellKey) -> Option<CellValue>;
}
@@ -0,0 +1,633 @@
//! `Eager` against the evaluator, and the harness against wrong backends.
//! Evidence: `context/test-evidence/improvise-incremental.md`.
use improvise_core::formula::ast::AggFunc;
use improvise_core::formula::parse_formula;
use improvise_core::model::cell::{CellKey, CellValue};
use improvise_core::model::{Aggregation, Model};
use improvise_core::view::Axis;
use improvise_incremental::{
Derived, Divergence, Eager, Edit, Maintained, Probe, differential, probes, same_value,
};
use proptest::prelude::*;
use std::path::Path;
fn key(region: &str, product: &str, measure: &str) -> CellKey {
CellKey::new(vec![
("Region".into(), region.into()),
("Product".into(), product.into()),
("_Measure".into(), measure.into()),
])
}
fn partial(product: &str, measure: &str) -> CellKey {
CellKey::new(vec![
("Product".into(), product.into()),
("_Measure".into(), measure.into()),
])
}
fn num(n: f64) -> Option<CellValue> {
Some(CellValue::Number(n))
}
fn text(s: &str) -> Option<CellValue> {
Some(CellValue::Text(s.into()))
}
fn mixed() -> Option<CellValue> {
Some(CellValue::Error("mixed".into()))
}
/// Region is the None category. Revenue and Cost are numbers; Note is text,
/// unanimous under A and conflicting under B.
fn hand_model() -> Model {
let mut m = Model::new("hand");
for (cat, items) in [("Region", ["East", "West"]), ("Product", ["A", "B"])] {
m.add_category(cat).unwrap();
for item in items {
m.category_mut(cat).unwrap().add_item(item);
}
}
for measure in ["Revenue", "Cost", "Note"] {
m.category_mut("_Measure").unwrap().add_item(measure);
}
let rows = [
("East", "A", 10.0, 4.0, "n", "n"),
("West", "A", 5.0, 2.0, "n", "n"),
("East", "B", 7.0, 3.0, "x", "x"),
("West", "B", 1.0, 1.0, "y", "y"),
];
for (r, p, rev, cost, note, _) in rows {
m.set_cell(key(r, p, "Revenue"), CellValue::Number(rev));
m.set_cell(key(r, p, "Cost"), CellValue::Number(cost));
m.set_cell(key(r, p, "Note"), CellValue::Text(note.into()));
}
m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
m
}
fn hand_eager() -> Eager {
Eager::new(hand_model(), vec!["Region".into()])
}
fn hand_edits() -> Vec<Edit> {
vec![
Edit::SetCell(key("East", "A", "Revenue"), CellValue::Number(20.0)),
Edit::SetMeasureAgg {
measure: "Revenue".into(),
func: AggFunc::Max,
},
Edit::ClearCell(key("West", "A", "Revenue")),
Edit::SetCell(key("West", "B", "Revenue"), CellValue::Text("oops".into())),
Edit::AddFormula(parse_formula("Double = Revenue * 2", "_Measure").unwrap()),
Edit::RemoveFormula {
target: "Profit".into(),
target_category: "_Measure".into(),
},
]
}
#[test]
fn eager_raw_fold_sums_by_default() {
assert_eq!(hand_eager().raw_fold(&partial("A", "Revenue")), num(15.0));
}
#[test]
fn eager_raw_fold_follows_the_measures_aggregation() {
let mut eager = hand_eager();
eager.apply(&Edit::SetMeasureAgg {
measure: "Revenue".into(),
func: AggFunc::Max,
});
assert_eq!(eager.raw_fold(&partial("A", "Revenue")), num(10.0));
assert_eq!(eager.raw_fold(&partial("A", "Cost")), num(6.0));
}
#[test]
fn eager_raw_fold_of_text_is_unanimous_or_mixed() {
let eager = hand_eager();
assert_eq!(eager.raw_fold(&partial("A", "Note")), text("n"));
assert_eq!(eager.raw_fold(&partial("B", "Note")), mixed());
}
#[test]
fn eager_value_reads_a_formula_exactly_and_folded() {
let eager = hand_eager();
assert_eq!(
eager.value(&key("East", "A", "Profit"), Aggregation::Exact),
num(6.0)
);
assert_eq!(
eager.value(&partial("A", "Profit"), Aggregation::OverHidden),
num(9.0)
);
}
#[test]
fn eager_follows_an_edit_to_the_cell_a_fold_reads() {
let mut eager = hand_eager();
eager.apply(&Edit::SetCell(
key("East", "A", "Revenue"),
CellValue::Number(20.0),
));
assert_eq!(eager.raw_fold(&partial("A", "Revenue")), num(25.0));
assert_eq!(
eager.value(&partial("A", "Profit"), Aggregation::OverHidden),
num(19.0)
);
}
#[test]
fn eager_numbers_among_text_are_mixed() {
let mut eager = hand_eager();
eager.apply(&Edit::SetCell(
key("West", "B", "Revenue"),
CellValue::Text("oops".into()),
));
assert_eq!(eager.raw_fold(&partial("B", "Revenue")), mixed());
}
fn corpus() -> Vec<(String, Model, Vec<String>)> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let mut out = Vec::new();
for file in ["examples/demo.improv", "web/demo.improv"] {
let wb = improvise_io::persistence::load(&root.join(file)).unwrap();
let mut seen: Vec<Vec<String>> = Vec::new();
for (name, view) in &wb.views {
let none: Vec<String> = view
.categories_on(Axis::None)
.into_iter()
.map(String::from)
.collect();
if !seen.contains(&none) {
seen.push(none.clone());
out.push((format!("{file}:{name}"), wb.model.clone(), none));
}
}
}
out.push(("hand".into(), hand_model(), vec!["Region".into()]));
out
}
/// Edits built from the model's own first cell: overwrite, text, clear, a new
/// record, a new aggregation, and the first formula removed then restored.
fn edits_for(model: &Model) -> Vec<Edit> {
let Some((first, _)) = model.data.iter_cells().next() else {
return vec![];
};
let (last_cat, _) = first.0.last().unwrap().clone();
let measure = first.get("_Measure").map(String::from);
let fresh = first.clone().with(last_cat, "Zz-new");
let mut edits = vec![
Edit::SetCell(first.clone(), CellValue::Number(7.5)),
Edit::SetCell(first.clone(), CellValue::Text("t".into())),
Edit::SetCell(fresh.clone(), CellValue::Number(3.0)),
Edit::ClearCell(first),
];
edits.extend(measure.map(|m| Edit::SetMeasureAgg {
measure: m,
func: AggFunc::Max,
}));
if let Some(f) = model.formulas().first() {
edits.push(Edit::RemoveFormula {
target: f.target.clone(),
target_category: f.target_category.clone(),
});
edits.push(Edit::AddFormula(f.clone()));
}
edits.push(Edit::ClearCell(fresh));
edits
}
#[test]
fn eager_matches_the_evaluator_on_the_shipped_documents() {
for (name, model, none) in corpus() {
let eager = Eager::new(model.clone(), none.clone());
let mut direct = model.clone();
direct.recompute_formulas(&none);
let asked = probes(eager.model());
assert!(!asked.is_empty(), "{name}: no probes");
for probe in asked {
let want = match &probe {
Probe::Value(k, a) => direct.evaluate_aggregated(k, *a),
Probe::RawFold(k) if direct.formula_for(k).is_some() => None,
Probe::RawFold(k) => direct.evaluate_aggregated(k, Aggregation::OverHidden),
};
assert_eq!(probe.ask(&eager), want, "{name}: {probe:?}");
}
}
}
#[test]
fn eager_agrees_with_itself_over_every_corpus_edit_sequence() {
let mut files_done: Vec<String> = Vec::new();
for (name, model, none) in corpus() {
let file = name.split(':').next().unwrap().to_string();
if files_done.contains(&file) {
continue;
}
files_done.push(file);
let mut edits = edits_for(&model);
// Probes grow with the cells squared-ish, and a debug build pays for it.
if model.data.iter_cells().count() > 40 {
edits.truncate(4);
}
let report = differential(
&model,
&none,
Eager::new(model.clone(), none.clone()),
&edits,
)
.unwrap_or_else(|d| panic!("{name}: {d}"));
assert_eq!(report.steps, edits.len() + 1, "{name}");
assert!(report.probes > 0, "{name}");
}
}
fn grouped_model() -> Model {
let mut m = Model::new("grouped");
m.add_category("Region").unwrap();
m.add_category("Product").unwrap();
m.category_mut("Region")
.unwrap()
.add_group(improvise_core::model::category::Group::new("Coastal"));
let region = m.category_mut("Region").unwrap();
region.add_item_in_group("East", "Coastal");
region.add_item_in_group("West", "Coastal");
region.add_item("Inland");
m.category_mut("Product").unwrap().add_item("A");
m.category_mut("_Measure").unwrap().add_item("Revenue");
for (r, v) in [("East", 10.0), ("West", 5.0), ("Inland", 2.0)] {
m.set_cell(key(r, "A", "Revenue"), CellValue::Number(v));
}
m
}
fn coastal() -> CellKey {
key("Coastal", "A", "Revenue")
}
/// A wrong backend: an `Eager` told every edit, answering one thing wrongly.
#[derive(Clone, Copy)]
enum Fault {
IgnoresAggregation,
IgnoresMeasureAggregation,
GoesStaleAfterTwoEdits,
IgnoresClear,
WrongFoldedFormula,
IgnoresRemoveFormula,
WrongWhenTwoAreDropped,
RawFoldReturnsFormula,
WrongOnAGroup,
}
struct Rival {
inner: Eager,
fault: Fault,
seen: usize,
}
impl Rival {
fn over(inner: Eager, fault: Fault) -> Self {
Rival {
inner,
fault,
seen: 0,
}
}
fn of(fault: Fault) -> Self {
Self::over(hand_eager(), fault)
}
}
impl Derived for Rival {
fn value(&self, k: &CellKey, agg: Aggregation) -> Option<CellValue> {
let wrong = match self.fault {
Fault::WrongFoldedFormula => {
*k == partial("A", "Profit") && agg == Aggregation::OverHidden
}
Fault::WrongWhenTwoAreDropped => *k == only_revenue(),
Fault::WrongOnAGroup => *k == coastal() && agg == Aggregation::Exact,
_ => false,
};
match (wrong, self.fault) {
(true, _) => num(-999.0),
(_, Fault::IgnoresAggregation) => self.inner.value(k, Aggregation::Exact),
_ => self.inner.value(k, agg),
}
}
fn raw_fold(&self, p: &CellKey) -> Option<CellValue> {
match self.fault {
Fault::WrongWhenTwoAreDropped if *p == only_revenue() => num(-999.0),
Fault::RawFoldReturnsFormula => self.inner.value(p, Aggregation::OverHidden),
_ => self.inner.raw_fold(p),
}
}
}
impl Maintained for Rival {
fn apply(&mut self, edit: &Edit) {
self.seen += 1;
let skip = match (self.fault, edit) {
(Fault::IgnoresMeasureAggregation, Edit::SetMeasureAgg { .. }) => true,
(Fault::GoesStaleAfterTwoEdits, _) => self.seen > 2,
(Fault::IgnoresClear, Edit::ClearCell(_)) => true,
(Fault::IgnoresRemoveFormula, Edit::RemoveFormula { .. }) => true,
_ => false,
};
if !skip {
self.inner.apply(edit);
}
}
}
fn only_revenue() -> CellKey {
CellKey::new(vec![("_Measure".into(), "Revenue".into())])
}
fn measure_of(probe: &Probe) -> Option<&str> {
match probe {
Probe::Value(k, _) | Probe::RawFold(k) => k.get("_Measure"),
}
}
fn run_rival(fault: Fault) -> Result<improvise_incremental::Report, Divergence> {
differential(
&hand_model(),
&["Region".to_string()],
Rival::of(fault),
&hand_edits(),
)
}
#[test]
fn the_harness_passes_a_backend_that_is_right() {
let report = differential(
&hand_model(),
&["Region".to_string()],
hand_eager(),
&hand_edits(),
)
.unwrap();
assert_eq!(report.steps, hand_edits().len() + 1);
}
#[test]
fn the_harness_catches_a_backend_that_ignores_the_aggregation() {
let d = run_rival(Fault::IgnoresAggregation).unwrap_err();
assert_eq!(d.step, 0);
assert_eq!(
d.probe,
Probe::Value(key("East", "A", "Profit"), Aggregation::OverHidden)
);
assert_eq!((d.eager, d.candidate), (None, num(6.0)));
}
#[test]
fn the_harness_catches_a_backend_that_ignores_the_measure_aggregation() {
let d = run_rival(Fault::IgnoresMeasureAggregation).unwrap_err();
assert_eq!(d.step, 2, "{d}");
assert_eq!(measure_of(&d.probe), Some("Revenue"), "{d}");
}
#[test]
fn the_harness_catches_a_backend_that_goes_stale_after_two_edits() {
let d = run_rival(Fault::GoesStaleAfterTwoEdits).unwrap_err();
assert_eq!(d.step, 3, "{d}");
assert_eq!(measure_of(&d.probe), Some("Revenue"), "{d}");
}
#[test]
fn the_harness_catches_a_backend_that_ignores_a_clear() {
let d = run_rival(Fault::IgnoresClear).unwrap_err();
assert_eq!(d.step, 3, "{d}");
assert_eq!(measure_of(&d.probe), Some("Revenue"), "{d}");
assert!(d.to_string().starts_with("divergence at step 3:"), "{d}");
}
#[test]
fn the_harness_catches_a_wrong_folded_formula_value() {
let d = run_rival(Fault::WrongFoldedFormula).unwrap_err();
assert_eq!(d.step, 0);
assert_eq!(
d.probe,
Probe::Value(partial("A", "Profit"), Aggregation::OverHidden)
);
assert_eq!((d.eager, d.candidate), (num(9.0), num(-999.0)));
}
#[test]
fn the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula() {
let d = run_rival(Fault::IgnoresRemoveFormula).unwrap_err();
assert_eq!(d.step, hand_edits().len(), "{d}");
assert_eq!(measure_of(&d.probe), Some("Profit"), "{d}");
assert!(d.eager.is_none() && d.candidate.is_some(), "{d}");
}
#[test]
fn the_harness_catches_a_fault_that_only_shows_after_two_coordinates_are_dropped() {
let d = run_rival(Fault::WrongWhenTwoAreDropped).unwrap_err();
assert_eq!(d.step, 0);
assert_eq!(d.probe, Probe::Value(only_revenue(), Aggregation::Exact));
assert_eq!(d.candidate, num(-999.0));
}
#[test]
fn the_harness_catches_a_raw_fold_that_returns_a_formula() {
let d = run_rival(Fault::RawFoldReturnsFormula).unwrap_err();
assert_eq!(d.step, 0);
assert!(matches!(d.probe, Probe::RawFold(_)), "{d}");
assert_eq!(measure_of(&d.probe), Some("Profit"), "{d}");
assert!(d.eager.is_none() && d.candidate.is_some(), "{d}");
}
#[test]
fn the_harness_catches_a_wrong_group_fold() {
let eager = Eager::new(grouped_model(), vec![]);
let d = differential(
&grouped_model(),
&[],
Rival::over(eager, Fault::WrongOnAGroup),
&[],
)
.unwrap_err();
assert_eq!(d.probe, Probe::Value(coastal(), Aggregation::Exact));
assert_eq!((d.eager, d.candidate), (num(15.0), num(-999.0)));
}
#[test]
fn eager_folds_a_group_it_is_asked_about() {
let eager = Eager::new(grouped_model(), vec![]);
assert_eq!(eager.value(&coastal(), Aggregation::Exact), num(15.0));
}
#[test]
fn eager_raw_fold_is_never_a_formula() {
let eager = hand_eager();
assert_eq!(
eager.value(&partial("A", "Profit"), Aggregation::OverHidden),
num(9.0)
);
assert_eq!(eager.raw_fold(&partial("A", "Profit")), None);
}
#[test]
fn eager_add_and_remove_formula_change_what_a_key_reads() {
let mut eager = hand_eager();
let k = key("East", "A", "Double");
assert_eq!(eager.value(&k, Aggregation::Exact), None);
eager.apply(&Edit::AddFormula(
parse_formula("Double = Revenue * 2", "_Measure").unwrap(),
));
assert_eq!(eager.value(&k, Aggregation::Exact), num(20.0));
eager.apply(&Edit::RemoveFormula {
target: "Double".into(),
target_category: "_Measure".into(),
});
assert_eq!(eager.value(&k, Aggregation::Exact), None);
}
#[test]
fn eager_clear_cell_leaves_the_key_absent() {
let mut eager = hand_eager();
let k = key("East", "A", "Revenue");
eager.apply(&Edit::ClearCell(k.clone()));
assert_eq!(eager.value(&k, Aggregation::Exact), None);
assert_eq!(eager.model().get_cell(&k), None);
}
#[test]
fn same_value_follows_the_float_rules_and_is_exact_elsewhere() {
let n = |x: f64| num(x);
assert!(same_value(&n(f64::NAN), &n(f64::NAN)));
assert!(same_value(&n(-0.0), &n(0.0)));
assert!(!same_value(&n(1.0), &n(1.000_000_000_000_000_2)));
assert!(!same_value(&n(1.0), &n(f64::NAN)));
assert!(same_value(&text("a"), &text("a")));
assert!(!same_value(&text("a"), &text("b")));
let err = |s: &str| Some(CellValue::Error(s.into()));
assert!(same_value(&err("mixed"), &err("mixed")));
assert!(!same_value(&err("mixed"), &err("circular")));
assert!(!same_value(&None, &n(0.0)));
assert!(same_value(&None, &None));
}
#[test]
fn probes_cover_folds_formulas_measures_and_groups() {
let hand = probes(&hand_model());
let has = |p: Probe| hand.contains(&p);
assert!(has(Probe::RawFold(partial("A", "Revenue"))));
assert!(has(Probe::Value(
key("East", "A", "Revenue"),
Aggregation::Exact
)));
assert!(has(Probe::Value(
partial("A", "Profit"),
Aggregation::Exact
)));
assert!(has(Probe::Value(
partial("A", "Profit"),
Aggregation::OverHidden
)));
assert!(has(Probe::Value(only_revenue(), Aggregation::OverHidden)));
assert!(has(Probe::RawFold(CellKey::new(vec![]))));
let grouped = probes(&grouped_model());
assert!(grouped.contains(&Probe::Value(coastal(), Aggregation::Exact)));
}
#[test]
fn an_edit_names_its_key_as_two_value_probes_and_a_fold() {
let k = key("East", "A", "Revenue");
let asked = improvise_incremental::edit_probes(&Edit::ClearCell(k.clone()));
assert!(asked.contains(&Probe::Value(k.clone(), Aggregation::Exact)));
assert!(asked.contains(&Probe::Value(k.clone(), Aggregation::OverHidden)));
assert!(asked.contains(&Probe::RawFold(k)));
let none = improvise_incremental::edit_probes(&Edit::SetMeasureAgg {
measure: "Revenue".into(),
func: AggFunc::Max,
});
assert!(none.is_empty());
}
/// An `Eager` rebuilt from scratch on every edit: maintaining one in place
/// must give the same answers as building it afresh.
struct Fresh {
model: Model,
none: Vec<String>,
eager: Eager,
}
impl Derived for Fresh {
fn value(&self, k: &CellKey, agg: Aggregation) -> Option<CellValue> {
self.eager.value(k, agg)
}
fn raw_fold(&self, p: &CellKey) -> Option<CellValue> {
self.eager.raw_fold(p)
}
}
impl Maintained for Fresh {
fn apply(&mut self, edit: &Edit) {
edit.apply(&mut self.model);
self.eager = Eager::new(self.model.clone(), self.none.clone());
}
}
fn any_key() -> impl Strategy<Value = CellKey> {
(
prop::sample::select(vec!["East", "West"]),
prop::sample::select(vec!["A", "B"]),
prop::sample::select(vec!["Revenue", "Cost", "Note"]),
)
.prop_map(|(r, p, m)| key(r, p, m))
}
fn any_value() -> impl Strategy<Value = CellValue> {
prop_oneof![
(-4i32..5).prop_map(|n| CellValue::Number(f64::from(n))),
Just(CellValue::Number(-0.0)),
prop::sample::select(vec!["n", "x"]).prop_map(|t| CellValue::Text(t.into())),
]
}
fn any_edit() -> impl Strategy<Value = Edit> {
prop_oneof![
(any_key(), any_value()).prop_map(|(k, v)| Edit::SetCell(k, v)),
any_key().prop_map(Edit::ClearCell),
prop::sample::select(vec!["Profit = Revenue - Cost", "Double = Revenue * 2"])
.prop_map(|raw| Edit::AddFormula(parse_formula(raw, "_Measure").unwrap())),
prop::sample::select(vec!["Profit", "Double"]).prop_map(|t| Edit::RemoveFormula {
target: t.into(),
target_category: "_Measure".into(),
}),
(
prop::sample::select(vec!["Revenue", "Cost"]),
prop::sample::select(vec![AggFunc::Sum, AggFunc::Max, AggFunc::Min, AggFunc::Avg]),
)
.prop_map(|(m, func)| Edit::SetMeasureAgg {
measure: m.into(),
func,
}),
]
}
proptest! {
#[test]
fn a_maintained_eager_equals_a_fresh_one_over_random_edit_sequences(
edits in prop::collection::vec(any_edit(), 0..4)
) {
let model = hand_model();
let none = vec!["Region".to_string()];
let fresh = Fresh {
model: model.clone(),
none: none.clone(),
eager: Eager::new(model.clone(), none.clone()),
};
let run = differential(&model, &none, fresh, &edits);
prop_assert!(run.is_ok(), "{}", run.unwrap_err());
}
}
+3 -3
View File
@@ -14,8 +14,8 @@ chrono = { version = "0.4", features = ["serde"] }
csv = "1"
flate2 = "1"
indexmap = "2"
pest = "2.9.2"
pest_derive = "2.9.2"
pest = "2.9.3"
pest_derive = "2.9.3"
serde = { version = "1", features = ["derive"] }
# Parses JSON a user's numbers cross, so it must read back the f64 written: per crate,
# because each build stands alone (improvise-6yvg, context/code/protocol.md).
@@ -47,6 +47,6 @@ features = [
]
[dev-dependencies]
pest_meta = "2.9.2"
pest_meta = "2.9.3"
proptest = "1"
tempfile = "3"
Generated
+12 -12
View File
@@ -47,11 +47,11 @@
},
"crane": {
"locked": {
"lastModified": 1789760033,
"narHash": "sha256-jtT4yxZpR8seYnlCMMWSSPlFN92zO6ICuZ8pJrmi86k=",
"lastModified": 1790899423,
"narHash": "sha256-THGtz65UGJU+sonZidRboyCMk+9DclpMF7pwL4uDV4M=",
"owner": "ipetkov",
"repo": "crane",
"rev": "73b980519cefc727a5f6cc8e5c0947a2f9be6edd",
"rev": "47b6b27ed9a3a9181415e4367d0c30ab2a0e0250",
"type": "github"
},
"original": {
@@ -71,11 +71,11 @@
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1782759041,
"narHash": "sha256-2577vxyoBa8+ZRiXr3CuPtOuEtPRYsFPSZuEc/KI/80=",
"lastModified": 1790579054,
"narHash": "sha256-S2NrwKB7aQa6HZsg2UGCmzQPI5Dw+bqUPm8R23BAUh8=",
"owner": "nix-community",
"repo": "crate2nix",
"rev": "5e1ecfd2d15b34ec90c2e51fdffbe8116595a767",
"rev": "1cb60331b14f15dad145dbd4e207c1ec110e5675",
"type": "github"
},
"original": {
@@ -297,11 +297,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1790185690,
"narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=",
"lastModified": 1791456040,
"narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4975466d324710c576dc11ad614684e6bd8cad8e",
"rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8",
"type": "github"
},
"original": {
@@ -368,11 +368,11 @@
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1790234952,
"narHash": "sha256-tjQa1vDoHJir4lasZTakz/xgmsfB6SRjeQqaIbefFOk=",
"lastModified": 1791535654,
"narHash": "sha256-QFgGi6Wgh7AgqPFdAXEmkTgnR0B5M0omheIHvo/02dU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "ed34466c59b766481248959a21a6e46cf0c60468",
"rev": "4ca2963243427cb86bd6b4d9ca6010db0162a619",
"type": "github"
},
"original": {
+2
View File
@@ -121,8 +121,10 @@
pkgs.rust-analyzer
crate2nix.packages.${system}.default
pkgs.cargo-expand
pkgs.cargo-edit
pkgs.cargo-llvm-cov
pkgs.global
pkgs.iconv
pkgs.universal-ctags
beads.packages.${system}.default
codeMaat
+44 -11
View File
@@ -307,19 +307,21 @@ dependencies = [
[[package]]
name = "pest"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8"
checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa"
dependencies = [
"memchr",
"psm",
"stacker",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf"
checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f"
dependencies = [
"pest",
"pest_generator",
@@ -327,22 +329,22 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109"
checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.6",
]
[[package]]
name = "pest_meta"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb"
checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1"
dependencies = [
"pest",
]
@@ -362,6 +364,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
dependencies = [
"cc",
]
[[package]]
name = "quote"
version = "1.0.47"
@@ -450,6 +461,19 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "stacker"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.119"
@@ -489,9 +513,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicase"
version = "2.9.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f"
[[package]]
name = "unicode-ident"
@@ -624,6 +648,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zlib-rs"
version = "0.6.8"
+2
View File
@@ -50,6 +50,8 @@
"numtower"
# ADR 009 clause 1: std-only; nothing depends on it yet.
"zset"
# ADR 009 clause 2: the Derived trait, Eager and the differential harness.
"improvise-incremental"
];
pFlags = crates: lib.concatMapStringsSep " " (c: "-p ${c}") crates;
+2
View File
@@ -69,6 +69,7 @@ const SECTIONS = [
["context/code/gp.md", "code/gp", "gp-tree and improvise-gp"],
["context/code/numtower.md", "code/numtower", "numtower, the numeric tower"],
["context/code/zset.md", "code/zset", "zset, the Z-set group"],
["context/code/improvise-incremental.md", "code/improvise-incremental", "improvise-incremental, the plug"],
["context/code/kb-tool.md", "code/kb-tool", "The kb tool"],
["context/code/tooling.md", "code/tooling", "The repository's own checkers"],
],
@@ -89,6 +90,7 @@ const SECTIONS = [
["context/test-evidence/gp.md", "test-evidence/gp", "gp-tree and improvise-gp"],
["context/test-evidence/numtower.md", "test-evidence/numtower", "numtower"],
["context/test-evidence/zset.md", "test-evidence/zset", "zset"],
["context/test-evidence/improvise-incremental.md", "test-evidence/improvise-incremental", "improvise-incremental"],
["context/test-evidence/tooling.md", "test-evidence/tooling", "The repository's own gates"],
["context/test-evidence/layout-cost.md", "test-evidence/layout-cost", "Layout and keypress cost"],
["context/test-evidence/scale-bisect.md", "test-evidence/scale-bisect", "Scale bisect"],