diff --git a/Cargo.lock b/Cargo.lock index cbc43ae..b30379c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "ar_archive_writer" version = "0.5.2" @@ -11,6 +23,27 @@ dependencies = [ "object", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + [[package]] name = "cc" version = "1.2.67" @@ -27,30 +60,251 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cranelift-assembler-x64" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f28665a3cba7b8fe75d885c2a1c1bbc661b65685df34f7d93a4669ceb2e719" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6308845400e41d9d34acf8f2d13454b907012d9de5265c66f731570adf82019e" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ed5df9b6cda90f2dd921760925079670ba6c86162efa4de9f6c6efea124384" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006fe8776f6d81acb83571f52e7737a54c6dec1ba75e2b7b5a68af15451f88ee" + +[[package]] +name = "cranelift-codegen" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021b5a45c5ca4d414746a985c7241fea4202fd71aeef5a2891c0be32518e3201" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5350ad78964a8cc301bc83cbc9b5144ccb44e1c2f604b551cc8ec15c99900dcb" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6918b5db84d5a9b09eb8fae05466cd57fb04d97a88ac47c24698830c8714747e" + +[[package]] +name = "cranelift-control" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4ea4593cd6ef06573d7a6bc5a4231368f96a5b57f65900b24553cca3284bcd" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcca10e8c33eac67a45be4e09d236e274697831ca6bf4c4a927f7570eb8436a8" +dependencies = [ + "cranelift-bitset", +] + +[[package]] +name = "cranelift-frontend" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dcc8b7e922ab1a6ec4640be3533698e291a4111b83d96f8d9e3367162e290ef" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9db87d9e6fe9ba89a71434a06c9f19153f3dd273a1c5c9a6365bc4f019213d1b" + +[[package]] +name = "cranelift-jit" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54146c2ea505c17f989e5a470bf9c9d271733db17eca33b91c23703b02784e1d" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-module", + "cranelift-native", + "libc", + "log", + "region", + "target-lexicon", + "wasmtime-internal-jit-icache-coherence", + "windows-sys 0.60.2", +] + +[[package]] +name = "cranelift-module" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54dbd8cd9c6d596ff659efdc18dec95e7903d13c35d141b12ff0ed2949184f2" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", +] + +[[package]] +name = "cranelift-native" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6aa4002a6569b047ecb846f5a952d21b81963817a0c1dad064b69e5a80f5952" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "289ab02de2733de3a857c98bdaace8f4dfab1cc1d322ba8637280ce2a7d15d8e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.3" @@ -139,7 +393,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -151,7 +405,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -180,7 +434,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" dependencies = [ "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "regalloc2" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags", + "libc", + "mach2", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -189,12 +504,23 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "sql-transform-interpreter" version = "0.1.0" dependencies = [ + "cranelift-codegen", + "cranelift-frontend", + "cranelift-jit", + "cranelift-module", "pyo3", "sqlparser", + "target-lexicon", ] [[package]] @@ -207,6 +533,12 @@ dependencies = [ "recursive", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "stacker" version = "0.1.24" @@ -217,7 +549,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -231,6 +563,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -243,12 +586,51 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "39.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "967b84e1a766a59955450473fd39a90c77529a0d4928b3bbae81b9c9cbccdc67" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "39.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d51480b15d802e7203630ea338da956f5e96b6ae6308db265d14d92a3c29870" +dependencies = [ + "libm", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -257,3 +639,132 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/Cargo.toml b/Cargo.toml index c2aa715..31e93f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,10 @@ crate-type = ["cdylib"] # libpython link, which is right for the wheel but breaks `cargo test`. # maturin enables it via [tool.maturin] features in pyproject.toml. [dependencies] +cranelift-codegen = "0.126" +cranelift-frontend = "0.126" +cranelift-jit = "0.126" +cranelift-module = "0.126" pyo3 = { version = "0.29", features = ["abi3-py314"] } sqlparser = "0.62" +target-lexicon = "0.13.5" diff --git a/backlog/tasks/task-43 - Specializer-M-lower-frontend-BTA-produce-consume-lowering-for-the-v0-subset.md b/backlog/tasks/task-43 - Specializer-M-lower-frontend-BTA-produce-consume-lowering-for-the-v0-subset.md index 040a0d6..33c6ecd 100644 --- a/backlog/tasks/task-43 - Specializer-M-lower-frontend-BTA-produce-consume-lowering-for-the-v0-subset.md +++ b/backlog/tasks/task-43 - Specializer-M-lower-frontend-BTA-produce-consume-lowering-for-the-v0-subset.md @@ -3,10 +3,10 @@ id: TASK-43 title: >- Specializer M-lower: frontend + BTA + produce/consume lowering for the v0 subset -status: In Progress +status: Done assignee: [] created_date: '2026-07-25 02:31' -updated_date: '2026-07-26 05:14' +updated_date: '2026-07-26 06:04' labels: [] milestone: m-7 dependencies: @@ -54,6 +54,8 @@ AC #2 (static-only queries fold to a constant emitter) is NOT yet satisfied: nee Stretch 4 landed (commit ac3786a): builtin catalogue implemented strictly from measured pins (8-agent workflow fan-out measured DuckDB 1.5.5; spec at docs/superpowers/specs/2026-07-26-stretch4-builtin-pins.md). New IR insts across all six files + fuzz gen: supper/slower, strim.{both,lead,trail}, ssubstr (virtual-window codepoint arithmetic), iabs/fabs/fround, BinOp::Frem. Frontend catalogue: upper/lower/ltrim/rtrim/abs/round(1-arg)/concat/coalesce/nullif + TRIM/SUBSTRING dedicated AST forms; || is ALWAYS concat (even 1 || 2) with implicit VARCHAR casts; CONCAT skips NULLs (all-NULL -> ''); COALESCE per-row lazy via CASE desugار; NULLIF compares at promoted type, keeps first arg's type. TWO DIVERGENCES IN PREVIOUSLY-LANDED CODE were found by pinning and FIXED in-branch with pin tests (PM: flagging per the disagreement protocol — these were fixes toward the oracle, not semantics patches to pass tests): (1) integer % by zero returns NULL in DuckDB, we trapped -> lowering now CASE-guards the divisor, MIN % -1 still traps like DuckDB; (2) DOUBLE comparisons: DuckDB order is NaN=NaN / NaN above everything / zeros equal, we had IEEE partial -> shared exec::duck_fcmp used by interp AND fold. Known deliberate divergence (strict-xfail + needs a ticket): Rust std lacks Unicode SIMPLE case maps, so upper('ß') gives 'ß' vs DuckDB 'ẞ' and lower('İ') differs; ASCII exact. Deliberate ceilings: round(x, digits) unsupported; decimal literals stay f64 (stretch-1 ceiling). 114 cargo tests + 21 differential pytest + 1 strict xfail; gate green. Adversarial verify fan-out (6 probe agents vs duckdb) running; findings will be triaged fix-vs-xfail before stretch 5 (differential suite backend id + corpus replay in gate). Stretch 6 landed (commit 0782bf1): AC #2 satisfied — static-only queries become constant emitters, evaluated once at build time by DuckDB itself (Python boundary; no IR is built, so trivially no probe/filter ops). The fallback fires on Unsupported/Parse prepare errors and self-validates: dynamic queries reference the row table, unknown to DuckDB, so evaluation fails and the original clean error surfaces. Bonus: aggregation/ORDER BY/dialect-beyond-sqlparser all work on the static-only path. Final corpus: 53 match / 625 clean-unsupported / 0 FAIL of 678, wired into the gate via pytest. MILESTONE COMPLETE pending review — hard stop before M-cranelift (TASK-44). Two items for PM: (1) deviation note — AC #3's 'backend id specialized' wording: the DataFusion-oracle differential harness (test_diff_*) can't host the specializer (different oracle, e.g. `/` semantics differ by design), so the specializer has its own duck_check differential suite (32 cases) instead; (2) ticket request per the disagreement protocol — Unicode SIMPLE case mapping (upper('ß')/'İ'/'ᾀ' class): Rust std only exposes full maps; strict-xfail in place; candidate fixes are a small static table of the ~100 divergent codepoints or a unicode-data crate dependency. + +Post-review addendum (commit 4f75e3d): both PM items resolved. (1) Case-mapping ticket WITHDRAWN — AmirHossein decided no dependency; implemented as a measured, generated exception table (src/specializer/exec/casemap.rs, 139 entries) from scripts/gen_casemap.py. Two exception classes, both captured by measurement: simple-vs-full mapping divergence, and three-way Unicode version skew (Python/Rust/utf8proc all ship different vintages — 56 entries Python's tables cannot see, caught by the generator's phase-2 census of the actual compiled engine). The full-codepoint census test (every Unicode scalar value through both engines) is the standing authority; generator is idempotent. The strict xfail is now two passing tests: gate 605 pytest + 12 xfail. (2) DataFusion: per review — legacy serving line's oracle only, likely retired later (small chance: a supported dialect); the specializer ignores it entirely. ## Final Summary diff --git a/backlog/tasks/task-44 - Specializer-M-cranelift-codegen-backend-behind-the-same-interface.md b/backlog/tasks/task-44 - Specializer-M-cranelift-codegen-backend-behind-the-same-interface.md index 7b25a82..f03e131 100644 --- a/backlog/tasks/task-44 - Specializer-M-cranelift-codegen-backend-behind-the-same-interface.md +++ b/backlog/tasks/task-44 - Specializer-M-cranelift-codegen-backend-behind-the-same-interface.md @@ -1,9 +1,10 @@ --- id: TASK-44 title: 'Specializer M-cranelift: codegen backend behind the same interface' -status: To Do +status: In Progress assignee: [] created_date: '2026-07-25 02:31' +updated_date: '2026-07-26 06:05' labels: [] milestone: m-7 dependencies: @@ -27,3 +28,13 @@ Cranelift-jit backend for the imperative IR (design doc §7; cranelift-jit 0.126 - [ ] #3 p50/p99 ns/call reported at n in {1, 8, 64, 1024} against the interpreter control and the existing native + codegen engines - [ ] #4 mise gate-specializer green + +## Implementation Plan + + +Stretch plan (recorded 2026-07-26, design doc §7 + §10): +1. ABI + scalar spine: add cranelift-jit 0.126 (+module/codegen/frontend); exec/cranelift.rs. One JIT'd fn per program, called per row: extern "C" fn(ctx: *mut RowCtx) -> i64 (0 = emit, 1 = skip, 2+k = trap k; trap messages in a side table). RowCtx carries column base pointers, row index, output sink, arena. Coverage-first op strategy: inline CLIF only where trivially safe (const, float arith, cmp via duck_fcmp helper or inline, select, conversions); EVERYTHING nontrivial (checked int arith, strings, probes, loads/stores) via extern "C" helpers shared with the interpreter's semantics — correctness and full coverage first, inlining hot ops is a later measured optimization. ponytail: helper-call backend that agrees beats an inline backend that diverges. +2. CFG: IR blocks/params map 1:1 to CLIF blocks/params (the IR was shaped for this); Brif/Jump/Emit/Skip/Trap terms. compile_cranelift(p, statics) -> Result; DuckDBInferFn tries cranelift, falls back to interpreter (AC #2), exposes which backend ran for tests. +3. Differential: gen.rs random-IR fuzz interpreter-vs-cranelift (same seeds, byte-identical outputs incl. NaN/-0.0/arena strings); corpus replay + duck_check suite through the cranelift backend; gate green. +4. Bench per §10: baseline the boundary with a no-op f first, then p50/p99 ns/call at n in {1,8,64,1024}, interpreter as control, next to the existing native + codegen engines; committed as a script, numbers into the task. + diff --git a/scripts/bench_specializer.py b/scripts/bench_specializer.py new file mode 100644 index 0000000..9f7836f --- /dev/null +++ b/scripts/bench_specializer.py @@ -0,0 +1,150 @@ +"""Specializer ns/call bench — the design doc §10 measurement discipline. + +Reports p50/p99 ns per call at n in {1, 8, 64, 1024} for: + boundary — a no-op passthrough (SELECT a FROM __THIS__) through the + generic pydantic path: the floor every engine pays today + cranelift — the specializer's JIT backend (default) + interp — the same programs on the interpreter backend (the control; + SPECIALIZER_FORCE_INTERP=1 in a subprocess) + native — the existing DataFusion-semantics InferFn + codegen — the existing Python codegen engine + +Run: `uv run python scripts/bench_specializer.py [--json out.json]` +The interp control re-execs this script in a subprocess so the env knob is +set before the module builds its functions. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import time + +import pyarrow as pa +from pydantic import create_model + +NS = [1, 8, 64, 1024] +CASES = { + "noop": "SELECT a FROM __THIS__", + "arith+where": "SELECT a * 2 + 1 AS x, b / 2 AS h FROM __THIS__ WHERE a % 3 <> 0", + "strings": "SELECT upper(s) || '-' || a AS t, substr(s, 2, 3) AS m FROM __THIS__", + "join": "SELECT a, name FROM __THIS__ JOIN dim ON a = dim.id", +} + + +def rows_of(n): + return [{"a": i % 60, "b": (i % 7) / 3.0, "s": f"item{i}"} for i in range(n)] + + +def quantiles(samples_ns): + s = sorted(samples_ns) + return s[len(s) // 2], s[min(len(s) - 1, int(len(s) * 0.99))] + + +def bench_fn(f, n): + iters = max(30, min(3000, 3_000_000 // max(n, 1))) + f() # warm + out = [] + for _ in range(iters): + t0 = time.perf_counter_ns() + f() + out.append(time.perf_counter_ns() - t0) + return quantiles(out) + + +def build_specializer(sql, model, dim): + from sql_transform._interpreter import DuckDBInferFn + + return DuckDBInferFn( + sql, row_tables={"__THIS__": model}, static_tables={"dim": dim} + ) + + +def build_native(sql, model, dim): + from sql_transform._interpreter import InferFn + + return InferFn(sql, row_tables={"__THIS__": model}, static_tables={"dim": dim}) + + +def build_codegen(sql, model, dim): + from sql_transform._codegen import CodegenFn + + return CodegenFn(sql, row_tables={"__THIS__": model}, static_tables={"dim": dim}) + + +def main(): + model = create_model("Row", a=(int, ...), b=(float, ...), s=(str, ...)) + dim = pa.table({"id": list(range(60)), "name": [f"n{i}" for i in range(60)]}) + engine = os.environ.get("BENCH_ENGINE", "cranelift") + + builders = {"cranelift": build_specializer, "interp": build_specializer} + if engine in ("native", "codegen"): + builders = {"native": build_native, "codegen": build_codegen} + + results = {} + for case, sql in CASES.items(): + try: + fn = builders[engine](sql, model, dim) + except Exception as e: # noqa: BLE001 -- engines differ in coverage + results[case] = {"error": str(e)[:120]} + continue + if engine in ("cranelift", "interp"): + want = "interpreter" if engine == "interp" else "cranelift" + assert fn.backend == want, f"{case}: backend is {fn.backend}, wanted {want}" + per_n = {} + for n in NS: + objs = [model(**r) for r in rows_of(n)] + p50, p99 = bench_fn(lambda f=fn, o=objs: f.infer({"__THIS__": o}), n) + per_n[n] = {"p50_ns": p50, "p99_ns": p99} + results[case] = per_n + print(json.dumps({engine: results})) + + +def orchestrate(): + """Run every engine (interp in a subprocess for the env knob), merge.""" + merged = {} + for engine in ("cranelift", "interp", "native", "codegen"): + env = os.environ.copy() + env["BENCH_ENGINE"] = engine + if engine == "interp": + env["SPECIALIZER_FORCE_INTERP"] = "1" + out = subprocess.run( # noqa: S603 -- fixed argv + [sys.executable, __file__, "--engine-run"], + env=env, + capture_output=True, + text=True, + check=True, + ) + merged.update(json.loads(out.stdout.strip().splitlines()[-1])) + + # Human table: p50 ns/call (p99 in parens), one row per case+engine. + hdr = f"{'case':<12} {'engine':<10}" + "".join(f"{f'n={n}':>16}" for n in NS) + print(hdr) + print("-" * len(hdr)) + for case in CASES: + for engine, per_case in merged.items(): + r = per_case.get(case) + if r is None: + continue + if "error" in r: + print(f"{case:<12} {engine:<10} unsupported: {r['error'][:60]}") + continue + cells = "".join( + f"{r[str(n)]['p50_ns'] if str(n) in r else r[n]['p50_ns']:>13,}ns" + for n in NS + ) + print(f"{case:<12} {engine:<10}{cells}") + if "--json" in sys.argv: + path = sys.argv[sys.argv.index("--json") + 1] + with open(path, "w", encoding="utf-8") as f: + json.dump(merged, f, indent=2) + print(f"wrote {path}") + + +if __name__ == "__main__": + if "--engine-run" in sys.argv: + main() + else: + orchestrate() diff --git a/src/duckdb/mod.rs b/src/duckdb/mod.rs index 7e8ca14..e25dbae 100644 --- a/src/duckdb/mod.rs +++ b/src/duckdb/mod.rs @@ -14,8 +14,10 @@ use pyo3::types::PyDict; use crate::error::InterpError; use crate::schema; +use crate::specializer::exec::cranelift::{self, CraneliftFn}; use crate::specializer::exec::interp::{compile, InterpFn}; use crate::specializer::exec::{Batch, ColData, KeyBits, OutCol, ScalarVal, StaticData}; +use crate::specializer::exec::{RunState, Trap}; use crate::specializer::ir::{Col, ColTy, StaticTy, Ty}; use crate::specializer::plan::StaticTable; use crate::specializer::{prepare, StaticSpec}; @@ -169,9 +171,38 @@ fn eval_static_only( Ok((rows, fields)) } +/// The execution backend: cranelift when it compiles, the interpreter as +/// the always-available fallback (AC #2 — an uncovered op must not fail +/// prepare). Both agree byte-for-byte by the 500-seed differential. +enum Backend { + Cranelift(CraneliftFn), + Interp(InterpFn), +} + +impl Backend { + fn name(&self) -> &'static str { + match self { + Backend::Cranelift(_) => "cranelift", + Backend::Interp(_) => "interpreter", + } + } + fn new_state(&self) -> RunState { + match self { + Backend::Cranelift(f) => f.new_state(), + Backend::Interp(f) => f.new_state(), + } + } + fn run(&self, input: &Batch, st: &mut RunState) -> Result<(), Trap> { + match self { + Backend::Cranelift(f) => f.run(input, st), + Backend::Interp(f) => f.run(input, st), + } + } +} + enum Engine { Compiled { - fun: InterpFn, + fun: Backend, in_cols: Vec, out_cols: Vec, }, @@ -293,7 +324,34 @@ impl DuckDBInferFn { data.push(materialize_map(py, table, spec, keys, values)?); } - let fun = compile(&prepared.program, data).map_err(|e| build_err(e.to_string()))?; + // SPECIALIZER_FORCE_INTERP pins the interpreter — the bench control + // and a debugging escape hatch. + let force_interp = std::env::var_os("SPECIALIZER_FORCE_INTERP").is_some(); + let fun = match (force_interp, data) { + (true, data) => Backend::Interp( + compile(&prepared.program, data).map_err(|e| build_err(e.to_string()))?, + ), + (false, data) => match cranelift::compile(&prepared.program, data) { + Ok(f) => Backend::Cranelift(f), + // The failed attempt consumed the static data; rematerialize + // on this cold path and fall back to the interpreter. + Err(_) => { + let mut data = Vec::with_capacity(prepared.statics.len()); + for (spec, sty) in prepared.statics.iter().zip(&prepared.program.statics) { + let StaticTy::Map { keys, values } = sty else { + return Err(build_err("internal: v0 lowering emits only map statics")); + }; + let table = static_tables + .get(&spec.table) + .expect("spec names come from the catalog"); + data.push(materialize_map(py, table, spec, keys, values)?); + } + Backend::Interp( + compile(&prepared.program, data).map_err(|e| build_err(e.to_string()))?, + ) + } + }, + }; let output_model = match output_model { // Supplied models are trusted as-is in v0 (no shape validation). Some(m) => m, @@ -310,6 +368,15 @@ impl DuckDBInferFn { }) } + /// Which engine executes: "cranelift", "interpreter", or "constant". + #[getter] + fn backend(&self) -> &'static str { + match &self.engine { + Engine::Compiled { fun, .. } => fun.name(), + Engine::Constant { .. } => "constant", + } + } + #[pyo3(signature = (tables=None, **kwargs))] fn infer( &self, diff --git a/src/specializer/exec/cranelift.rs b/src/specializer/exec/cranelift.rs new file mode 100644 index 0000000..a7f7622 --- /dev/null +++ b/src/specializer/exec/cranelift.rs @@ -0,0 +1,1403 @@ +//! Cranelift-jit backend: the same verified IR, compiled to native code. +//! +//! Coverage-first (TASK-44 stretch plan): trivially-safe ops map inline to +//! CLIF (const scalars, float arith, int compares, logic, select, itof, +//! fabs); everything with nontrivial semantics — checked int arith, every +//! string op, loads/stores, statics — calls an `extern "C"` helper that +//! delegates to the SAME functions the interpreter uses (casemap, +//! substr_window, duck_fcmp, DuckF64, the arena). The two backends cannot +//! drift where they share code; the random-IR differential in tests.rs +//! guards the rest. Inlining hot helpers is a later, measured optimization. +//! +//! ABI: one JIT'd function per program, called once per row: +//! `extern "C" fn(*mut Cx) -> i64` returning 0 = emit, 1 = skip, 2 = a +//! helper trapped (message in `Cx::trap`), 3+k = the program's k-th +//! `Term::Trap`. `Cx` is `#[repr(C)]` and the generated code reads exactly +//! one field directly: `trap_flag` at offset 0, checked after every +//! fallible helper call. Str values live in the arena and travel through +//! the JIT'd code as two i64s (offset, length). +//! +//! A `CraneliftFn` owns a compiled `InterpFn` too: it provides the +//! input/state checks, `new_state`, the prepared statics the helpers read, +//! and an always-available fallback. + +use std::collections::HashMap; + +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::{ + types, AbiParam, InstBuilder, MemFlags, StackSlotData, StackSlotKind, Value as CVal, +}; +use cranelift_codegen::settings::{self, Configurable}; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext}; +use cranelift_jit::{JITBuilder, JITModule}; +use cranelift_module::{FuncId, Linkage, Module}; + +use super::super::ir::{ + BinOp, CmpPred, Inst, Lit, NumOp1, Program, RoundMode, StaticTy, StrOp1, Term, TrimSide, Ty, +}; +use super::interp::{ + self, apply_ord, col_valid, substr_range_ok, substr_window, CompileError, DuckF64, InterpFn, + PreparedStatic, +}; +use super::{casemap, Arena, Batch, ColData, KeyBits, OutCol, RunState, ScalarVal, StrRef, Trap}; + +// ------------------------------------------------------- runtime context -- + +/// Everything a helper can touch during one row. `#[repr(C)]` because the +/// JIT'd code loads `trap_flag` (guaranteed offset 0) directly; every other +/// field is only ever dereferenced from Rust inside helpers. +#[repr(C)] +struct Cx { + trap_flag: u8, + row: usize, + arena: *mut Arena, + out: *mut OutCol, + out_len: usize, + input: *const Batch, + statics: *const PreparedStatic, + statics_len: usize, + trap: Option, +} + +impl Cx { + fn arena(&mut self) -> &mut Arena { + unsafe { &mut *self.arena } + } + fn out(&mut self) -> &mut [OutCol] { + unsafe { std::slice::from_raw_parts_mut(self.out, self.out_len) } + } + fn input(&self) -> &Batch { + unsafe { &*self.input } + } + fn statics(&self) -> &[PreparedStatic] { + unsafe { std::slice::from_raw_parts(self.statics, self.statics_len) } + } + fn get(&mut self, off: i64, len: i64) -> String { + // Owned copy: several helpers read one span and push another; a + // borrow across arena mutation is not expressible here. + self.arena() + .get(StrRef { + off: off as usize, + len: len as usize, + }) + .to_string() + } + fn set_trap(&mut self, msg: String) { + self.trap_flag = 1; + self.trap = Some(Trap(msg)); + } +} + +/// Per-probe-site descriptor owned by the `CraneliftFn`; the JIT'd code +/// passes its absolute address to `h_probe`. +struct ProbeDesc { + static_id: usize, + key_tys: Vec, + val_tys: Vec, +} + +/// One 16-byte scratch cell: scalar payload in `[0]` (f64 as bits, bool as +/// 0/1), strings as (offset, length). +type Cell = [u64; 2]; + +// --------------------------------------------------------------- helpers -- +// Each mirrors the interpreter closure for the same instruction — most by +// calling the identical shared function. + +unsafe fn cx<'a>(p: *mut Cx) -> &'a mut Cx { + unsafe { &mut *p } +} + +extern "C" fn h_load_i1(p: *mut Cx, col: i64) -> u8 { + let c = unsafe { cx(p) }; + match &c.input().cols[col as usize] { + ColData::I1 { data, .. } => data[c.row] as u8, + _ => unreachable!("load type checked by the verifier"), + } +} + +extern "C" fn h_load_i64(p: *mut Cx, col: i64) -> i64 { + let c = unsafe { cx(p) }; + match &c.input().cols[col as usize] { + ColData::I64 { data, .. } => data[c.row], + _ => unreachable!("load type checked by the verifier"), + } +} + +extern "C" fn h_load_f64(p: *mut Cx, col: i64) -> f64 { + let c = unsafe { cx(p) }; + match &c.input().cols[col as usize] { + ColData::F64 { data, .. } => data[c.row], + _ => unreachable!("load type checked by the verifier"), + } +} + +extern "C" fn h_load_str(p: *mut Cx, col: i64, len_out: *mut i64) -> i64 { + let c = unsafe { cx(p) }; + let s = match &c.input().cols[col as usize] { + ColData::Str { data, .. } => data[c.row].clone(), + _ => unreachable!("load type checked by the verifier"), + }; + let r = c.arena().push_str(&s); + unsafe { *len_out = r.len as i64 }; + r.off as i64 +} + +extern "C" fn h_load_valid(p: *mut Cx, col: i64) -> u8 { + let c = unsafe { cx(p) }; + col_valid(&c.input().cols[col as usize], c.row) as u8 +} + +extern "C" fn h_const_str(p: *mut Cx, ptr: i64, len: i64) -> i64 { + let c = unsafe { cx(p) }; + let s = unsafe { + std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr as *const u8, len as usize)) + }; + c.arena().push_str(s).off as i64 +} + +macro_rules! checked_bin { + ($name:ident, $method:ident, $msg:literal) => { + extern "C" fn $name(p: *mut Cx, a: i64, b: i64) -> i64 { + match a.$method(b) { + Some(v) => v, + None => { + unsafe { cx(p) }.set_trap(format!($msg)); + 0 + } + } + } + }; +} +checked_bin!(h_iadd, checked_add, "i64 overflow in iadd"); +checked_bin!(h_isub, checked_sub, "i64 overflow in isub"); +checked_bin!(h_imul, checked_mul, "i64 overflow in imul"); + +macro_rules! checked_div { + ($name:ident, $method:ident, $opname:literal) => { + extern "C" fn $name(p: *mut Cx, a: i64, b: i64) -> i64 { + if b == 0 { + unsafe { cx(p) }.set_trap(format!("division by zero in {}", $opname)); + return 0; + } + match a.$method(b) { + Some(v) => v, + None => { + unsafe { cx(p) }.set_trap(format!("i64 overflow in {}", $opname)); + 0 + } + } + } + }; +} +checked_div!(h_idiv, checked_div, "idiv"); +checked_div!(h_irem, checked_rem, "irem"); + +extern "C" fn h_frem(a: f64, b: f64) -> f64 { + a % b +} + +extern "C" fn h_fround(a: f64) -> f64 { + a.round() +} + +extern "C" fn h_iabs(p: *mut Cx, a: i64) -> i64 { + match a.checked_abs() { + Some(v) => v, + None => { + unsafe { cx(p) }.set_trap("i64 overflow in iabs".to_string()); + 0 + } + } +} + +extern "C" fn h_fcmp(a: f64, b: f64, pred: i64) -> u8 { + apply_ord(decode_pred(pred), super::duck_fcmp(a, b)) as u8 +} + +extern "C" fn h_scmp(p: *mut Cx, ao: i64, al: i64, bo: i64, bl: i64, pred: i64) -> u8 { + let c = unsafe { cx(p) }; + let (a, b) = (c.get(ao, al), c.get(bo, bl)); + apply_ord(decode_pred(pred), a.as_str().cmp(b.as_str())) as u8 +} + +fn decode_pred(p: i64) -> CmpPred { + match p { + 0 => CmpPred::Eq, + 1 => CmpPred::Ne, + 2 => CmpPred::Lt, + 3 => CmpPred::Le, + 4 => CmpPred::Gt, + _ => CmpPred::Ge, + } +} + +fn encode_pred(p: CmpPred) -> i64 { + match p { + CmpPred::Eq => 0, + CmpPred::Ne => 1, + CmpPred::Lt => 2, + CmpPred::Le => 3, + CmpPred::Gt => 4, + CmpPred::Ge => 5, + } +} + +extern "C" fn h_ftoi(p: *mut Cx, x: f64, round: i64) -> i64 { + let r = if round != 0 { x.round() } else { x.trunc() }; + // Mirrors interp: 2^63 is exactly representable; [-2^63, 2^63) fits. + if r.is_finite() && r >= -(2f64.powi(63)) && r < 2f64.powi(63) { + r as i64 + } else { + unsafe { cx(p) }.set_trap(format!("f64 value {x:?} out of i64 range in ftoi")); + 0 + } +} + +extern "C" fn h_itos(p: *mut Cx, v: i64, len_out: *mut i64) -> i64 { + let c = unsafe { cx(p) }; + let r = c.arena().push_str(&format!("{v}")); + unsafe { *len_out = r.len as i64 }; + r.off as i64 +} + +extern "C" fn h_ftos(p: *mut Cx, v: f64, len_out: *mut i64) -> i64 { + let c = unsafe { cx(p) }; + let r = c.arena().push_str(&format!("{}", DuckF64(v))); + unsafe { *len_out = r.len as i64 }; + r.off as i64 +} + +extern "C" fn h_stoi(p: *mut Cx, off: i64, len: i64, valid_out: *mut u8) -> i64 { + let c = unsafe { cx(p) }; + let s = c.get(off, len); + match s.trim_ascii().parse::() { + Ok(v) => { + unsafe { *valid_out = 1 }; + v + } + Err(_) => { + unsafe { *valid_out = 0 }; + 0 + } + } +} + +extern "C" fn h_stof(p: *mut Cx, off: i64, len: i64, valid_out: *mut u8) -> f64 { + let c = unsafe { cx(p) }; + let s = c.get(off, len); + match s.trim_ascii().parse::() { + Ok(v) => { + unsafe { *valid_out = 1 }; + v + } + Err(_) => { + unsafe { *valid_out = 0 }; + 0.0 + } + } +} + +extern "C" fn h_sconcat(p: *mut Cx, ao: i64, al: i64, bo: i64, bl: i64, len_out: *mut i64) -> i64 { + let c = unsafe { cx(p) }; + let r = c.arena().concat( + StrRef { + off: ao as usize, + len: al as usize, + }, + StrRef { + off: bo as usize, + len: bl as usize, + }, + ); + unsafe { *len_out = r.len as i64 }; + r.off as i64 +} + +extern "C" fn h_scase(p: *mut Cx, off: i64, len: i64, upper: i64, len_out: *mut i64) -> i64 { + let c = unsafe { cx(p) }; + let s = c.get(off, len); + let out: String = if upper != 0 { + s.chars().map(casemap::simple_upper).collect() + } else { + s.chars().map(casemap::simple_lower).collect() + }; + let r = c.arena().push_str(&out); + unsafe { *len_out = r.len as i64 }; + r.off as i64 +} + +extern "C" fn h_strim( + p: *mut Cx, + side: i64, + ao: i64, + al: i64, + co: i64, + cl: i64, + len_out: *mut i64, +) -> i64 { + let c = unsafe { cx(p) }; + let (s, set_s) = (c.get(ao, al), c.get(co, cl)); + let set: Vec = set_s.chars().collect(); + let hit = |ch: char| set.contains(&ch); + let t = match side { + 0 => s.trim_matches(hit), + 1 => s.trim_start_matches(hit), + _ => s.trim_end_matches(hit), + } + .to_string(); + let r = c.arena().push_str(&t); + unsafe { *len_out = r.len as i64 }; + r.off as i64 +} + +extern "C" fn h_ssubstr( + p: *mut Cx, + ao: i64, + al: i64, + start: i64, + len: i64, + has_len: i64, + len_out: *mut i64, +) -> i64 { + let c = unsafe { cx(p) }; + if !substr_range_ok(start) { + c.set_trap("substring offset outside of supported range".to_string()); + unsafe { *len_out = 0 }; + return 0; + } + if has_len != 0 && !substr_range_ok(len) { + c.set_trap("substring length outside of supported range".to_string()); + unsafe { *len_out = 0 }; + return 0; + } + let s = c.get(ao, al); + let out = substr_window(&s, start, (has_len != 0).then_some(len)); + let r = c.arena().push_str(&out); + unsafe { *len_out = r.len as i64 }; + r.off as i64 +} + +macro_rules! store_h { + ($name:ident, $variant:ident, $ty:ty, $conv:expr) => { + extern "C" fn $name(p: *mut Cx, col: i64, valid: u8, val: $ty) { + let c = unsafe { cx(p) }; + match &mut c.out()[col as usize] { + OutCol::$variant(v) => v.push((valid != 0, $conv(val))), + _ => unreachable!("store type checked by the verifier"), + } + } + }; +} +store_h!(h_store_i1, I1, u8, |v: u8| v != 0); +store_h!(h_store_i64, I64, i64, |v| v); +store_h!(h_store_f64, F64, f64, |v| v); + +extern "C" fn h_store_str(p: *mut Cx, col: i64, valid: u8, off: i64, len: i64) { + let c = unsafe { cx(p) }; + let r = StrRef { + off: off as usize, + len: len as usize, + }; + match &mut c.out()[col as usize] { + OutCol::Str(v) => v.push((valid != 0, r)), + _ => unreachable!("store type checked by the verifier"), + } +} + +/// Scalar static read; covers `sload` (flag ignored) and `sload.opt`. +extern "C" fn h_sload(p: *mut Cx, sid: i64, valid_out: *mut u8, cell_out: *mut Cell) { + let c = unsafe { cx(p) }; + let PreparedStatic::Scalar { valid, val } = &c.statics()[sid as usize] else { + unreachable!("static kind checked at compile"); + }; + let (valid, val) = (*valid, val.clone()); + // Mirrors interp: an invalid scalar's payload is the type default, the + // stored value never leaks through a false flag. + let val = if valid { + val + } else { + match val.ty() { + Ty::I1 => ScalarVal::I1(false), + Ty::I64 => ScalarVal::I64(0), + Ty::F64 => ScalarVal::F64(0.0), + Ty::Str => ScalarVal::Str(String::new()), + } + }; + let cell = match val { + ScalarVal::I1(b) => [b as u64, 0], + ScalarVal::I64(i) => [i as u64, 0], + ScalarVal::F64(f) => [f.to_bits(), 0], + ScalarVal::Str(s) => { + let r = c.arena().push_str(&s); + [r.off as u64, r.len as u64] + } + }; + unsafe { + *valid_out = valid as u8; + *cell_out = cell; + } +} + +extern "C" fn h_probe( + p: *mut Cx, + desc: *const ProbeDesc, + keys: *const Cell, + outs: *mut Cell, +) -> u8 { + let c = unsafe { cx(p) }; + let desc = unsafe { &*desc }; + let keys = unsafe { std::slice::from_raw_parts(keys, desc.key_tys.len()) }; + let PreparedStatic::Map { entries } = &c.statics()[desc.static_id] else { + unreachable!("static kind checked at compile"); + }; + + // Mirrors interp::cmp_key: canonical f64 bits, arena strings by byte + // order. The arena is only read here, never written, so the borrow is + // fine to hold across the search. + let arena = unsafe { &*c.arena }; + let cmp = |stored: &[KeyBits]| -> std::cmp::Ordering { + for (kb, cell) in stored.iter().zip(keys.iter()) { + let ord = match kb { + KeyBits::I1(s) => s.cmp(&(cell[0] != 0)), + KeyBits::I64(s) => s.cmp(&(cell[0] as i64)), + KeyBits::F64(s) => s.cmp(&super::canon_f64_bits(f64::from_bits(cell[0]))), + KeyBits::Str(s) => { + let v = arena.get(StrRef { + off: cell[0] as usize, + len: cell[1] as usize, + }); + s.as_str().cmp(v) + } + }; + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + std::cmp::Ordering::Equal + }; + + let found = entries.binary_search_by(|(k, _)| cmp(k)).ok(); + let hit = found.is_some(); + let values: Vec = match found { + Some(idx) => entries[idx].1.clone(), + None => desc + .val_tys + .iter() + .map(|ty| match ty { + Ty::I1 => ScalarVal::I1(false), + Ty::I64 => ScalarVal::I64(0), + Ty::F64 => ScalarVal::F64(0.0), + Ty::Str => ScalarVal::Str(String::new()), + }) + .collect(), + }; + for (i, v) in values.into_iter().enumerate() { + let cell = match v { + ScalarVal::I1(b) => [b as u64, 0], + ScalarVal::I64(x) => [x as u64, 0], + ScalarVal::F64(f) => [f.to_bits(), 0], + ScalarVal::Str(s) => { + let r = c.arena().push_str(&s); + [r.off as u64, r.len as u64] + } + }; + unsafe { *outs.add(i) = cell }; + } + hit as u8 +} + +// ------------------------------------------------------------ the JIT'd fn -- + +type RowFn = extern "C" fn(*mut Cx) -> i64; + +pub struct CraneliftFn { + /// Keeps the executable memory alive; also the fallback + checker. + _module: JITModule, + row_fn: RowFn, + interp: InterpFn, + trap_msgs: Vec, + /// Owned backing for absolute addresses baked into the code. + _const_strs: Vec>, + _probe_descs: Vec>, +} + +/// A JIT'd value: scalars are one CLIF value, strings are (offset, length). +#[derive(Clone, Copy)] +enum V { + S(CVal), + Str(CVal, CVal), +} + +impl V { + fn s(self) -> CVal { + match self { + V::S(v) => v, + V::Str(..) => unreachable!("scalar use of a str value"), + } + } + fn str2(self) -> (CVal, CVal) { + match self { + V::Str(o, l) => (o, l), + V::S(_) => unreachable!("str use of a scalar value"), + } + } +} + +fn clif_ty(ty: Ty) -> types::Type { + match ty { + Ty::I1 => types::I8, + Ty::I64 => types::I64, + Ty::F64 => types::F64, + Ty::Str => unreachable!("str values are two i64s, expanded at use sites"), + } +} + +pub fn compile(p: &Program, statics: Vec) -> Result { + // The interpreter compile also runs verify + prepare_statics; its + // prepared statics are the ones the helpers read. + let interp = interp::compile(p, statics)?; + + let mut flags = settings::builder(); + flags.set("use_colocated_libcalls", "false").unwrap(); + flags.set("is_pic", "false").unwrap(); + flags.set("opt_level", "speed").unwrap(); + let isa = cranelift_codegen::isa::lookup(target_lexicon::Triple::host()) + .map_err(|e| CompileError::Static(format!("cranelift: no host ISA: {e}")))? + .finish(settings::Flags::new(flags)) + .map_err(|e| CompileError::Static(format!("cranelift: ISA: {e}")))?; + + let mut jb = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names()); + for (name, ptr) in HELPERS { + jb.symbol(*name, *ptr); + } + let mut module = JITModule::new(jb); + let ptr_ty = module.target_config().pointer_type(); + + // Declare helper signatures once. + let mut helper_ids: HashMap<&'static str, FuncId> = HashMap::new(); + for (name, _) in HELPERS { + let mut sig = module.make_signature(); + helper_sig(name, &mut sig, ptr_ty); + let id = module + .declare_function(name, Linkage::Import, &sig) + .map_err(|e| CompileError::Static(format!("cranelift declare {name}: {e}")))?; + helper_ids.insert(name, id); + } + + let mut ctx = module.make_context(); + ctx.func.signature.params.push(AbiParam::new(ptr_ty)); + ctx.func.signature.returns.push(AbiParam::new(types::I64)); + + let mut const_strs: Vec> = Vec::new(); + let mut probe_descs: Vec> = Vec::new(); + let mut trap_msgs: Vec = Vec::new(); + + { + let mut fbc = FunctionBuilderContext::new(); + let mut b = FunctionBuilder::new(&mut ctx.func, &mut fbc); + + // Scratch stack slots: one 16-byte out-cell for helper out-params, + // plus room for the widest probe's keys and values. + let max_keys = p + .statics + .iter() + .map(|s| match s { + StaticTy::Map { keys, .. } => keys.len(), + _ => 1, + }) + .max() + .unwrap_or(1) + .max(1); + let max_vals = p + .statics + .iter() + .map(|s| match s { + StaticTy::Map { values, .. } => values.len(), + _ => 1, + }) + .max() + .unwrap_or(1) + .max(1); + let slot_out = + b.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 16, 3)); + let slot_keys = b.create_sized_stack_slot(StackSlotData::new( + StackSlotKind::ExplicitSlot, + 16 * max_keys as u32, + 3, + )); + let slot_vals = b.create_sized_stack_slot(StackSlotData::new( + StackSlotKind::ExplicitSlot, + 16 * max_vals as u32, + 3, + )); + + let entry = b.create_block(); + b.append_block_param(entry, ptr_ty); + + // One CLIF block per IR block, with params per the IR's block params + // (strings expand to two). + let blocks: Vec<_> = p.blocks.iter().map(|_| b.create_block()).collect(); + let mut vals: HashMap = HashMap::new(); + for (ib, cb) in p.blocks.iter().zip(blocks.iter()) { + for (v, ty) in &ib.params { + let pv = match ty { + Ty::Str => { + let o = b.append_block_param(*cb, types::I64); + let l = b.append_block_param(*cb, types::I64); + V::Str(o, l) + } + t => V::S(b.append_block_param(*cb, clif_ty(*t))), + }; + vals.insert(v.0, pv); + } + } + let trap_exit = b.create_block(); + + b.switch_to_block(entry); + let cxp = b.block_params(entry)[0]; + b.ins().jump(blocks[0], &[]); + + b.switch_to_block(trap_exit); + let two = b.ins().iconst(types::I64, 2); + b.ins().return_(&[two]); + + let call_h = |b: &mut FunctionBuilder, + module: &mut JITModule, + name: &str, + args: &[CVal]| + -> Option { + let id = helper_ids[name]; + let fref = module.declare_func_in_func(id, b.func); + let call = b.ins().call(fref, args); + b.inst_results(call).first().copied() + }; + + for (bi, ib) in p.blocks.iter().enumerate() { + b.switch_to_block(blocks[bi]); + for inst in &ib.insts { + translate_inst( + &mut b, + &mut module, + &call_h, + inst, + p, + cxp, + &mut vals, + slot_out, + slot_keys, + slot_vals, + &mut const_strs, + &mut probe_descs, + trap_exit, + ); + } + // Terminator. + let arg_list = |vals: &HashMap, + args: &[super::super::ir::Value]| + -> Vec { + let mut out = Vec::new(); + for a in args { + match vals[&a.0] { + V::S(v) => out.push(v.into()), + V::Str(o, l) => { + out.push(o.into()); + out.push(l.into()); + } + } + } + out + }; + match &ib.term { + Term::Jump { to, args } => { + let a = arg_list(&vals, args); + b.ins().jump(blocks[to.0 as usize], &a); + } + Term::Brif { + cond, + then_to, + then_args, + else_to, + else_args, + } => { + let c = vals[&cond.0].s(); + let ta = arg_list(&vals, then_args); + let ea = arg_list(&vals, else_args); + b.ins().brif( + c, + blocks[then_to.0 as usize], + &ta, + blocks[else_to.0 as usize], + &ea, + ); + } + Term::Emit => { + let z = b.ins().iconst(types::I64, 0); + b.ins().return_(&[z]); + } + Term::Skip => { + let o = b.ins().iconst(types::I64, 1); + b.ins().return_(&[o]); + } + Term::Trap { msg } => { + let code = 3 + trap_msgs.len() as i64; + trap_msgs.push(msg.clone()); + let v = b.ins().iconst(types::I64, code); + b.ins().return_(&[v]); + } + } + } + + b.seal_all_blocks(); + b.finalize(); + } + + let fid = module + .declare_function("row", Linkage::Export, &ctx.func.signature) + .map_err(|e| CompileError::Static(format!("cranelift declare row: {e}")))?; + module + .define_function(fid, &mut ctx) + .map_err(|e| CompileError::Static(format!("cranelift define: {e}")))?; + module.clear_context(&mut ctx); + module + .finalize_definitions() + .map_err(|e| CompileError::Static(format!("cranelift finalize: {e}")))?; + let code = module.get_finalized_function(fid); + let row_fn: RowFn = unsafe { std::mem::transmute(code) }; + + Ok(CraneliftFn { + _module: module, + row_fn, + interp, + trap_msgs, + _const_strs: const_strs, + _probe_descs: probe_descs, + }) +} + +impl CraneliftFn { + pub fn new_state(&self) -> RunState { + self.interp.new_state() + } + + /// Same contract as [`InterpFn::run`]. + pub fn run(&self, input: &Batch, st: &mut RunState) -> Result<(), Trap> { + self.interp.check_input(input)?; + self.interp.check_state(st)?; + st.arena.clear(); + st.emitted = 0; + for col in st.out.iter_mut() { + col.clear(); + } + interp::reserve_out(&mut st.out, input.rows); + + let statics = self.interp.statics(); + let mut emitted = 0usize; + for row in 0..input.rows { + let mut cx = Cx { + trap_flag: 0, + row, + arena: &mut st.arena, + out: st.out.as_mut_ptr(), + out_len: st.out.len(), + input, + statics: statics.as_ptr(), + statics_len: statics.len(), + trap: None, + }; + match (self.row_fn)(&mut cx) { + 0 => emitted += 1, + 1 => {} + 2 => return Err(cx.trap.take().expect("helper trap sets the message")), + k => return Err(Trap(self.trap_msgs[(k - 3) as usize].clone())), + } + } + st.emitted = emitted; + Ok(()) + } +} + +// ------------------------------------------------------- inst translation -- + +#[allow(clippy::too_many_arguments)] +fn translate_inst( + b: &mut FunctionBuilder, + module: &mut JITModule, + call_h: &dyn Fn(&mut FunctionBuilder, &mut JITModule, &str, &[CVal]) -> Option, + inst: &Inst, + p: &Program, + cxp: CVal, + vals: &mut HashMap, + slot_out: cranelift_codegen::ir::StackSlot, + slot_keys: cranelift_codegen::ir::StackSlot, + slot_vals: cranelift_codegen::ir::StackSlot, + const_strs: &mut Vec>, + probe_descs: &mut Vec>, + trap_exit: cranelift_codegen::ir::Block, +) { + // After any fallible helper: check trap_flag (offset 0 of Cx) and bail. + let trap_check = |b: &mut FunctionBuilder| { + let flag = b.ins().load(types::I8, MemFlags::trusted(), cxp, 0); + let cont = b.create_block(); + b.ins().brif(flag, trap_exit, &[], cont, &[]); + b.switch_to_block(cont); + }; + let icon = |b: &mut FunctionBuilder, v: i64| b.ins().iconst(types::I64, v); + + match inst { + Inst::Const { dst, lit } => { + let v = match lit { + Lit::I1(x) => V::S(b.ins().iconst(types::I8, *x as i64)), + Lit::I64(x) => V::S(b.ins().iconst(types::I64, *x)), + Lit::F64(x) => V::S(b.ins().f64const(*x)), + Lit::Str(s) => { + let boxed: Box = s.clone().into_boxed_str(); + let ptr = boxed.as_ptr() as i64; + let len = boxed.len() as i64; + const_strs.push(boxed); + let pv = icon(b, ptr); + let lv = icon(b, len); + let off = call_h(b, module, "h_const_str", &[cxp, pv, lv]).unwrap(); + V::Str(off, lv) + } + }; + vals.insert(dst.0, v); + } + Inst::Bin { op, dst, a, b: rhs } => { + let (x, y) = (vals[&a.0].s(), vals[&rhs.0].s()); + let v = match op { + BinOp::Iadd => call_h(b, module, "h_iadd", &[cxp, x, y]).unwrap(), + BinOp::Isub => call_h(b, module, "h_isub", &[cxp, x, y]).unwrap(), + BinOp::Imul => call_h(b, module, "h_imul", &[cxp, x, y]).unwrap(), + BinOp::Idiv => call_h(b, module, "h_idiv", &[cxp, x, y]).unwrap(), + BinOp::Irem => call_h(b, module, "h_irem", &[cxp, x, y]).unwrap(), + BinOp::Fadd => b.ins().fadd(x, y), + BinOp::Fsub => b.ins().fsub(x, y), + BinOp::Fmul => b.ins().fmul(x, y), + BinOp::Fdiv => b.ins().fdiv(x, y), + BinOp::Frem => call_h(b, module, "h_frem", &[x, y]).unwrap(), + BinOp::And => b.ins().band(x, y), + BinOp::Or => b.ins().bor(x, y), + BinOp::Xor => b.ins().bxor(x, y), + }; + if matches!( + op, + BinOp::Iadd | BinOp::Isub | BinOp::Imul | BinOp::Idiv | BinOp::Irem + ) { + trap_check(b); + } + vals.insert(dst.0, V::S(v)); + } + Inst::Cmp { + pred, + ty, + dst, + a, + b: rhs, + } => { + let v = match ty { + Ty::I64 => { + let cc = match pred { + CmpPred::Eq => IntCC::Equal, + CmpPred::Ne => IntCC::NotEqual, + CmpPred::Lt => IntCC::SignedLessThan, + CmpPred::Le => IntCC::SignedLessThanOrEqual, + CmpPred::Gt => IntCC::SignedGreaterThan, + CmpPred::Ge => IntCC::SignedGreaterThanOrEqual, + }; + let (x, y) = (vals[&a.0].s(), vals[&rhs.0].s()); + b.ins().icmp(cc, x, y) + } + Ty::F64 => { + let (x, y) = (vals[&a.0].s(), vals[&rhs.0].s()); + let pv = icon(b, encode_pred(*pred)); + call_h(b, module, "h_fcmp", &[x, y, pv]).unwrap() + } + Ty::Str => { + let (ao, al) = vals[&a.0].str2(); + let (bo, bl) = vals[&rhs.0].str2(); + let pv = icon(b, encode_pred(*pred)); + call_h(b, module, "h_scmp", &[cxp, ao, al, bo, bl, pv]).unwrap() + } + Ty::I1 => unreachable!("cmp on i1 is rejected by the verifier"), + }; + vals.insert(dst.0, V::S(v)); + } + Inst::Not { dst, a } => { + let v = b.ins().bxor_imm(vals[&a.0].s(), 1); + vals.insert(dst.0, V::S(v)); + } + Inst::Select { + dst, + cond, + a, + b: rhs, + } => { + let c = vals[&cond.0].s(); + let v = match (vals[&a.0], vals[&rhs.0]) { + (V::S(x), V::S(y)) => V::S(b.ins().select(c, x, y)), + (V::Str(xo, xl), V::Str(yo, yl)) => { + let o = b.ins().select(c, xo, yo); + let l = b.ins().select(c, xl, yl); + V::Str(o, l) + } + _ => unreachable!("select arms share a type"), + }; + vals.insert(dst.0, v); + } + Inst::Itof { dst, a } => { + let v = b.ins().fcvt_from_sint(types::F64, vals[&a.0].s()); + vals.insert(dst.0, V::S(v)); + } + Inst::Ftoi { mode, dst, a } => { + let r = icon(b, matches!(mode, RoundMode::Round) as i64); + let v = call_h(b, module, "h_ftoi", &[cxp, vals[&a.0].s(), r]).unwrap(); + trap_check(b); + vals.insert(dst.0, V::S(v)); + } + Inst::Itos { dst, a } => { + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_itos", &[cxp, vals[&a.0].s(), lp]).unwrap(); + let len = b.ins().stack_load(types::I64, slot_out, 0); + vals.insert(dst.0, V::Str(off, len)); + } + Inst::Ftos { dst, a } => { + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_ftos", &[cxp, vals[&a.0].s(), lp]).unwrap(); + let len = b.ins().stack_load(types::I64, slot_out, 0); + vals.insert(dst.0, V::Str(off, len)); + } + Inst::StoiOpt { flag, dst, a } => { + let (o, l) = vals[&a.0].str2(); + let vp = b.ins().stack_addr(types::I64, slot_out, 0); + let v = call_h(b, module, "h_stoi", &[cxp, o, l, vp]).unwrap(); + let f = b.ins().stack_load(types::I8, slot_out, 0); + vals.insert(flag.0, V::S(f)); + vals.insert(dst.0, V::S(v)); + } + Inst::StofOpt { flag, dst, a } => { + let (o, l) = vals[&a.0].str2(); + let vp = b.ins().stack_addr(types::I64, slot_out, 0); + let v = call_h(b, module, "h_stof", &[cxp, o, l, vp]).unwrap(); + let f = b.ins().stack_load(types::I8, slot_out, 0); + vals.insert(flag.0, V::S(f)); + vals.insert(dst.0, V::S(v)); + } + Inst::Sconcat { dst, a, b: rhs } => { + let (ao, al) = vals[&a.0].str2(); + let (bo, bl) = vals[&rhs.0].str2(); + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_sconcat", &[cxp, ao, al, bo, bl, lp]).unwrap(); + let len = b.ins().stack_load(types::I64, slot_out, 0); + vals.insert(dst.0, V::Str(off, len)); + } + Inst::Str1 { op, dst, a } => { + let (o, l) = vals[&a.0].str2(); + let up = icon(b, matches!(op, StrOp1::Upper) as i64); + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_scase", &[cxp, o, l, up, lp]).unwrap(); + let len = b.ins().stack_load(types::I64, slot_out, 0); + vals.insert(dst.0, V::Str(off, len)); + } + Inst::Strim { + side, + dst, + a, + chars, + } => { + let (ao, al) = vals[&a.0].str2(); + let (co, cl) = vals[&chars.0].str2(); + let sv = icon( + b, + match side { + TrimSide::Both => 0, + TrimSide::Lead => 1, + TrimSide::Trail => 2, + }, + ); + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_strim", &[cxp, sv, ao, al, co, cl, lp]).unwrap(); + let len = b.ins().stack_load(types::I64, slot_out, 0); + vals.insert(dst.0, V::Str(off, len)); + } + Inst::Ssubstr { dst, a, start, len } => { + let (ao, al) = vals[&a.0].str2(); + let sv = vals[&start.0].s(); + let (lv, has) = match len { + Some(l) => (vals[&l.0].s(), icon(b, 1)), + None => (icon(b, 0), icon(b, 0)), + }; + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_ssubstr", &[cxp, ao, al, sv, lv, has, lp]).unwrap(); + trap_check(b); + let lnew = b.ins().stack_load(types::I64, slot_out, 0); + vals.insert(dst.0, V::Str(off, lnew)); + } + Inst::Num1 { op, dst, a } => { + let x = vals[&a.0].s(); + let v = match op { + NumOp1::Iabs => { + let v = call_h(b, module, "h_iabs", &[cxp, x]).unwrap(); + trap_check(b); + v + } + NumOp1::Fabs => b.ins().fabs(x), + NumOp1::Fround => call_h(b, module, "h_fround", &[x]).unwrap(), + }; + vals.insert(dst.0, V::S(v)); + } + Inst::Load { dst, col } => { + let ty = p.in_cols[*col as usize].ty.ty; + let cv = icon(b, *col as i64); + let v = match ty { + Ty::I1 => V::S(call_h(b, module, "h_load_i1", &[cxp, cv]).unwrap()), + Ty::I64 => V::S(call_h(b, module, "h_load_i64", &[cxp, cv]).unwrap()), + Ty::F64 => V::S(call_h(b, module, "h_load_f64", &[cxp, cv]).unwrap()), + Ty::Str => { + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_load_str", &[cxp, cv, lp]).unwrap(); + let len = b.ins().stack_load(types::I64, slot_out, 0); + V::Str(off, len) + } + }; + vals.insert(dst.0, v); + } + Inst::LoadOpt { flag, dst, col } => { + // Mirrors interp: on a false flag the payload is NORMALIZED to + // the type default (the input batch may carry garbage there). + let ty = p.in_cols[*col as usize].ty.ty; + let cv = icon(b, *col as i64); + let f = call_h(b, module, "h_load_valid", &[cxp, cv]).unwrap(); + vals.insert(flag.0, V::S(f)); + let v = match ty { + Ty::I1 => { + let raw = call_h(b, module, "h_load_i1", &[cxp, cv]).unwrap(); + let zero = b.ins().iconst(types::I8, 0); + V::S(b.ins().select(f, raw, zero)) + } + Ty::I64 => { + let raw = call_h(b, module, "h_load_i64", &[cxp, cv]).unwrap(); + let zero = icon(b, 0); + V::S(b.ins().select(f, raw, zero)) + } + Ty::F64 => { + let raw = call_h(b, module, "h_load_f64", &[cxp, cv]).unwrap(); + let zero = b.ins().f64const(0.0); + V::S(b.ins().select(f, raw, zero)) + } + Ty::Str => { + let lp = b.ins().stack_addr(types::I64, slot_out, 0); + let off = call_h(b, module, "h_load_str", &[cxp, cv, lp]).unwrap(); + let len = b.ins().stack_load(types::I64, slot_out, 0); + let zero = icon(b, 0); + let o = b.ins().select(f, off, zero); + let l = b.ins().select(f, len, zero); + V::Str(o, l) + } + }; + vals.insert(dst.0, v); + } + Inst::Store { col, val } => { + let one = b.ins().iconst(types::I8, 1); + store(b, module, call_h, p, cxp, vals, *col, one, *val); + } + Inst::StoreOpt { col, flag, val } => { + let f = vals[&flag.0].s(); + store(b, module, call_h, p, cxp, vals, *col, f, *val); + } + Inst::Probe { + static_id, + hit, + dsts, + keys, + } => { + let StaticTy::Map { + keys: key_tys, + values: val_tys, + } = &p.statics[*static_id as usize] + else { + unreachable!("probe on non-map is rejected by the verifier"); + }; + // Write the key cells. + for (i, k) in keys.iter().enumerate() { + let base = (16 * i) as i32; + match vals[&k.0] { + V::S(v) => { + let ty = key_tys[i]; + let as64 = match ty { + Ty::I1 => b.ins().uextend(types::I64, v), + Ty::I64 => v, + Ty::F64 => b.ins().bitcast(types::I64, MemFlags::new(), v), + Ty::Str => unreachable!(), + }; + b.ins().stack_store(as64, slot_keys, base); + } + V::Str(o, l) => { + b.ins().stack_store(o, slot_keys, base); + b.ins().stack_store(l, slot_keys, base + 8); + } + } + } + let desc = Box::new(ProbeDesc { + static_id: *static_id as usize, + key_tys: key_tys.clone(), + val_tys: val_tys.clone(), + }); + let desc_ptr = &*desc as *const ProbeDesc as i64; + probe_descs.push(desc); + let dp = icon(b, desc_ptr); + let kp = b.ins().stack_addr(types::I64, slot_keys, 0); + let vp = b.ins().stack_addr(types::I64, slot_vals, 0); + let h = call_h(b, module, "h_probe", &[cxp, dp, kp, vp]).unwrap(); + vals.insert(hit.0, V::S(h)); + for (i, d) in dsts.iter().enumerate() { + let base = (16 * i) as i32; + let v = match val_tys[i] { + Ty::I1 => { + let x = b.ins().stack_load(types::I64, slot_vals, base); + V::S(b.ins().ireduce(types::I8, x)) + } + Ty::I64 => V::S(b.ins().stack_load(types::I64, slot_vals, base)), + Ty::F64 => { + let x = b.ins().stack_load(types::I64, slot_vals, base); + V::S(b.ins().bitcast(types::F64, MemFlags::new(), x)) + } + Ty::Str => { + let o = b.ins().stack_load(types::I64, slot_vals, base); + let l = b.ins().stack_load(types::I64, slot_vals, base + 8); + V::Str(o, l) + } + }; + vals.insert(d.0, v); + } + } + Inst::Sload { static_id, dst } => { + sload( + b, module, call_h, p, cxp, vals, *static_id, None, *dst, slot_out, slot_vals, + ); + } + Inst::SloadOpt { + static_id, + flag, + dst, + } => { + sload( + b, + module, + call_h, + p, + cxp, + vals, + *static_id, + Some(*flag), + *dst, + slot_out, + slot_vals, + ); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn store( + b: &mut FunctionBuilder, + module: &mut JITModule, + call_h: &dyn Fn(&mut FunctionBuilder, &mut JITModule, &str, &[CVal]) -> Option, + p: &Program, + cxp: CVal, + vals: &HashMap, + col: u32, + valid: CVal, + val: super::super::ir::Value, +) { + let ty = p.out_cols[col as usize].ty.ty; + let cv = b.ins().iconst(types::I64, col as i64); + match ty { + Ty::I1 => { + let v = vals[&val.0].s(); + call_h(b, module, "h_store_i1", &[cxp, cv, valid, v]); + } + Ty::I64 => { + let v = vals[&val.0].s(); + call_h(b, module, "h_store_i64", &[cxp, cv, valid, v]); + } + Ty::F64 => { + let v = vals[&val.0].s(); + call_h(b, module, "h_store_f64", &[cxp, cv, valid, v]); + } + Ty::Str => { + let (o, l) = vals[&val.0].str2(); + call_h(b, module, "h_store_str", &[cxp, cv, valid, o, l]); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn sload( + b: &mut FunctionBuilder, + module: &mut JITModule, + call_h: &dyn Fn(&mut FunctionBuilder, &mut JITModule, &str, &[CVal]) -> Option, + p: &Program, + cxp: CVal, + vals: &mut HashMap, + static_id: u32, + flag: Option, + dst: super::super::ir::Value, + slot_out: cranelift_codegen::ir::StackSlot, + slot_vals: cranelift_codegen::ir::StackSlot, +) { + let StaticTy::Scalar(ct) = &p.statics[static_id as usize] else { + unreachable!("sload on non-scalar is rejected by the verifier"); + }; + let sv = b.ins().iconst(types::I64, static_id as i64); + let fp = b.ins().stack_addr(types::I64, slot_out, 0); + let cp = b.ins().stack_addr(types::I64, slot_vals, 0); + call_h(b, module, "h_sload", &[cxp, sv, fp, cp]); + if let Some(f) = flag { + let fv = b.ins().stack_load(types::I8, slot_out, 0); + vals.insert(f.0, V::S(fv)); + } + let v = match ct.ty { + Ty::I1 => { + let x = b.ins().stack_load(types::I64, slot_vals, 0); + V::S(b.ins().ireduce(types::I8, x)) + } + Ty::I64 => V::S(b.ins().stack_load(types::I64, slot_vals, 0)), + Ty::F64 => { + let x = b.ins().stack_load(types::I64, slot_vals, 0); + V::S(b.ins().bitcast(types::F64, MemFlags::new(), x)) + } + Ty::Str => { + let o = b.ins().stack_load(types::I64, slot_vals, 0); + let l = b.ins().stack_load(types::I64, slot_vals, 8); + V::Str(o, l) + } + }; + vals.insert(dst.0, v); +} + +// -------------------------------------------------- helper symbol table -- + +const HELPERS: &[(&str, *const u8)] = &[ + ("h_load_i1", h_load_i1 as *const u8), + ("h_load_i64", h_load_i64 as *const u8), + ("h_load_f64", h_load_f64 as *const u8), + ("h_load_str", h_load_str as *const u8), + ("h_load_valid", h_load_valid as *const u8), + ("h_const_str", h_const_str as *const u8), + ("h_iadd", h_iadd as *const u8), + ("h_isub", h_isub as *const u8), + ("h_imul", h_imul as *const u8), + ("h_idiv", h_idiv as *const u8), + ("h_irem", h_irem as *const u8), + ("h_frem", h_frem as *const u8), + ("h_fround", h_fround as *const u8), + ("h_iabs", h_iabs as *const u8), + ("h_fcmp", h_fcmp as *const u8), + ("h_scmp", h_scmp as *const u8), + ("h_ftoi", h_ftoi as *const u8), + ("h_itos", h_itos as *const u8), + ("h_ftos", h_ftos as *const u8), + ("h_stoi", h_stoi as *const u8), + ("h_stof", h_stof as *const u8), + ("h_sconcat", h_sconcat as *const u8), + ("h_scase", h_scase as *const u8), + ("h_strim", h_strim as *const u8), + ("h_ssubstr", h_ssubstr as *const u8), + ("h_store_i1", h_store_i1 as *const u8), + ("h_store_i64", h_store_i64 as *const u8), + ("h_store_f64", h_store_f64 as *const u8), + ("h_store_str", h_store_str as *const u8), + ("h_sload", h_sload as *const u8), + ("h_probe", h_probe as *const u8), +]; + +fn helper_sig(name: &str, sig: &mut cranelift_codegen::ir::Signature, ptr: types::Type) { + use types::{F64, I64, I8}; + let (params, ret): (&[types::Type], Option) = match name { + "h_load_i1" | "h_load_valid" => (&[ptr, I64], Some(I8)), + "h_load_i64" => (&[ptr, I64], Some(I64)), + "h_load_f64" => (&[ptr, I64], Some(F64)), + "h_load_str" => (&[ptr, I64, I64], Some(I64)), + "h_const_str" => (&[ptr, I64, I64], Some(I64)), + "h_iadd" | "h_isub" | "h_imul" | "h_idiv" | "h_irem" => (&[ptr, I64, I64], Some(I64)), + "h_frem" => (&[F64, F64], Some(F64)), + "h_fround" => (&[F64], Some(F64)), + "h_iabs" => (&[ptr, I64], Some(I64)), + "h_fcmp" => (&[F64, F64, I64], Some(I8)), + "h_scmp" => (&[ptr, I64, I64, I64, I64, I64], Some(I8)), + "h_ftoi" => (&[ptr, F64, I64], Some(I64)), + "h_itos" => (&[ptr, I64, I64], Some(I64)), + "h_ftos" => (&[ptr, F64, I64], Some(I64)), + "h_stoi" => (&[ptr, I64, I64, I64], Some(I64)), + "h_stof" => (&[ptr, I64, I64, I64], Some(F64)), + "h_sconcat" => (&[ptr, I64, I64, I64, I64, I64], Some(I64)), + "h_scase" => (&[ptr, I64, I64, I64, I64], Some(I64)), + "h_strim" => (&[ptr, I64, I64, I64, I64, I64, I64], Some(I64)), + "h_ssubstr" => (&[ptr, I64, I64, I64, I64, I64, I64], Some(I64)), + "h_store_i1" => (&[ptr, I64, I8, I8], None), + "h_store_i64" => (&[ptr, I64, I8, I64], None), + "h_store_f64" => (&[ptr, I64, I8, F64], None), + "h_store_str" => (&[ptr, I64, I8, I64, I64], None), + "h_sload" => (&[ptr, I64, I64, I64], None), + "h_probe" => (&[ptr, I64, I64, I64], Some(I8)), + _ => unreachable!("unknown helper {name}"), + }; + for p in params { + sig.params.push(AbiParam::new(*p)); + } + if let Some(r) = ret { + sig.returns.push(AbiParam::new(r)); + } +} + +#[cfg(test)] +mod tests { + use cranelift_codegen::ir::{types, AbiParam, InstBuilder}; + use cranelift_codegen::settings::{self, Configurable}; + use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext}; + use cranelift_jit::{JITBuilder, JITModule}; + use cranelift_module::{Linkage, Module}; + + #[test] + fn jit_smoke_add_two_i64() { + let mut flags = settings::builder(); + flags.set("use_colocated_libcalls", "false").unwrap(); + flags.set("is_pic", "false").unwrap(); + let isa = cranelift_codegen::isa::lookup(target_lexicon::Triple::host()) + .unwrap() + .finish(settings::Flags::new(flags)) + .unwrap(); + let mut module = JITModule::new(JITBuilder::with_isa( + isa, + cranelift_module::default_libcall_names(), + )); + + let mut ctx = module.make_context(); + ctx.func.signature.params.push(AbiParam::new(types::I64)); + ctx.func.signature.params.push(AbiParam::new(types::I64)); + ctx.func.signature.returns.push(AbiParam::new(types::I64)); + + let mut fb_ctx = FunctionBuilderContext::new(); + let mut b = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); + let entry = b.create_block(); + b.append_block_params_for_function_params(entry); + b.switch_to_block(entry); + b.seal_block(entry); + let (x, y) = (b.block_params(entry)[0], b.block_params(entry)[1]); + let sum = b.ins().iadd(x, y); + b.ins().return_(&[sum]); + b.finalize(); + + let id = module + .declare_function("add", Linkage::Export, &ctx.func.signature) + .unwrap(); + module.define_function(id, &mut ctx).unwrap(); + module.clear_context(&mut ctx); + module.finalize_definitions().unwrap(); + + let code = module.get_finalized_function(id); + let f: extern "C" fn(i64, i64) -> i64 = unsafe { std::mem::transmute(code) }; + assert_eq!(f(40, 2), 42); + } +} diff --git a/src/specializer/exec/interp.rs b/src/specializer/exec/interp.rs index 16e09b2..23571a0 100644 --- a/src/specializer/exec/interp.rs +++ b/src/specializer/exec/interp.rs @@ -72,7 +72,7 @@ struct Ctx<'a> { row: usize, } -enum PreparedStatic { +pub(super) enum PreparedStatic { Scalar { valid: bool, val: ScalarVal, @@ -240,9 +240,14 @@ impl InterpFn { Ok(()) } + /// The prepared statics, shared with the Cranelift backend's helpers. + pub(super) fn statics(&self) -> &[PreparedStatic] { + &self.statics + } + /// A `RunState` is only valid for the `InterpFn` that created it — /// reject a foreign one with a trap instead of an index panic. - fn check_state(&self, st: &RunState) -> Result<(), Trap> { + pub(super) fn check_state(&self, st: &RunState) -> Result<(), Trap> { if st.regs.len() < self.nregs { return Err(Trap(format!( "RunState has {} register(s), this function needs {} — states are not \ @@ -276,7 +281,7 @@ impl InterpFn { Ok(()) } - fn check_input(&self, input: &Batch) -> Result<(), Trap> { + pub(super) fn check_input(&self, input: &Batch) -> Result<(), Trap> { if input.cols.len() != self.in_decl.len() { return Err(Trap(format!( "input has {} column(s), the program declares {}", @@ -321,7 +326,7 @@ fn do_moves(regs: &mut [RegVal], moves: &[(u32, u32)]) { } } -fn reserve_out(out: &mut [OutCol], rows: usize) { +pub(super) fn reserve_out(out: &mut [OutCol], rows: usize) { for col in out { match col { OutCol::I1(v) => v.reserve(rows), @@ -332,7 +337,7 @@ fn reserve_out(out: &mut [OutCol], rows: usize) { } } -fn prepare_statics( +pub(super) fn prepare_statics( p: &Program, statics: Vec, ) -> Result, CompileError> { @@ -518,7 +523,7 @@ impl std::fmt::Write for ArenaWriter<'_> { /// a non-negative length runs forward `[rs, rs+len)`, a NEGATIVE length /// slices BACKWARDS `[rs+len, rs)`; `len: None` is the 2-arg rest-of-string /// form. -fn substr_window(s: &str, start: i64, len: Option) -> String { +pub(super) fn substr_window(s: &str, start: i64, len: Option) -> String { let n = s.chars().count() as i64; let rs = if start < 0 { (n + start + 1).max(1) @@ -542,14 +547,14 @@ fn substr_window(s: &str, start: i64, len: Option) -> String { /// The offset/length guard DuckDB applies before the window: values outside /// [-2^32, 2^32-1] raise an Out of Range error (measured boundary-exactly). -fn substr_range_ok(v: i64) -> bool { +pub(super) fn substr_range_ok(v: i64) -> bool { (-(1i64 << 32)..(1i64 << 32)).contains(&v) } /// DuckDB's DOUBLE -> VARCHAR text (measured 1.5.5): Rust's shortest /// round-trip form, except the exponent carries an explicit sign and at /// least two digits (`1e+300`, `1e-05`) and NaN is lowercase `nan`. -struct DuckF64(f64); +pub(super) struct DuckF64(pub(super) f64); impl std::fmt::Display for DuckF64 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -1022,7 +1027,7 @@ fn cmp_key(stored: &[KeyBits], key_regs: &[usize], ctx: &Ctx<'_>) -> std::cmp::O Ordering::Equal } -fn apply_ord(pred: CmpPred, ord: std::cmp::Ordering) -> bool { +pub(super) fn apply_ord(pred: CmpPred, ord: std::cmp::Ordering) -> bool { use std::cmp::Ordering::*; match pred { CmpPred::Eq => ord == Equal, @@ -1034,7 +1039,7 @@ fn apply_ord(pred: CmpPred, ord: std::cmp::Ordering) -> bool { } } -fn valid_len(c: &ColData) -> usize { +pub(super) fn valid_len(c: &ColData) -> usize { match c { ColData::I1 { valid, .. } | ColData::I64 { valid, .. } @@ -1046,7 +1051,7 @@ fn valid_len(c: &ColData) -> usize { /// Only called for nullable columns, whose validity length check_input has /// already enforced — the unwrap_or is unreachable there and don't-care for /// non-nullable columns (which never reach this). -fn col_valid(c: &ColData, row: usize) -> bool { +pub(super) fn col_valid(c: &ColData, row: usize) -> bool { match c { ColData::I1 { valid, .. } | ColData::I64 { valid, .. } diff --git a/src/specializer/exec/mod.rs b/src/specializer/exec/mod.rs index 822f438..86f6e90 100644 --- a/src/specializer/exec/mod.rs +++ b/src/specializer/exec/mod.rs @@ -11,6 +11,7 @@ //! exist. pub mod casemap; +pub mod cranelift; pub mod interp; #[cfg(test)] diff --git a/src/specializer/exec/tests.rs b/src/specializer/exec/tests.rs index 93d0478..04353eb 100644 --- a/src/specializer/exec/tests.rs +++ b/src/specializer/exec/tests.rs @@ -8,7 +8,7 @@ use std::cell::Cell; use super::super::ir::{fixtures, gen, parse::parse, verify::verify, Program, StaticTy, Ty}; use super::interp::{compile, CompileError}; -use super::testutil::{batch, built, c_f64, c_i1, c_i64, c_str, rows, run_snapshot}; +use super::testutil::{batch, built, c_f64, c_i1, c_i64, c_str, rows, run_snapshot, snapshot}; use super::{Batch, ColData, KeyBits, OutCol, ScalarVal, StaticData, Trap}; // ------------------------------------------------- counting allocator -- @@ -818,3 +818,92 @@ fn casemap_tables_sorted_and_marquee_pins() { assert_eq!(simple_lower('É'), 'é'); assert_eq!(simple_upper('fi'), 'fi'); } + +/// THE backend contract (TASK-44): cranelift and the interpreter agree +/// byte-for-byte on every generated program — outputs, emitted counts, and +/// traps. Same seeds as the determinism fuzz, both backends fed identical +/// statics and inputs. +#[test] +fn fuzz_cranelift_agrees_with_interpreter() { + use super::cranelift; + // More seeds than the determinism fuzz: this is the backend contract. + for seed in 0..500u64 { + let p = gen::gen_program(seed); + let mut rng = gen::Rng::new(seed ^ 0x9E37_79B9_7F4A_7C15); + let statics_i = gen_statics(&mut rng, &p); + let mut rng2 = gen::Rng::new(seed ^ 0x9E37_79B9_7F4A_7C15); + let statics_c = gen_statics(&mut rng2, &p); + let input = gen_input(&mut rng, &p); + + let fi = compile(&p, statics_i).expect("interp compile"); + let fc = match cranelift::compile(&p, statics_c) { + Ok(f) => f, + Err(e) => panic!("seed {seed}: cranelift failed to compile: {e}"), + }; + let a = run_snapshot(&fi, &input); + let mut st = fc.new_state(); + let b = fc.run(&input, &mut st).map(|_| snapshot(&st)); + match (a, b) { + (Ok(x), Ok(y)) => assert_eq!(x, y, "seed {seed}: outputs diverge"), + (Err(x), Err(y)) => assert_eq!(x, y, "seed {seed}: traps diverge"), + (x, y) => panic!("seed {seed}: outcome diverged: interp {x:?} vs cranelift {y:?}"), + } + } +} + +/// Raw compute, no Python boundary: `cargo test --release backend_compute_ +/// bench -- --ignored --nocapture`. Informational, not a gate assertion. +#[test] +#[ignore = "bench, run explicitly"] +fn backend_compute_bench() { + use super::cranelift; + use std::time::Instant; + let p = parse( + r#"fn f(in: batch{a: i64, b: f64}, out: batch{x: i64, h: f64, k: i1}) { +entry: + %a = load in.a + %b = load in.b + %two = const.i64 2 + %one = const.i64 1 + %m = imul %a, %two + %x = iadd %m, %one + %hf = const.f64 0.5 + %h = fmul %b, %hf + %z = const.f64 10.0 + %k = fcmp.gt %h, %z + store out.x, %x + store out.h, %h + store out.k, %k + emit +}"#, + ) + .unwrap(); + let n = 100_000usize; + let a: Vec> = (0..n).map(|i| Some(i as i64 % 1000)).collect(); + let b: Vec> = (0..n).map(|i| Some(i as f64 / 7.0)).collect(); + let input = batch(n, vec![c_i64(&a), c_f64(&b)]); + + let fi = compile(&p, vec![]).unwrap(); + let fc = cranelift::compile(&p, vec![]).unwrap(); + let mut sti = fi.new_state(); + let mut stc = fc.new_state(); + for (name, run) in [ + ( + "interp", + &mut (|| fi.run(&input, &mut sti).unwrap()) as &mut dyn FnMut(), + ), + ("cranelift", &mut (|| fc.run(&input, &mut stc).unwrap())), + ] { + run(); // warm + let mut best = u128::MAX; + for _ in 0..20 { + let t = Instant::now(); + run(); + best = best.min(t.elapsed().as_nanos()); + } + println!( + "{name:>10}: {:>6.2} ns/row (best of 20, n={n})", + best as f64 / n as f64 + ); + } +} diff --git a/tests/test_duckdb_interpreter.py b/tests/test_duckdb_interpreter.py index 941e80d..443be3e 100644 --- a/tests/test_duckdb_interpreter.py +++ b/tests/test_duckdb_interpreter.py @@ -427,3 +427,25 @@ def test_unknown_driving_table_stays_clean_unsupported(): row_tables={"__THIS__": _row_model({"a": "int"})}, static_tables={}, ) + + +# --------------------------------------------------------- M-cranelift: +# the codegen backend runs the same suite; these pin the backend choice. + + +def test_v0_queries_run_on_cranelift(): + fn = DuckDBInferFn( + "SELECT k + 1 AS x, upper(s) AS u FROM __THIS__ JOIN dim ON k = dim.id", + row_tables={"__THIS__": _row_model({"k": "int", "s": "str"})}, + static_tables={"dim": DIM}, + ) + assert fn.backend == "cranelift" + + +def test_static_only_backend_is_constant(): + fn = DuckDBInferFn( + "SELECT sum(v) AS s FROM dim", + row_tables={"__THIS__": _row_model({"a": "int"})}, + static_tables={"dim": static({"v": "int"}, [{"v": 1}, {"v": 2}])}, + ) + assert fn.backend == "constant"