├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── examples └── run.rs ├── src ├── error.rs ├── lib.rs ├── library.rs └── rpn.rs └── test.clif /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "allocator-api2" 16 | version = "0.2.21" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys 0.59.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell", 67 | "windows-sys 0.59.0", 68 | ] 69 | 70 | [[package]] 71 | name = "anyhow" 72 | version = "1.0.95" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 75 | 76 | [[package]] 77 | name = "arbitrary" 78 | version = "1.4.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" 81 | 82 | [[package]] 83 | name = "bitflags" 84 | version = "1.3.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 87 | 88 | [[package]] 89 | name = "bumpalo" 90 | version = "3.16.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 93 | dependencies = [ 94 | "allocator-api2", 95 | ] 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "1.0.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 102 | 103 | [[package]] 104 | name = "colorchoice" 105 | version = "1.0.3" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 108 | 109 | [[package]] 110 | name = "cranelift" 111 | version = "0.116.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "a71de5e59f616d79d14d2c71aa2799ce898241d7f10f7e64a4997014b4000a28" 114 | dependencies = [ 115 | "cranelift-codegen", 116 | "cranelift-frontend", 117 | "cranelift-jit", 118 | "cranelift-module", 119 | ] 120 | 121 | [[package]] 122 | name = "cranelift-bforest" 123 | version = "0.116.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4" 126 | dependencies = [ 127 | "cranelift-entity", 128 | ] 129 | 130 | [[package]] 131 | name = "cranelift-bitset" 132 | version = "0.116.1" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34" 135 | 136 | [[package]] 137 | name = "cranelift-codegen" 138 | version = "0.116.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e" 141 | dependencies = [ 142 | "bumpalo", 143 | "cranelift-bforest", 144 | "cranelift-bitset", 145 | "cranelift-codegen-meta", 146 | "cranelift-codegen-shared", 147 | "cranelift-control", 148 | "cranelift-entity", 149 | "cranelift-isle", 150 | "gimli", 151 | "hashbrown 0.14.5", 152 | "log", 153 | "regalloc2", 154 | "rustc-hash", 155 | "serde", 156 | "smallvec", 157 | "target-lexicon", 158 | ] 159 | 160 | [[package]] 161 | name = "cranelift-codegen-meta" 162 | version = "0.116.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8" 165 | dependencies = [ 166 | "cranelift-codegen-shared", 167 | ] 168 | 169 | [[package]] 170 | name = "cranelift-codegen-shared" 171 | version = "0.116.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb" 174 | 175 | [[package]] 176 | name = "cranelift-control" 177 | version = "0.116.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef" 180 | dependencies = [ 181 | "arbitrary", 182 | ] 183 | 184 | [[package]] 185 | name = "cranelift-entity" 186 | version = "0.116.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323" 189 | dependencies = [ 190 | "cranelift-bitset", 191 | ] 192 | 193 | [[package]] 194 | name = "cranelift-frontend" 195 | version = "0.116.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57" 198 | dependencies = [ 199 | "cranelift-codegen", 200 | "log", 201 | "smallvec", 202 | "target-lexicon", 203 | ] 204 | 205 | [[package]] 206 | name = "cranelift-isle" 207 | version = "0.116.1" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d" 210 | 211 | [[package]] 212 | name = "cranelift-jit" 213 | version = "0.116.1" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "5e65c42755a719b09662b00c700daaf76cc35d5ace1f5c002ad404b591ff1978" 216 | dependencies = [ 217 | "anyhow", 218 | "cranelift-codegen", 219 | "cranelift-control", 220 | "cranelift-entity", 221 | "cranelift-module", 222 | "cranelift-native", 223 | "libc", 224 | "log", 225 | "region", 226 | "target-lexicon", 227 | "wasmtime-jit-icache-coherence", 228 | "windows-sys 0.59.0", 229 | ] 230 | 231 | [[package]] 232 | name = "cranelift-module" 233 | version = "0.116.1" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "4d55612bebcf16ff7306c8a6f5bdb6d45662b8aa1ee058ecce8807ad87db719b" 236 | dependencies = [ 237 | "anyhow", 238 | "cranelift-codegen", 239 | "cranelift-control", 240 | ] 241 | 242 | [[package]] 243 | name = "cranelift-native" 244 | version = "0.116.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7" 247 | dependencies = [ 248 | "cranelift-codegen", 249 | "libc", 250 | "target-lexicon", 251 | ] 252 | 253 | [[package]] 254 | name = "env_filter" 255 | version = "0.1.3" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 258 | dependencies = [ 259 | "log", 260 | "regex", 261 | ] 262 | 263 | [[package]] 264 | name = "env_logger" 265 | version = "0.11.6" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 268 | dependencies = [ 269 | "anstream", 270 | "anstyle", 271 | "env_filter", 272 | "humantime", 273 | "log", 274 | ] 275 | 276 | [[package]] 277 | name = "equivalent" 278 | version = "1.0.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 281 | 282 | [[package]] 283 | name = "fallible-iterator" 284 | version = "0.3.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 287 | 288 | [[package]] 289 | name = "fnv" 290 | version = "1.0.7" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 293 | 294 | [[package]] 295 | name = "gimli" 296 | version = "0.31.1" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 299 | dependencies = [ 300 | "fallible-iterator", 301 | "indexmap", 302 | "stable_deref_trait", 303 | ] 304 | 305 | [[package]] 306 | name = "hashbrown" 307 | version = "0.14.5" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 310 | 311 | [[package]] 312 | name = "hashbrown" 313 | version = "0.15.2" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 316 | 317 | [[package]] 318 | name = "humantime" 319 | version = "2.1.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 322 | 323 | [[package]] 324 | name = "indexmap" 325 | version = "2.7.1" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 328 | dependencies = [ 329 | "equivalent", 330 | "hashbrown 0.15.2", 331 | ] 332 | 333 | [[package]] 334 | name = "is_terminal_polyfill" 335 | version = "1.70.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 338 | 339 | [[package]] 340 | name = "libc" 341 | version = "0.2.169" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 344 | 345 | [[package]] 346 | name = "log" 347 | version = "0.4.25" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 350 | 351 | [[package]] 352 | name = "mach2" 353 | version = "0.4.2" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 356 | dependencies = [ 357 | "libc", 358 | ] 359 | 360 | [[package]] 361 | name = "math-jit" 362 | version = "0.2.0" 363 | dependencies = [ 364 | "cranelift", 365 | "cranelift-codegen", 366 | "cranelift-native", 367 | "env_logger", 368 | "log", 369 | "meval", 370 | "thiserror", 371 | ] 372 | 373 | [[package]] 374 | name = "memchr" 375 | version = "2.7.4" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 378 | 379 | [[package]] 380 | name = "meval" 381 | version = "0.2.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" 384 | dependencies = [ 385 | "fnv", 386 | "nom", 387 | ] 388 | 389 | [[package]] 390 | name = "nom" 391 | version = "1.2.4" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" 394 | 395 | [[package]] 396 | name = "once_cell" 397 | version = "1.20.2" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 400 | 401 | [[package]] 402 | name = "proc-macro2" 403 | version = "1.0.93" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 406 | dependencies = [ 407 | "unicode-ident", 408 | ] 409 | 410 | [[package]] 411 | name = "quote" 412 | version = "1.0.38" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 415 | dependencies = [ 416 | "proc-macro2", 417 | ] 418 | 419 | [[package]] 420 | name = "regalloc2" 421 | version = "0.11.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "145c1c267e14f20fb0f88aa76a1c5ffec42d592c1d28b3cd9148ae35916158d3" 424 | dependencies = [ 425 | "allocator-api2", 426 | "bumpalo", 427 | "hashbrown 0.15.2", 428 | "log", 429 | "rustc-hash", 430 | "smallvec", 431 | ] 432 | 433 | [[package]] 434 | name = "regex" 435 | version = "1.11.1" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 438 | dependencies = [ 439 | "aho-corasick", 440 | "memchr", 441 | "regex-automata", 442 | "regex-syntax", 443 | ] 444 | 445 | [[package]] 446 | name = "regex-automata" 447 | version = "0.4.9" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 450 | dependencies = [ 451 | "aho-corasick", 452 | "memchr", 453 | "regex-syntax", 454 | ] 455 | 456 | [[package]] 457 | name = "regex-syntax" 458 | version = "0.8.5" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 461 | 462 | [[package]] 463 | name = "region" 464 | version = "3.0.2" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" 467 | dependencies = [ 468 | "bitflags", 469 | "libc", 470 | "mach2", 471 | "windows-sys 0.52.0", 472 | ] 473 | 474 | [[package]] 475 | name = "rustc-hash" 476 | version = "2.1.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" 479 | 480 | [[package]] 481 | name = "serde" 482 | version = "1.0.217" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 485 | dependencies = [ 486 | "serde_derive", 487 | ] 488 | 489 | [[package]] 490 | name = "serde_derive" 491 | version = "1.0.217" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "syn", 498 | ] 499 | 500 | [[package]] 501 | name = "smallvec" 502 | version = "1.13.2" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 505 | 506 | [[package]] 507 | name = "stable_deref_trait" 508 | version = "1.2.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 511 | 512 | [[package]] 513 | name = "syn" 514 | version = "2.0.96" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 517 | dependencies = [ 518 | "proc-macro2", 519 | "quote", 520 | "unicode-ident", 521 | ] 522 | 523 | [[package]] 524 | name = "target-lexicon" 525 | version = "0.13.1" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "dc12939a1c9b9d391e0b7135f72fd30508b73450753e28341fed159317582a77" 528 | 529 | [[package]] 530 | name = "thiserror" 531 | version = "2.0.11" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 534 | dependencies = [ 535 | "thiserror-impl", 536 | ] 537 | 538 | [[package]] 539 | name = "thiserror-impl" 540 | version = "2.0.11" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 543 | dependencies = [ 544 | "proc-macro2", 545 | "quote", 546 | "syn", 547 | ] 548 | 549 | [[package]] 550 | name = "unicode-ident" 551 | version = "1.0.14" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 554 | 555 | [[package]] 556 | name = "utf8parse" 557 | version = "0.2.2" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 560 | 561 | [[package]] 562 | name = "wasmtime-jit-icache-coherence" 563 | version = "29.0.1" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1" 566 | dependencies = [ 567 | "anyhow", 568 | "cfg-if", 569 | "libc", 570 | "windows-sys 0.59.0", 571 | ] 572 | 573 | [[package]] 574 | name = "windows-sys" 575 | version = "0.52.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 578 | dependencies = [ 579 | "windows-targets", 580 | ] 581 | 582 | [[package]] 583 | name = "windows-sys" 584 | version = "0.59.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 587 | dependencies = [ 588 | "windows-targets", 589 | ] 590 | 591 | [[package]] 592 | name = "windows-targets" 593 | version = "0.52.6" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 596 | dependencies = [ 597 | "windows_aarch64_gnullvm", 598 | "windows_aarch64_msvc", 599 | "windows_i686_gnu", 600 | "windows_i686_gnullvm", 601 | "windows_i686_msvc", 602 | "windows_x86_64_gnu", 603 | "windows_x86_64_gnullvm", 604 | "windows_x86_64_msvc", 605 | ] 606 | 607 | [[package]] 608 | name = "windows_aarch64_gnullvm" 609 | version = "0.52.6" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 612 | 613 | [[package]] 614 | name = "windows_aarch64_msvc" 615 | version = "0.52.6" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 618 | 619 | [[package]] 620 | name = "windows_i686_gnu" 621 | version = "0.52.6" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 624 | 625 | [[package]] 626 | name = "windows_i686_gnullvm" 627 | version = "0.52.6" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 630 | 631 | [[package]] 632 | name = "windows_i686_msvc" 633 | version = "0.52.6" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 636 | 637 | [[package]] 638 | name = "windows_x86_64_gnu" 639 | version = "0.52.6" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 642 | 643 | [[package]] 644 | name = "windows_x86_64_gnullvm" 645 | version = "0.52.6" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 648 | 649 | [[package]] 650 | name = "windows_x86_64_msvc" 651 | version = "0.52.6" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 654 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "math-jit" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT" 6 | repository = "https://github.com/kamirr/math-jit" 7 | description = "Compile arithmetic expressions to native code" 8 | 9 | [[example]] 10 | name = "run" 11 | 12 | [dependencies] 13 | cranelift = { version = "0.116.1", features = ["jit", "module"] } 14 | cranelift-codegen = "0.116.1" 15 | cranelift-native = "0.116.1" 16 | log = "0.4.25" 17 | meval = "0.2.0" 18 | thiserror = "2.0.11" 19 | 20 | [dev-dependencies] 21 | env_logger = "0.11.6" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Math-JIT 2 | [![Crates.io][crates-badge]][crates-url] 3 | [![MIT licensed][mit-badge]][mit-url] 4 | ![CI][ci-url] 5 | 6 | [crates-badge]: https://img.shields.io/crates/v/math-jit.svg 7 | [crates-url]: https://crates.io/crates/math-jit 8 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 9 | [mit-url]: https://github.com/kamirr/math-jit/blob/main/LICENSE 10 | [ci-url]: https://github.com/kamirr/math-jit/actions/workflows/rust.yml/badge.svg 11 | 12 | Math-JIT is a limited-scope implementation of a JIT compiler using 13 | [cranelift](https://cranelift.dev/). It compiles arithmetic expressions for the 14 | host architecture and exposes a function pointer to the result. 15 | 16 | ## Functionality 17 | The expression parsing is implemented in [meval](https://docs.rs/meval/latest/meval). 18 | The common arithmetic operations supported by the compiler are: 19 | - Binary: 20 | - Addition 21 | - Subtraction 22 | - Multiplication 23 | - Division 24 | - Powers 25 | - Unary: 26 | - Negation 27 | - Functions: 28 | - Sine, cosine, tangent 29 | - Absolute value 30 | - Square root 31 | 32 | The expressions can utilize 8 variables, values of which are supplied by the 33 | caller: `x`, `y`, `a`, `b`, `c`, `d`, `_1` and `_2`. `_1` and `_2` are 34 | in-out variables -- they can be overriden by calling special functions `_1(..)` 35 | and `_2(..)`, which set the values of the two signals to that of their arguments. 36 | All reads of the signals observe the value before any writes, and the ordering 37 | of writes is unspecified. 38 | 39 | ## Extendability 40 | New 1+ argument functions can be added. Operators and syntax cannot be extended, 41 | and 0-argument functions don't work due to a bug in the parser. 42 | 43 | ## What for 44 | Walking the AST or evaluating RPN manually is slow. I'm working on a real-time 45 | [modular software synthesiser](https://en.wikipedia.org/wiki/Modular_synthesizer), 46 | [Modal](https://github.com/kamirr/modal), which requires 44100 executions per 47 | second for multiple modules. Interpreters are sufficient in such cases, but 48 | greatly lower the margin of safety. 49 | 50 | Other than that, I found the toy language example in cranelift to be quite 51 | unapproachable for a complete beginner like myself. This project may prove more 52 | useful as introductory material for the codegen-related part of compilers. 53 | 54 | ## Optimizations 55 | After translating expressions to RPN, simple constant folding is performed and 56 | variable liveliness is minimized. Functions from the library will be called 57 | during constant folding under the assumption that they're pure. 58 | 59 | ## Code please 60 | ```rust 61 | use math_jit::{Program, Compiler, Library}; 62 | 63 | let program = Program::parse_from_infix("x * 2 + _1(y)").unwrap(); 64 | let mut compiler = Compiler::new(&Library::default()).unwrap(); 65 | 66 | let func = compiler.compile(&program).unwrap(); 67 | let mut sig1 = 0.0; 68 | let mut sig2 = 0.0; 69 | let result = func(3.0, 1.0, 0.0, 0.0, 0.0, 0.0, &mut sig1, &mut sig2); 70 | 71 | assert_eq!(result, 7.0); 72 | assert_eq!(sig1, 1.0); 73 | assert_eq!(sig2, 0.0); 74 | ``` -------------------------------------------------------------------------------- /examples/run.rs: -------------------------------------------------------------------------------- 1 | use env_logger::Env; 2 | use math_jit::{library::Library, rpn::Program, Compiler}; 3 | use std::{hint, io::Write, time::Instant}; 4 | 5 | fn test( 6 | func: fn(f32, f32, f32, f32, f32, f32, &mut f32, &mut f32) -> f32, 7 | x: f32, 8 | y: f32, 9 | a: f32, 10 | b: f32, 11 | c: f32, 12 | d: f32, 13 | mut sig1: f32, 14 | mut sig2: f32, 15 | ) { 16 | let res = func(x, y, a, b, c, d, &mut sig1, &mut sig2); 17 | 18 | println!("f(..) = {res:.4}, sig1 = {sig1:.4}, sig2 = {sig2:.4}"); 19 | } 20 | 21 | fn bench(func: fn(f32, f32, f32, f32, f32, f32, &mut f32, &mut f32) -> f32) { 22 | let start = Instant::now(); 23 | let mut sig1 = 0.5f32; 24 | let mut sig2 = 0.0f32; 25 | for _ in 0..44100 { 26 | hint::black_box(func(1.0, 2.0, 0.0, 0.0, 0.0, 0.0, &mut sig1, &mut sig2)); 27 | } 28 | let elapsed = start.elapsed().as_secs_f32(); 29 | 30 | println!("perf coefficient: {}", 1.0 / elapsed); 31 | } 32 | 33 | fn main() { 34 | env_logger::Builder::from_env(Env::default().default_filter_or("debug,cranelift_codegen=info")) 35 | .init(); 36 | 37 | let mut expr = String::new(); 38 | let library = Library::default(); 39 | 40 | loop { 41 | expr.clear(); 42 | 43 | print!("expression: "); 44 | std::io::stdout().flush().unwrap(); 45 | std::io::stdin().read_line(&mut expr).unwrap(); 46 | 47 | if expr.is_empty() { 48 | break; 49 | } 50 | 51 | let mut program = Program::parse_from_infix(expr.as_str()).unwrap(); 52 | 53 | log::debug!("parsed: {program:?}"); 54 | program.optimize(&library); 55 | log::debug!("optimized: {program:?}"); 56 | 57 | let mut compiler = Compiler::new(&library).unwrap(); 58 | let func = compiler.compile(&program).unwrap(); 59 | 60 | test(func, 1.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); 61 | bench(func); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use cranelift::module::ModuleError; 2 | use cranelift_codegen::{settings::SetError, CodegenError}; 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum JitError { 7 | #[error("expression cannot be parsed")] 8 | ParseError(#[from] meval::ParseError), 9 | #[error("RPN cannot be constructed")] 10 | RpnConstruction(#[from] meval::RPNError), 11 | #[error("unknown RPN token `{0}`")] 12 | ParseUnknownToken(String), 13 | #[error("unknown variable name `{0}`")] 14 | ParseUnknownVariable(String), 15 | #[error("unknown binary operation `{0}`")] 16 | ParseUnknownBinop(String), 17 | #[error("unknown unary operation `{0}`")] 18 | ParseUnknownUnop(String), 19 | #[error("unknown function call `{0}`")] 20 | ParseUnknownFunc(String), 21 | #[error("function not present in library `{0}`")] 22 | CompileUknownFunc(String), 23 | #[error("function `{0}` called with {1} args, expected {2}")] 24 | CompileFuncArgsMismatch(String, usize, usize), 25 | #[error("internal error `{0}`")] 26 | CompileInternal(&'static str), 27 | #[error("couldn't set cranelift setting: {0}")] 28 | CraneliftSetting(#[from] SetError), 29 | #[error("module operation failed: {0}")] 30 | CraneliftModule(#[from] ModuleError), 31 | #[error("host architecture not supported: {0}")] 32 | CraneliftHostUnsupported(&'static str), 33 | #[error("codegen error: {0}")] 34 | CraneliftCodegenError(#[from] CodegenError), 35 | } 36 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub mod error; 4 | pub mod library; 5 | pub mod rpn; 6 | 7 | use std::collections::HashMap; 8 | 9 | use cranelift::jit::{JITBuilder, JITModule}; 10 | use cranelift::module::{Linkage, Module}; 11 | use cranelift::prelude::{ 12 | types::F32, AbiParam, Configurable, FunctionBuilder, FunctionBuilderContext, InstBuilder, 13 | MemFlags, Signature, 14 | }; 15 | use cranelift_codegen::{ir, settings, Context}; 16 | 17 | pub use error::JitError; 18 | pub use library::Library; 19 | pub use rpn::Program; 20 | 21 | /// RPN JIT compiler 22 | pub struct Compiler { 23 | module: JITModule, 24 | module_ctx: Context, 25 | builder_ctx: FunctionBuilderContext, 26 | fun_sigs: Vec<(String, Signature)>, 27 | } 28 | 29 | impl Compiler { 30 | /// New instance of the compiler 31 | /// 32 | /// The entries in the library are made available to the programs compiled 33 | /// later on. 34 | pub fn new(library: &Library) -> Result { 35 | let flags = [ 36 | ("use_colocated_libcalls", "false"), 37 | ("is_pic", "false"), 38 | ("opt_level", "speed"), 39 | ("enable_alias_analysis", "true"), 40 | ]; 41 | 42 | let mut flag_builder = settings::builder(); 43 | for (flag, value) in flags { 44 | flag_builder.set(flag, value)?; 45 | } 46 | 47 | let isa_builder = 48 | cranelift_native::builder().map_err(JitError::CraneliftHostUnsupported)?; 49 | 50 | let isa = isa_builder.finish(settings::Flags::new(flag_builder))?; 51 | let mut builder = JITBuilder::with_isa(isa, default_libcall_names()); 52 | for fun in library.iter() { 53 | builder.symbol(&fun.name, fun.ptr); 54 | } 55 | 56 | let module = JITModule::new(builder); 57 | let module_ctx = module.make_context(); 58 | let builder_ctx = FunctionBuilderContext::new(); 59 | 60 | let mut fun_sigs = Vec::new(); 61 | for fun in library.iter() { 62 | let mut sig = module.make_signature(); 63 | for _ in 0..fun.param_count { 64 | sig.params.push(AbiParam::new(F32)); 65 | } 66 | sig.returns.push(AbiParam::new(F32)); 67 | fun_sigs.push((fun.name.clone(), sig)); 68 | } 69 | 70 | Ok(Compiler { 71 | module, 72 | module_ctx, 73 | builder_ctx, 74 | fun_sigs, 75 | }) 76 | } 77 | 78 | /// Compile a [`Program`] returning a function pointer 79 | pub fn compile( 80 | &mut self, 81 | program: &Program, 82 | ) -> Result f32, JitError> { 83 | let ptr_type = self.module.target_config().pointer_type(); 84 | 85 | self.module_ctx.func.signature.params = vec![ 86 | AbiParam::new(F32), 87 | AbiParam::new(F32), 88 | AbiParam::new(F32), 89 | AbiParam::new(F32), 90 | AbiParam::new(F32), 91 | AbiParam::new(F32), 92 | AbiParam::new(ptr_type), 93 | AbiParam::new(ptr_type), 94 | ]; 95 | self.module_ctx.func.signature.returns = vec![AbiParam::new(F32)]; 96 | 97 | let id = self.module.declare_function( 98 | "jit_main", 99 | Linkage::Export, 100 | &self.module_ctx.func.signature, 101 | )?; 102 | 103 | let mut builder = FunctionBuilder::new(&mut self.module_ctx.func, &mut self.builder_ctx); 104 | 105 | let block = builder.create_block(); 106 | builder.seal_block(block); 107 | 108 | builder.append_block_params_for_function_params(block); 109 | builder.switch_to_block(block); 110 | 111 | let (v_x, v_y, v_a, v_b, v_c, v_d, v_sig1, v_sig2) = { 112 | let params = builder.block_params(block); 113 | ( 114 | params[0], params[1], params[2], params[3], params[4], params[5], params[6], 115 | params[7], 116 | ) 117 | }; 118 | 119 | let v_sig1_rd = program.0.iter().find_map(|tok| { 120 | use rpn::{Token, Var}; 121 | if let Token::PushVar(Var::Sig1) = tok { 122 | Some(builder.ins().load(F32, MemFlags::new(), v_sig1, 0)) 123 | } else { 124 | None 125 | } 126 | }); 127 | let v_sig2_rd = program.0.iter().find_map(|tok| { 128 | use rpn::{Token, Var}; 129 | if let Token::PushVar(Var::Sig2) = tok { 130 | Some(builder.ins().load(F32, MemFlags::new(), v_sig2, 0)) 131 | } else { 132 | None 133 | } 134 | }); 135 | 136 | let extern_funs = { 137 | let mut tmp = HashMap::new(); 138 | for (name, sig) in &self.fun_sigs { 139 | let callee = self.module.declare_function(&name, Linkage::Import, &sig)?; 140 | let fun_ref = self.module.declare_func_in_func(callee, builder.func); 141 | 142 | tmp.insert(name.as_str(), (fun_ref, sig.params.len())); 143 | } 144 | 145 | tmp 146 | }; 147 | 148 | let mut stack = Vec::new(); 149 | 150 | for token in &program.0 { 151 | use rpn::{Binop, Function, Out, Token, Unop, Var}; 152 | 153 | match token { 154 | Token::Push(v) => { 155 | let val = builder.ins().f32const(v.value()); 156 | stack.push(val); 157 | } 158 | Token::PushVar(var) => { 159 | let val = 160 | match var { 161 | // ins 162 | Var::X => v_x, 163 | Var::Y => v_y, 164 | Var::A => v_a, 165 | Var::B => v_b, 166 | Var::C => v_c, 167 | Var::D => v_d, 168 | // inouts 169 | Var::Sig1 => v_sig1_rd 170 | .ok_or(JitError::CompileInternal("sig1 read not prepared"))?, 171 | Var::Sig2 => v_sig2_rd 172 | .ok_or(JitError::CompileInternal("sig1 read not prepared"))?, 173 | }; 174 | stack.push(val); 175 | } 176 | Token::Binop(op) => { 177 | let b = stack 178 | .pop() 179 | .ok_or(JitError::CompileInternal("RPN stack exhausted"))?; 180 | let a = stack 181 | .pop() 182 | .ok_or(JitError::CompileInternal("RPN stack exhausted"))?; 183 | 184 | let val = match op { 185 | Binop::Add => builder.ins().fadd(a, b), 186 | Binop::Sub => builder.ins().fsub(a, b), 187 | Binop::Mul => builder.ins().fmul(a, b), 188 | Binop::Div => builder.ins().fdiv(a, b), 189 | }; 190 | 191 | stack.push(val); 192 | } 193 | Token::Unop(op) => { 194 | let x = stack 195 | .pop() 196 | .ok_or(JitError::CompileInternal("RPN stack exhausted"))?; 197 | let val = match op { 198 | Unop::Neg => builder.ins().fneg(x), 199 | }; 200 | 201 | stack.push(val); 202 | } 203 | Token::Write(out) => { 204 | let x = *stack 205 | .last() 206 | .ok_or(JitError::CompileInternal("RPN stack exhausted"))?; 207 | let ptr = match out { 208 | Out::Sig1 => v_sig1, 209 | Out::Sig2 => v_sig2, 210 | }; 211 | builder.ins().store(MemFlags::new(), x, ptr, 0); 212 | } 213 | Token::Function(Function { name, args }) => { 214 | let (func, param_n) = *extern_funs 215 | .get(name.as_str()) 216 | .ok_or_else(|| JitError::CompileUknownFunc(name.clone()))?; 217 | 218 | // Ensure that invalid RPN won't result in an invalid function call 219 | if param_n != *args { 220 | return Err(JitError::CompileFuncArgsMismatch( 221 | name.to_string(), 222 | param_n, 223 | *args, 224 | )); 225 | } 226 | 227 | let mut arg_vs = Vec::new(); 228 | for _ in 0..*args { 229 | let arg = stack 230 | .pop() 231 | .ok_or(JitError::CompileInternal("RPN stack exhausted"))?; 232 | arg_vs.push(arg); 233 | } 234 | arg_vs.reverse(); 235 | 236 | let call = builder.ins().call(func, &arg_vs); 237 | let result = builder.inst_results(call)[0]; 238 | 239 | stack.push(result); 240 | } 241 | Token::Noop => {} 242 | } 243 | } 244 | 245 | let read_ret = stack 246 | .pop() 247 | .ok_or(JitError::CompileInternal("RPN stack exhausted"))?; 248 | builder.ins().return_(&[read_ret]); 249 | builder.finalize(); 250 | 251 | self.module.define_function(id, &mut self.module_ctx)?; 252 | 253 | self.module.clear_context(&mut self.module_ctx); 254 | self.module.finalize_definitions()?; 255 | 256 | let code = self.module.get_finalized_function(id); 257 | 258 | let func = unsafe { 259 | std::mem::transmute::<_, fn(f32, f32, f32, f32, f32, f32, &mut f32, &mut f32) -> f32>( 260 | code, 261 | ) 262 | }; 263 | 264 | Ok(func) 265 | } 266 | 267 | /// Free the functions built by this [`Compiler`] 268 | /// 269 | /// SAFETY: 270 | /// - None of the function pointers returned from this compiler can run 271 | /// at the moment this function is called or ever called again. 272 | pub unsafe fn free_memory(self) { 273 | self.module.free_memory(); 274 | } 275 | } 276 | 277 | /// Default names for [ir::LibCall]s. A function by this name is imported into the object as 278 | /// part of the translation of a [ir::ExternalName::LibCall] variant. 279 | fn default_libcall_names() -> Box String + Send + Sync> { 280 | Box::new(move |libcall| match libcall { 281 | ir::LibCall::Probestack => "__cranelift_probestack".to_owned(), 282 | ir::LibCall::CeilF32 => "ceilf".to_owned(), 283 | ir::LibCall::CeilF64 => "ceil".to_owned(), 284 | ir::LibCall::FloorF32 => "floorf".to_owned(), 285 | ir::LibCall::FloorF64 => "floor".to_owned(), 286 | ir::LibCall::TruncF32 => "truncf".to_owned(), 287 | ir::LibCall::TruncF64 => "trunc".to_owned(), 288 | ir::LibCall::NearestF32 => "nearbyintf".to_owned(), 289 | ir::LibCall::NearestF64 => "nearbyint".to_owned(), 290 | ir::LibCall::FmaF32 => "fmaf".to_owned(), 291 | ir::LibCall::FmaF64 => "fma".to_owned(), 292 | ir::LibCall::Memcpy => "memcpy".to_owned(), 293 | ir::LibCall::Memset => "memset".to_owned(), 294 | ir::LibCall::Memmove => "memmove".to_owned(), 295 | ir::LibCall::Memcmp => "memcmp".to_owned(), 296 | 297 | ir::LibCall::ElfTlsGetAddr => "__tls_get_addr".to_owned(), 298 | ir::LibCall::ElfTlsGetOffset => "__tls_get_offset".to_owned(), 299 | ir::LibCall::X86Pshufb => "__cranelift_x86_pshufb".to_owned(), 300 | }) 301 | } 302 | 303 | #[cfg(test)] 304 | mod tests { 305 | use super::*; 306 | 307 | #[test] 308 | fn test_basic() { 309 | let x = 1.0f32; 310 | let y = 2.0f32; 311 | let a = 3.0; 312 | let b = 5.0; 313 | let c = 8.0; 314 | let d = 13.0; 315 | let sig1 = 21.0; 316 | let sig2 = 34.0; 317 | 318 | let cases = [ 319 | ("x", (x, sig1, sig2)), 320 | ("sin(x * y)", ((x * y).sin(), sig1, sig2)), 321 | ("a + b + c + d", (a + b + c + d, sig1, sig2)), 322 | ("_1(a) + _2(b)", (a + b, a, b)), 323 | ("_1(x) + _2(y)", (x + y, x, y)), 324 | ("sin(x) + 2 * cos(y)", (x.sin() + 2.0 * y.cos(), sig1, sig2)), 325 | ("_1(c) * 0 + _1", (sig1, c, sig2)), 326 | ("_1(1234) * 0 + _1", (sig1, 1234.0, sig2)), 327 | ]; 328 | 329 | let library = Library::default(); 330 | 331 | for (code, expected) in cases { 332 | let mut compiler = Compiler::new(&library).unwrap(); 333 | 334 | let parsed = Program::parse_from_infix(code).unwrap(); 335 | let func = compiler.compile(&parsed).unwrap(); 336 | 337 | let mut sig1_ = sig1; 338 | let mut sig2_ = sig2; 339 | 340 | let result = func(x, y, a, b, c, d, &mut sig1_, &mut sig2_); 341 | 342 | const EPS: f32 = 0.00001; 343 | assert!( 344 | (result - expected.0) < EPS, 345 | "{} = {}, expected {}", 346 | code, 347 | result, 348 | expected.0 349 | ); 350 | assert!( 351 | (sig1_ - expected.1) < EPS, 352 | "{} | sig1 = {}, expected {}", 353 | code, 354 | sig1_, 355 | expected.1 356 | ); 357 | assert!( 358 | (sig2_ - expected.2) < EPS, 359 | "{} | sig2 = {}, expected {}", 360 | code, 361 | sig2_, 362 | expected.2 363 | ); 364 | } 365 | } 366 | 367 | #[test] 368 | fn test_sig_behavior() { 369 | let x = 1.0f32; 370 | let y = 0.0f32; 371 | let a = 0.0; 372 | let b = 0.0; 373 | let c = 0.0; 374 | let d = 0.0; 375 | let mut sig1 = 0.0; 376 | let mut sig2 = 0.0; 377 | 378 | let expr = "_1(_1 + x)"; 379 | 380 | let parsed = Program::parse_from_infix(expr).unwrap(); 381 | let mut compiler = Compiler::new(&Library::default()).unwrap(); 382 | let func = compiler.compile(&parsed).unwrap(); 383 | 384 | for k in 1..531 { 385 | let r = func(x, y, a, b, c, d, &mut sig1, &mut sig2); 386 | assert_eq!((r, sig1, sig2), (k as f32, k as f32, 0.0),) 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/library.rs: -------------------------------------------------------------------------------- 1 | //! Management of functions accessible to programs 2 | 3 | use std::mem::transmute; 4 | 5 | /// Description of a function accessible to the compiled program 6 | /// 7 | /// Currently only functions of the form `fn(f32, [f32, ..]) -> f32` are 8 | /// supported. 9 | pub struct FunctionF32 { 10 | /// Name of the function exposed to the program 11 | pub name: String, 12 | /// Pointer to the function 13 | pub(crate) ptr: *const u8, 14 | /// Number of arguments of the function 15 | pub(crate) param_count: usize, 16 | } 17 | 18 | impl FunctionF32 { 19 | /// Constructs a new instance of 1-argument [`FunctionF32`] 20 | pub fn new_1(name: String, ptr: extern "C" fn(f32) -> f32) -> Self { 21 | FunctionF32 { 22 | name, 23 | ptr: ptr as *const u8, 24 | param_count: 1, 25 | } 26 | } 27 | 28 | /// Constructs a new instance of 2-argument [`FunctionF32`] 29 | pub fn new_2(name: String, ptr: extern "C" fn(f32, f32) -> f32) -> Self { 30 | FunctionF32 { 31 | name, 32 | ptr: ptr as *const u8, 33 | param_count: 2, 34 | } 35 | } 36 | 37 | /// Call this function with 1 argument 38 | /// 39 | /// Returns None if parameter count is incorrect. 40 | pub fn call_1(&self, x: f32) -> Option { 41 | if self.param_count != 1 { 42 | None 43 | } else { 44 | // SAFETY: param_count proves that ptr was instantiated from a 45 | // function of this signature. 46 | Some(unsafe { transmute::<_, extern "C" fn(f32) -> f32>(self.ptr)(x) }) 47 | } 48 | } 49 | 50 | /// Call this function with 2 arguments 51 | /// 52 | /// Returns None if parameter count is incorrect. 53 | pub fn call_2(&self, x: f32, y: f32) -> Option { 54 | if self.param_count != 2 { 55 | None 56 | } else { 57 | // SAFETY: param_count proves that ptr was instantiated from a 58 | // function of this signature. 59 | Some(unsafe { transmute::<_, extern "C" fn(f32, f32) -> f32>(self.ptr)(x, y) }) 60 | } 61 | } 62 | } 63 | 64 | /// Library of functions accessible to the program 65 | pub struct Library { 66 | funs: Vec, 67 | } 68 | 69 | impl Library { 70 | /// Append a new function to the library 71 | pub fn insert(&mut self, fun: FunctionF32) { 72 | self.funs.push(fun); 73 | } 74 | 75 | /// Iterator over the functions 76 | pub fn iter(&self) -> impl Iterator { 77 | self.funs.iter() 78 | } 79 | } 80 | 81 | /// Default implementation of the library 82 | /// 83 | /// Provider `sin(x)`, `cos(x)` and `pow(a, b)`, as those functions are 84 | /// accessible to all programs. 85 | impl Default for Library { 86 | fn default() -> Self { 87 | extern "C" fn sin_(x: f32) -> f32 { 88 | x.sin() 89 | } 90 | 91 | extern "C" fn cos_(x: f32) -> f32 { 92 | x.cos() 93 | } 94 | 95 | extern "C" fn tan_(x: f32) -> f32 { 96 | x.tan() 97 | } 98 | 99 | extern "C" fn abs_(x: f32) -> f32 { 100 | x.abs() 101 | } 102 | 103 | extern "C" fn sqrt_(x: f32) -> f32 { 104 | x.sqrt() 105 | } 106 | 107 | extern "C" fn pow_(a: f32, b: f32) -> f32 { 108 | a.powf(b) 109 | } 110 | 111 | Library { 112 | funs: vec![ 113 | FunctionF32::new_1("sin".into(), sin_), 114 | FunctionF32::new_1("cos".into(), cos_), 115 | FunctionF32::new_1("tan".into(), tan_), 116 | FunctionF32::new_1("abs".into(), abs_), 117 | FunctionF32::new_1("sqrt".into(), sqrt_), 118 | FunctionF32::new_2("pow".into(), pow_), 119 | ], 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/rpn.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and operations on the program 2 | 3 | use crate::{error::JitError, Library}; 4 | 5 | /// RPN Token 6 | #[derive(Clone, Debug, PartialEq, PartialOrd)] 7 | pub enum Token { 8 | /// Push a value onto the stack 9 | Push(Value), 10 | /// Push variable value onto the stack 11 | PushVar(Var), 12 | /// Write top of stack to in-out variable 13 | Write(Out), 14 | /// Binary operation 15 | /// 16 | /// Pops 2 values from the stack, performs the operation, and pushes the 17 | /// result back onto the stack 18 | Binop(Binop), 19 | /// Unary operation 20 | /// 21 | /// Replaces the top value on the stack with the result of the operation 22 | Unop(Unop), 23 | /// Function call 24 | /// 25 | /// Pops a number of arguments from the stack, evaluates the function, and 26 | /// pushes the result back onto the stack. 27 | Function(Function), 28 | /// No operation 29 | Noop, 30 | } 31 | 32 | /// Constant value 33 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 34 | pub enum Value { 35 | /// Arbotrary value 36 | Literal(f32), 37 | /// Pi 38 | Pi, 39 | /// Euler's constant 40 | E, 41 | } 42 | 43 | impl Value { 44 | /// Obtains the corresponding value 45 | pub fn value(self) -> f32 { 46 | match self { 47 | Value::Literal(f) => f, 48 | Value::Pi => std::f32::consts::PI, 49 | Value::E => std::f32::consts::E, 50 | } 51 | } 52 | } 53 | 54 | /// Readable variables 55 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 56 | pub enum Var { 57 | X, 58 | Y, 59 | A, 60 | B, 61 | C, 62 | D, 63 | Sig1, 64 | Sig2, 65 | } 66 | 67 | /// Writeable variables 68 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 69 | pub enum Out { 70 | Sig1, 71 | Sig2, 72 | } 73 | 74 | /// Binary operation 75 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 76 | pub enum Binop { 77 | /// Addition 78 | Add, 79 | /// Subtraction 80 | Sub, 81 | /// Multiplication 82 | Mul, 83 | /// Division 84 | Div, 85 | } 86 | 87 | /// Unary operation 88 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 89 | pub enum Unop { 90 | /// Negation 91 | Neg, 92 | } 93 | 94 | /// Function call 95 | #[derive(Clone, Debug, PartialEq, PartialOrd)] 96 | pub struct Function { 97 | /// Name of the function 98 | pub name: String, 99 | /// Number of arguments 100 | pub args: usize, 101 | } 102 | 103 | /// Parsed program representation 104 | /// 105 | /// The program is represented using Reverse Polish Notation, which is lends 106 | /// to easy iterative translation into CLIF as well as to simple optimizations. 107 | #[derive(Debug, PartialEq, PartialOrd)] 108 | pub struct Program(pub Vec); 109 | 110 | impl Program { 111 | /// Constructs program directly from RPN 112 | pub fn new(tokens: Vec) -> Self { 113 | Program(tokens) 114 | } 115 | 116 | /// Parses an infix notation into RPN 117 | pub fn parse_from_infix(expr: &str) -> Result { 118 | let tokens = meval::tokenizer::tokenize(expr)?; 119 | let meval_rpn = meval::shunting_yard::to_rpn(&tokens)?; 120 | 121 | let mut prog = Vec::new(); 122 | for meval_token in meval_rpn { 123 | use meval::tokenizer::Operation as MevalOp; 124 | use meval::tokenizer::Token as MevalToken; 125 | let token = match meval_token { 126 | MevalToken::Var(name) => match name.as_str() { 127 | "x" => Token::PushVar(Var::X), 128 | "y" => Token::PushVar(Var::Y), 129 | "a" => Token::PushVar(Var::A), 130 | "b" => Token::PushVar(Var::B), 131 | "c" => Token::PushVar(Var::C), 132 | "d" => Token::PushVar(Var::D), 133 | "_1" => Token::PushVar(Var::Sig1), 134 | "_2" => Token::PushVar(Var::Sig2), 135 | "pi" => Token::Push(Value::Pi), 136 | "e" => Token::Push(Value::E), 137 | _ => return Err(JitError::ParseUnknownVariable(name.to_string())), 138 | }, 139 | MevalToken::Number(f) => Token::Push(Value::Literal(f as f32)), 140 | MevalToken::Binary(op) => match op { 141 | MevalOp::Plus => Token::Binop(Binop::Add), 142 | MevalOp::Minus => Token::Binop(Binop::Sub), 143 | MevalOp::Times => Token::Binop(Binop::Mul), 144 | MevalOp::Div => Token::Binop(Binop::Div), 145 | MevalOp::Pow => Token::Function(Function { 146 | name: "pow".to_string(), 147 | args: 2, 148 | }), 149 | _ => return Err(JitError::ParseUnknownBinop(format!("{op:?}"))), 150 | }, 151 | MevalToken::Unary(op) => match op { 152 | MevalOp::Plus => Token::Noop, 153 | MevalOp::Minus => Token::Unop(Unop::Neg), 154 | _ => return Err(JitError::ParseUnknownUnop(format!("{op:?}"))), 155 | }, 156 | MevalToken::Func(name, Some(1)) if name == "_1" => Token::Write(Out::Sig1), 157 | MevalToken::Func(name, Some(1)) if name == "_2" => Token::Write(Out::Sig2), 158 | MevalToken::Func(name, args) => Token::Function(Function { 159 | name, 160 | args: args.unwrap_or_default(), 161 | }), 162 | 163 | other => return Err(JitError::ParseUnknownToken(format!("{other:?}"))), 164 | }; 165 | 166 | prog.push(token); 167 | } 168 | 169 | Ok(Program(prog)) 170 | } 171 | 172 | /// Rewrites RPN into a deeper form that's more optimizable 173 | /// 174 | /// The optimizer isn't able to optimize RPN like `[.. 1 + 1 +]`. This 175 | /// function will replace it with `[.. 1 1 + +]`, which the optimizer 176 | /// will rewrite as `[.. 2 +]`. 177 | /// 178 | /// The resultant form has a deeper stack, meaning more variables need to 179 | /// be kept alive at the same time. 180 | pub fn reorder_ops_deepen(&mut self) { 181 | for n in 2..self.0.len() { 182 | let (tok0, tok1, tok2) = ( 183 | self.0[n - 2].clone(), 184 | self.0[n - 1].clone(), 185 | self.0[n].clone(), 186 | ); 187 | 188 | let (ntok0, ntok1, ntok2) = match (tok0, tok1, tok2) { 189 | ( 190 | op1 @ Token::Binop(Binop::Add | Binop::Sub), 191 | push @ (Token::Push(_) | Token::PushVar(_)), 192 | op2 @ Token::Binop(Binop::Add | Binop::Sub), 193 | ) => (push, op2, op1), 194 | ( 195 | op1 @ Token::Binop(Binop::Mul | Binop::Div), 196 | push @ (Token::Push(_) | Token::PushVar(_)), 197 | op2 @ Token::Binop(Binop::Mul | Binop::Div), 198 | ) => (push, op2, op1), 199 | _ => continue, 200 | }; 201 | 202 | self.0[n - 2] = ntok0; 203 | self.0[n - 1] = ntok1; 204 | self.0[n] = ntok2; 205 | } 206 | } 207 | 208 | /// Rewrites RPN into a form that requires a lower stack 209 | /// 210 | /// `a * (b / c)` will produce RPN `a b c / *`, which keeps up to 3 variables 211 | /// alive at once. This optimization will rewrite it into RPN `a b * c /`, 212 | /// which does the same work despite using less memory. 213 | /// 214 | /// Notably the constant folding algorithm in this library will fail to 215 | /// optimize this form. 216 | pub fn reorder_ops_flatten(&mut self) { 217 | let mut work_done = true; 218 | while work_done { 219 | work_done = false; 220 | 221 | for n in 2..self.0.len() { 222 | let (tok0, tok1, tok2) = ( 223 | self.0[n - 2].clone(), 224 | self.0[n - 1].clone(), 225 | self.0[n].clone(), 226 | ); 227 | 228 | let (ntok0, ntok1, ntok2) = match (tok0, tok1, tok2) { 229 | ( 230 | push @ (Token::Push(_) | Token::PushVar(_)), 231 | op2 @ Token::Binop(Binop::Add | Binop::Sub | Binop::Mul | Binop::Div), 232 | op1 @ Token::Binop(Binop::Add | Binop::Sub | Binop::Mul | Binop::Div), 233 | ) => (op1, push, op2), 234 | _ => continue, 235 | }; 236 | 237 | self.0[n - 2] = ntok0; 238 | self.0[n - 1] = ntok1; 239 | self.0[n] = ntok2; 240 | work_done = true; 241 | } 242 | } 243 | } 244 | 245 | /// Evaluate some constant expressions 246 | /// 247 | /// Optimizes binary and unary operations: 248 | /// - replace `[const0, const1, op]` with `[op(const0, const1)]` 249 | /// - replace `[const, op]` with `[op(const)]` 250 | /// 251 | /// [`Token::Noop`] is removed in the process. Only one pass over the code 252 | /// is made. Returns `false` if no further progress can be made. 253 | /// 254 | /// Doesn't support reordering of associative operations, so 255 | /// `[var, const0, add, const1, add]` is *not* replaced with 256 | /// `[var, add(const0, const1), add]` and so on. 257 | pub fn fold_constants_step(&mut self, library: &Library) -> bool { 258 | let mut work_done = false; 259 | 260 | for n in 2..self.0.len() { 261 | match self.0[n].clone() { 262 | Token::Unop(unop) => { 263 | let Token::Push(a) = self.0[n - 1] else { 264 | continue; 265 | }; 266 | let result = match unop { 267 | Unop::Neg => -a.value(), 268 | }; 269 | 270 | self.0[n - 1] = Token::Noop; 271 | self.0[n] = Token::Push(Value::Literal(result)); 272 | work_done = true; 273 | } 274 | Token::Binop(binop) => { 275 | let Token::Push(a) = self.0[n - 2] else { 276 | continue; 277 | }; 278 | let Token::Push(b) = self.0[n - 1] else { 279 | continue; 280 | }; 281 | 282 | let (a, b) = (a.value(), b.value()); 283 | let result = match binop { 284 | Binop::Add => a + b, 285 | Binop::Sub => a - b, 286 | Binop::Mul => a * b, 287 | Binop::Div => a / b, 288 | }; 289 | 290 | self.0[n - 2] = Token::Noop; 291 | self.0[n - 1] = Token::Noop; 292 | self.0[n] = Token::Push(Value::Literal(result)); 293 | work_done = true; 294 | } 295 | Token::Function(Function { name, args }) => { 296 | let Some(extern_fun) = library.iter().find(|f| f.name == name) else { 297 | log::warn!("No function {name} in library, compilation will fail"); 298 | continue; 299 | }; 300 | 301 | let result = match args { 302 | 1 => { 303 | let Token::Push(a) = self.0[n - 1] else { 304 | continue; 305 | }; 306 | extern_fun.call_1(a.value()) 307 | } 308 | 2 => { 309 | let Token::Push(a) = self.0[n - 2] else { 310 | continue; 311 | }; 312 | let Token::Push(b) = self.0[n - 1] else { 313 | continue; 314 | }; 315 | extern_fun.call_2(a.value(), b.value()) 316 | } 317 | _ => continue, 318 | }; 319 | 320 | let Some(value) = result else { 321 | log::warn!("Function {name} called with invalid number of arguments, compilation will fail"); 322 | continue; 323 | }; 324 | 325 | self.0[n - args..n].fill_with(|| Token::Noop); 326 | self.0[n] = Token::Push(Value::Literal(value)); 327 | } 328 | _ => continue, 329 | } 330 | } 331 | 332 | self.0.retain(|tok| *tok != Token::Noop); 333 | 334 | work_done 335 | } 336 | 337 | /// Rewrites RPN into a form most suitable for codegen 338 | /// 339 | /// Performs constant folding and minimizes stack usage of the resultant RPN. 340 | /// 341 | /// For details, see: 342 | /// - [`Self::reorder_ops_deepen`] 343 | /// - [`Self::reorder_ops_flatten`] 344 | /// - [`Self::fold_constants_step`] 345 | pub fn optimize(&mut self, library: &Library) { 346 | let mut work_done = true; 347 | while work_done { 348 | self.reorder_ops_deepen(); 349 | work_done = self.fold_constants_step(library); 350 | } 351 | 352 | self.reorder_ops_flatten(); 353 | } 354 | } 355 | 356 | #[cfg(test)] 357 | mod tests { 358 | use std::f32::consts::PI; 359 | 360 | use crate::{ 361 | rpn::{Token, Value}, 362 | Library, Program, 363 | }; 364 | 365 | use super::{Binop, Function, Out, Unop, Var}; 366 | 367 | #[test] 368 | fn test_parse() { 369 | let two = || Token::Push(Value::Literal(2.0)); 370 | 371 | let cases = [ 372 | ("2", vec![two()]), 373 | ("2 + 2", vec![two(), two(), Token::Binop(Binop::Add)]), 374 | ("2 - 2", vec![two(), two(), Token::Binop(Binop::Sub)]), 375 | ("2 * 2", vec![two(), two(), Token::Binop(Binop::Mul)]), 376 | ("2 / 2", vec![two(), two(), Token::Binop(Binop::Div)]), 377 | ( 378 | "2 ^ 2", 379 | vec![ 380 | two(), 381 | two(), 382 | Token::Function(Function { 383 | name: "pow".into(), 384 | args: 2, 385 | }), 386 | ], 387 | ), 388 | ("-2", vec![two(), Token::Unop(Unop::Neg)]), 389 | ( 390 | "sin(cos(tan(_2(_1(2)))))", 391 | vec![ 392 | two(), 393 | Token::Write(Out::Sig1), 394 | Token::Write(Out::Sig2), 395 | Token::Function(Function { 396 | name: "tan".into(), 397 | args: 1, 398 | }), 399 | Token::Function(Function { 400 | name: "cos".into(), 401 | args: 1, 402 | }), 403 | Token::Function(Function { 404 | name: "sin".into(), 405 | args: 1, 406 | }), 407 | ], 408 | ), 409 | ("x", vec![Token::PushVar(Var::X)]), 410 | ("y", vec![Token::PushVar(Var::Y)]), 411 | ("a", vec![Token::PushVar(Var::A)]), 412 | ("b", vec![Token::PushVar(Var::B)]), 413 | ("c", vec![Token::PushVar(Var::C)]), 414 | ("d", vec![Token::PushVar(Var::D)]), 415 | ("pi", vec![Token::Push(Value::Pi)]), 416 | ("e", vec![Token::Push(Value::E)]), 417 | ]; 418 | 419 | for (expr, tokens) in cases { 420 | assert_eq!(Program::parse_from_infix(expr).unwrap(), Program(tokens)); 421 | } 422 | } 423 | 424 | #[test] 425 | fn test_optimize() { 426 | let x = |x| Token::Push(Value::Literal(x)); 427 | 428 | fn rough_compare(prog0: &Program, prog1: &Program) -> bool { 429 | if prog0.0.len() != prog1.0.len() { 430 | return false; 431 | } 432 | 433 | for (tok0, tok1) in prog0.0.iter().zip(prog1.0.iter()) { 434 | const EPS: f32 = 0.00001; 435 | match (tok0, tok1) { 436 | (Token::Push(Value::Literal(l)), Token::Push(Value::Literal(r))) => { 437 | if (l - r).abs() > EPS { 438 | return false; 439 | } 440 | } 441 | (left, right) => { 442 | if left != right { 443 | return false; 444 | } 445 | } 446 | } 447 | } 448 | 449 | true 450 | } 451 | 452 | let cases = [ 453 | ("2", vec![x(2.0)]), 454 | ("2 + 2", vec![x(4.0)]), 455 | ("2 + -2", vec![x(0.0)]), 456 | ("sin(pi/2 + pi/2)", vec![x(0.0)]), 457 | ( 458 | "sin(pi/2 + pi/2) + x", 459 | vec![x(0.0), Token::PushVar(Var::X), Token::Binop(Binop::Add)], 460 | ), 461 | ( 462 | "x + 1 + 1", 463 | vec![Token::PushVar(Var::X), x(2.0), Token::Binop(Binop::Add)], 464 | ), 465 | ( 466 | "x * pi/4/3", 467 | vec![ 468 | Token::PushVar(Var::X), 469 | x(PI / 12.0), 470 | Token::Binop(Binop::Mul), 471 | ], 472 | ), 473 | ( 474 | "a + b + c", 475 | vec![ 476 | Token::PushVar(Var::A), 477 | Token::PushVar(Var::B), 478 | Token::Binop(Binop::Add), 479 | Token::PushVar(Var::C), 480 | Token::Binop(Binop::Add), 481 | ], 482 | ), 483 | ( 484 | "x * (a / b)", 485 | vec![ 486 | Token::PushVar(Var::X), 487 | Token::PushVar(Var::A), 488 | Token::Binop(Binop::Mul), 489 | Token::PushVar(Var::B), 490 | Token::Binop(Binop::Div), 491 | ], 492 | ), 493 | ]; 494 | 495 | for (expr, tokens) in cases { 496 | let mut program = Program::parse_from_infix(expr).unwrap(); 497 | program.optimize(&Library::default()); 498 | let expected = Program(tokens); 499 | assert!( 500 | rough_compare(&program, &expected), 501 | "{program:?} != {expected:?}" 502 | ); 503 | } 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /test.clif: -------------------------------------------------------------------------------- 1 | function u0:0(f32, f32, f32, f32, f32, f32, i64, i64) -> f32 system_v { 2 | sig0 = (f32) -> f32 system_v 3 | sig1 = (f32) -> f32 system_v 4 | sig2 = (f32, f32) -> f32 system_v 5 | fn0 = u0:1 sig0 6 | fn1 = u0:2 sig1 7 | fn2 = u0:3 sig2 8 | 9 | block0(v0: f32, v1: f32, v2: f32, v3: f32, v4: f32, v5: f32, v6: i64, v7: i64): 10 | v10 = f32const 0x1.921fb6p1 11 | v11 = f32const 0x1.000000p1 12 | v12 = fdiv v10, v11 ; v10 = 0x1.921fb6p1, v11 = 0x1.000000p1 13 | store v12, v7 14 | v13 = fadd v0, v12 15 | v14 = call fn0(v13) 16 | return v14 17 | } --------------------------------------------------------------------------------