From ac34c7c283e3cdba300854cc395ea0c77bd640aa Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Fri, 13 Mar 2026 13:57:12 -0600 Subject: [PATCH] feat: add native Rust TTSR regex engine via RegexSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TTSR's checkDelta() runs O(rules x conditions) regex evaluations per streaming token — the hottest path in GSD. This adds a Rust native module that compiles all condition patterns into a single RegexSet, testing them in one DFA pass instead of sequential JS RegExp iteration. The TtsrManager transparently uses the native engine when available and falls back to the existing JS regex loop when it is not. Co-Authored-By: Claude Opus 4.6 (1M context) --- native/Cargo.lock | 565 +++++++++++++++++- native/crates/engine/Cargo.toml | 1 + native/crates/engine/src/lib.rs | 1 + native/crates/engine/src/ttsr.rs | 124 ++++ packages/native/src/__tests__/ttsr.test.mjs | 135 +++++ packages/native/src/index.ts | 3 + packages/native/src/native.ts | 3 + packages/native/src/ttsr/index.ts | 39 ++ packages/native/src/ttsr/types.ts | 10 + src/resources/extensions/ttsr/ttsr-manager.ts | 86 +++ 10 files changed, 964 insertions(+), 3 deletions(-) create mode 100644 native/crates/engine/src/ttsr.rs create mode 100644 packages/native/src/__tests__/ttsr.test.mjs create mode 100644 packages/native/src/ttsr/index.ts create mode 100644 packages/native/src/ttsr/types.ts diff --git a/native/Cargo.lock b/native/Cargo.lock index be0931d5b..453057f5b 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -30,6 +36,26 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "ast-grep-core" version = "0.39.9" @@ -51,12 +77,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -89,6 +130,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "cc" version = "1.2.56" @@ -105,6 +158,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "const-random" version = "0.1.18" @@ -134,6 +202,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -175,6 +252,30 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "either" version = "1.15.0" @@ -205,24 +306,106 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -234,6 +417,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "globset" version = "0.4.18" @@ -338,11 +531,23 @@ dependencies = [ name = "gsd-engine" version = "0.1.0" dependencies = [ + "arboard", + "dashmap", + "globset", + "gsd-ast", "gsd-grep", "html-to-markdown-rs", + "ignore", + "image", + "libc", "napi", "napi-build", "napi-derive", + "regex", + "smallvec", + "syntect", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -356,6 +561,23 @@ dependencies = [ "rayon", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" @@ -419,6 +641,35 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -426,7 +677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -451,6 +702,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "lock_api" version = "0.4.14" @@ -472,7 +729,7 @@ version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -501,6 +758,26 @@ dependencies = [ "libc", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "napi" version = "2.16.17" @@ -564,6 +841,88 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -593,6 +952,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "phf" version = "0.13.1" @@ -646,6 +1011,19 @@ dependencies = [ "siphasher", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -661,6 +1039,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.45" @@ -728,6 +1118,19 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "same-file" version = "1.0.6" @@ -798,6 +1201,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "siphasher" version = "1.0.2" @@ -851,6 +1260,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "regex-syntax", + "serde", + "serde_derive", + "thiserror", + "walkdir", +] + [[package]] name = "tendril" version = "0.5.0" @@ -881,6 +1308,20 @@ dependencies = [ "syn", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -1292,6 +1733,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "utf-8" version = "0.7.6" @@ -1338,13 +1785,19 @@ dependencies = [ "string_cache_codegen", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1353,6 +1806,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1362,6 +1824,88 @@ dependencies = [ "windows-link", ] +[[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", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "zerocopy" version = "0.8.42" @@ -1387,3 +1931,18 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] diff --git a/native/crates/engine/Cargo.toml b/native/crates/engine/Cargo.toml index 506628fd6..e1ed956c0 100644 --- a/native/crates/engine/Cargo.toml +++ b/native/crates/engine/Cargo.toml @@ -26,6 +26,7 @@ image = { version = "0.25", default-features = false, features = [ ] } napi = { version = "2", features = ["napi8"] } napi-derive = "2" +regex = "1" smallvec = "1" syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "regex-fancy"] } unicode-segmentation = "1" diff --git a/native/crates/engine/src/lib.rs b/native/crates/engine/src/lib.rs index 0c4d3230e..d85986280 100644 --- a/native/crates/engine/src/lib.rs +++ b/native/crates/engine/src/lib.rs @@ -19,4 +19,5 @@ mod html; mod ps; mod task; mod text; +mod ttsr; mod image; diff --git a/native/crates/engine/src/ttsr.rs b/native/crates/engine/src/ttsr.rs new file mode 100644 index 000000000..571105936 --- /dev/null +++ b/native/crates/engine/src/ttsr.rs @@ -0,0 +1,124 @@ +//! N-API bindings for the TTSR (Time Traveling Stream Rules) regex engine. +//! +//! Pre-compiles all rule condition patterns into a `regex::RegexSet` so that +//! `checkBuffer` can test all patterns against the accumulated stream buffer +//! in a single DFA pass, instead of iterating O(rules x conditions) in JS. + +use napi::bindgen_prelude::*; +use napi_derive::napi; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; + +/// Maps a compiled regex back to its owning rule. +struct PatternMapping { + /// Index into the RegexSet's pattern list. + _pattern_index: usize, + /// Name of the rule this pattern belongs to. + rule_name: String, +} + +struct CompiledRuleSet { + regex_set: regex::RegexSet, + mappings: Vec, +} + +// Global handle store — handles are u64 keys into this map. +static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1); +static STORE: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); + +#[napi(object)] +pub struct NapiTtsrRuleInput { + pub name: String, + pub conditions: Vec, +} + +/// Compile a set of TTSR rules into an optimized regex engine. +/// +/// Returns an opaque numeric handle. Each rule has one or more regex condition +/// patterns. All patterns are compiled into a single `RegexSet` for O(1)-style +/// matching against the stream buffer. +#[napi(js_name = "ttsrCompileRules")] +pub fn ttsr_compile_rules(rules: Vec) -> Result { + let mut patterns: Vec = Vec::new(); + let mut mappings: Vec = Vec::new(); + + for rule in &rules { + for condition in &rule.conditions { + let idx = patterns.len(); + patterns.push(condition.clone()); + mappings.push(PatternMapping { + _pattern_index: idx, + rule_name: rule.name.clone(), + }); + } + } + + if patterns.is_empty() { + return Err(Error::from_reason("No valid patterns provided")); + } + + let regex_set = regex::RegexSet::new(&patterns) + .map_err(|e| Error::from_reason(format!("Failed to compile regex set: {e}")))?; + + let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed); + + let compiled = CompiledRuleSet { + regex_set, + mappings, + }; + + STORE + .lock() + .map_err(|e| Error::from_reason(format!("Lock poisoned: {e}")))? + .insert(handle, compiled); + + // Return as f64 since napi BigInt interop is awkward; handles won't exceed 2^53. + Ok(handle as f64) +} + +/// Check a buffer against compiled TTSR rules. +/// +/// Returns an array of unique rule names whose conditions matched the buffer. +/// The RegexSet tests all patterns in a single pass over the buffer. +#[napi(js_name = "ttsrCheckBuffer")] +pub fn ttsr_check_buffer(handle: f64, buffer: String) -> Result> { + let handle_key = handle as u64; + + let store = STORE + .lock() + .map_err(|e| Error::from_reason(format!("Lock poisoned: {e}")))?; + + let compiled = store + .get(&handle_key) + .ok_or_else(|| Error::from_reason(format!("Invalid TTSR handle: {handle}")))?; + + let matching_indices: Vec = compiled.regex_set.matches(&buffer).into_iter().collect(); + + // Deduplicate: multiple conditions from the same rule should produce one entry. + let mut seen = std::collections::HashSet::new(); + let mut matched_rules: Vec = Vec::new(); + + for idx in matching_indices { + let rule_name = &compiled.mappings[idx].rule_name; + if seen.insert(rule_name.clone()) { + matched_rules.push(rule_name.clone()); + } + } + + Ok(matched_rules) +} + +/// Free a compiled TTSR rule set, releasing memory. +#[napi(js_name = "ttsrFreeRules")] +pub fn ttsr_free_rules(handle: f64) -> Result<()> { + let handle_key = handle as u64; + + STORE + .lock() + .map_err(|e| Error::from_reason(format!("Lock poisoned: {e}")))? + .remove(&handle_key); + + Ok(()) +} diff --git a/packages/native/src/__tests__/ttsr.test.mjs b/packages/native/src/__tests__/ttsr.test.mjs new file mode 100644 index 000000000..d62a2e8c1 --- /dev/null +++ b/packages/native/src/__tests__/ttsr.test.mjs @@ -0,0 +1,135 @@ +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); + +// Load the native addon directly +const addonDir = path.resolve(__dirname, "..", "..", "..", "..", "native", "addon"); +const platformTag = `${process.platform}-${process.arch}`; +const candidates = [ + path.join(addonDir, `gsd_engine.${platformTag}.node`), + path.join(addonDir, "gsd_engine.dev.node"), +]; + +let native; +for (const candidate of candidates) { + try { + native = require(candidate); + break; + } catch { + // try next + } +} + +if (!native) { + console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + process.exit(1); +} + +describe("native ttsr: ttsrCompileRules()", () => { + test("compiles rules and returns a numeric handle", () => { + const handle = native.ttsrCompileRules([ + { name: "rule1", conditions: ["foo", "bar"] }, + ]); + assert.equal(typeof handle, "number"); + assert.ok(handle > 0); + native.ttsrFreeRules(handle); + }); + + test("rejects empty conditions", () => { + assert.throws(() => { + native.ttsrCompileRules([]); + }); + }); + + test("rejects invalid regex patterns", () => { + assert.throws(() => { + native.ttsrCompileRules([ + { name: "bad", conditions: ["(unclosed"] }, + ]); + }); + }); +}); + +describe("native ttsr: ttsrCheckBuffer()", () => { + test("returns matching rule names", () => { + const handle = native.ttsrCompileRules([ + { name: "greet", conditions: ["hello\\s+world"] }, + { name: "farewell", conditions: ["goodbye"] }, + ]); + + const matches = native.ttsrCheckBuffer(handle, "say hello world please"); + assert.deepEqual(matches, ["greet"]); + + native.ttsrFreeRules(handle); + }); + + test("returns multiple matching rules", () => { + const handle = native.ttsrCompileRules([ + { name: "a", conditions: ["alpha"] }, + { name: "b", conditions: ["beta"] }, + { name: "c", conditions: ["gamma"] }, + ]); + + const matches = native.ttsrCheckBuffer(handle, "alpha and beta together"); + assert.ok(matches.includes("a")); + assert.ok(matches.includes("b")); + assert.ok(!matches.includes("c")); + + native.ttsrFreeRules(handle); + }); + + test("returns empty array on no match", () => { + const handle = native.ttsrCompileRules([ + { name: "x", conditions: ["zzz_no_match"] }, + ]); + + const matches = native.ttsrCheckBuffer(handle, "nothing here"); + assert.deepEqual(matches, []); + + native.ttsrFreeRules(handle); + }); + + test("deduplicates when multiple conditions of same rule match", () => { + const handle = native.ttsrCompileRules([ + { name: "multi", conditions: ["foo", "bar"] }, + ]); + + const matches = native.ttsrCheckBuffer(handle, "foo and bar"); + assert.deepEqual(matches, ["multi"]); + + native.ttsrFreeRules(handle); + }); + + test("handles large buffers efficiently", () => { + const handle = native.ttsrCompileRules([ + { name: "needle", conditions: ["NEEDLE_PATTERN_XYZ"] }, + ]); + + // 1MB buffer with the needle near the end + const bigBuffer = "x".repeat(1024 * 1024) + "NEEDLE_PATTERN_XYZ"; + const matches = native.ttsrCheckBuffer(handle, bigBuffer); + assert.deepEqual(matches, ["needle"]); + + native.ttsrFreeRules(handle); + }); +}); + +describe("native ttsr: ttsrFreeRules()", () => { + test("frees handle without error", () => { + const handle = native.ttsrCompileRules([ + { name: "temp", conditions: ["tmp"] }, + ]); + native.ttsrFreeRules(handle); + }); + + test("rejects invalid handle on check", () => { + assert.throws(() => { + native.ttsrCheckBuffer(99999, "test"); + }); + }); +}); diff --git a/packages/native/src/index.ts b/packages/native/src/index.ts index 989b59bac..9971a66a2 100644 --- a/packages/native/src/index.ts +++ b/packages/native/src/index.ts @@ -83,3 +83,6 @@ export type { export { parseImage, ImageFormat, SamplingFilter } from "./image/index.js"; export type { NativeImageHandle } from "./image/index.js"; + +export { ttsrCompileRules, ttsrCheckBuffer, ttsrFreeRules } from "./ttsr/index.js"; +export type { TtsrHandle, TtsrRuleInput } from "./ttsr/index.js"; diff --git a/packages/native/src/native.ts b/packages/native/src/native.ts index 9ef18e863..505fa2a93 100644 --- a/packages/native/src/native.ts +++ b/packages/native/src/native.ts @@ -88,4 +88,7 @@ export const native = loadNative() as { visibleWidth: (text: string, tabWidth?: number) => number; fuzzyFind: (options: unknown) => unknown; NativeImage: unknown; + ttsrCompileRules: (rules: unknown[]) => number; + ttsrCheckBuffer: (handle: number, buffer: string) => string[]; + ttsrFreeRules: (handle: number) => void; }; diff --git a/packages/native/src/ttsr/index.ts b/packages/native/src/ttsr/index.ts new file mode 100644 index 000000000..28f3ff9f3 --- /dev/null +++ b/packages/native/src/ttsr/index.ts @@ -0,0 +1,39 @@ +/** + * Native TTSR regex engine. + * + * Pre-compiles all rule condition patterns into a single Rust RegexSet for + * O(1)-style matching per buffer check, replacing per-rule JS regex iteration. + */ + +import { native } from "../native.js"; +import type { TtsrHandle, TtsrRuleInput } from "./types.js"; + +export type { TtsrHandle, TtsrRuleInput }; + +/** + * Compile TTSR rules into an optimized native regex engine. + * + * Returns an opaque handle for use with `ttsrCheckBuffer` and `ttsrFreeRules`. + */ +export function ttsrCompileRules(rules: TtsrRuleInput[]): TtsrHandle { + return native.ttsrCompileRules(rules) as TtsrHandle; +} + +/** + * Check a buffer against compiled TTSR rules. + * + * Returns an array of unique rule names whose conditions matched. + * All patterns are tested in a single pass via Rust's RegexSet. + */ +export function ttsrCheckBuffer(handle: TtsrHandle, buffer: string): string[] { + return native.ttsrCheckBuffer(handle, buffer) as string[]; +} + +/** + * Free a compiled TTSR rule set, releasing native memory. + * + * Call when rules are no longer needed (e.g., session end). + */ +export function ttsrFreeRules(handle: TtsrHandle): void { + native.ttsrFreeRules(handle); +} diff --git a/packages/native/src/ttsr/types.ts b/packages/native/src/ttsr/types.ts new file mode 100644 index 000000000..6ce3c7e82 --- /dev/null +++ b/packages/native/src/ttsr/types.ts @@ -0,0 +1,10 @@ +/** Input rule for TTSR regex compilation. */ +export interface TtsrRuleInput { + /** Unique rule name. */ + name: string; + /** Regex condition patterns (any match triggers the rule). */ + conditions: string[]; +} + +/** Opaque handle to a compiled TTSR rule set. */ +export type TtsrHandle = number; diff --git a/src/resources/extensions/ttsr/ttsr-manager.ts b/src/resources/extensions/ttsr/ttsr-manager.ts index d24fc1d60..b44eead88 100644 --- a/src/resources/extensions/ttsr/ttsr-manager.ts +++ b/src/resources/extensions/ttsr/ttsr-manager.ts @@ -4,9 +4,34 @@ * Manages rules that get injected mid-stream when their condition pattern matches * the agent's output. When a match occurs, the stream is aborted, the rule is * injected as a system reminder, and the request is retried. + * + * The regex hot-path is delegated to a native Rust RegexSet engine when + * available, testing all patterns in a single DFA pass. Falls back to + * per-rule JS RegExp iteration when the native module is not loaded. */ import picomatch from "picomatch"; +// ── Native TTSR engine (optional) ───────────────────────────────────── +let nativeTtsr: { + ttsrCompileRules: (rules: { name: string; conditions: string[] }[]) => number; + ttsrCheckBuffer: (handle: number, buffer: string) => string[]; + ttsrFreeRules: (handle: number) => void; +} | null = null; + +try { + // Dynamic import to avoid hard dependency — gracefully degrades to JS. + const native = await import("@gsd/native"); + if (native.ttsrCompileRules && native.ttsrCheckBuffer && native.ttsrFreeRules) { + nativeTtsr = { + ttsrCompileRules: native.ttsrCompileRules, + ttsrCheckBuffer: native.ttsrCheckBuffer, + ttsrFreeRules: native.ttsrFreeRules, + }; + } +} catch { + // Native module not available — JS fallback will be used. +} + export type TtsrMatchSource = "text" | "thinking" | "tool"; /** Context about the stream content currently being checked against TTSR rules. */ @@ -86,6 +111,8 @@ export class TtsrManager { readonly #injectionRecords = new Map(); readonly #buffers = new Map(); #messageCount = 0; + #nativeHandle: number | null = null; + #nativeDirty = false; constructor(settings?: TtsrSettings) { this.#settings = { ...DEFAULT_SETTINGS, ...settings }; @@ -245,6 +272,40 @@ export class TtsrManager { return false; } + /** Compile (or recompile) the native RegexSet from all current rules. */ + #compileNative(): void { + if (!nativeTtsr || !this.#nativeDirty) return; + + // Free previous handle if any. + if (this.#nativeHandle !== null) { + try { + nativeTtsr.ttsrFreeRules(this.#nativeHandle); + } catch { /* ignore */ } + this.#nativeHandle = null; + } + + const ruleInputs: { name: string; conditions: string[] }[] = []; + for (const [, entry] of this.#rules) { + ruleInputs.push({ + name: entry.rule.name, + conditions: entry.rule.condition, + }); + } + + if (ruleInputs.length === 0) { + this.#nativeDirty = false; + return; + } + + try { + this.#nativeHandle = nativeTtsr.ttsrCompileRules(ruleInputs); + } catch (err) { + console.warn(`[ttsr] Native compilation failed, using JS fallback: ${(err as Error).message}`); + this.#nativeHandle = null; + } + this.#nativeDirty = false; + } + /** Add a TTSR rule to be monitored. */ addRule(rule: Rule): boolean { if (this.#rules.has(rule.name)) return false; @@ -257,6 +318,7 @@ export class TtsrManager { const globalPathMatchers = this.#compileGlobalPathMatchers(rule.globs); this.#rules.set(rule.name, { rule, conditions, scope, globalPathMatchers }); + this.#nativeDirty = true; return true; } @@ -265,6 +327,10 @@ export class TtsrManager { * * Buffers are isolated by source/tool key so matches don't bleed across * assistant prose, thinking text, and unrelated tool argument streams. + * + * When the native Rust engine is available, all regex conditions are tested + * in a single DFA pass via RegexSet. Scope, glob, and repeat-gate checks + * remain in JS as they are lightweight and context-dependent. */ checkDelta(delta: string, context: TtsrMatchContext): Rule[] { const bufferKey = this.#bufferKey(context); @@ -275,6 +341,26 @@ export class TtsrManager { } this.#buffers.set(bufferKey, nextBuffer); + // Lazily compile native engine if rules changed. + if (this.#nativeDirty) this.#compileNative(); + + // ── Native path: single-pass RegexSet match ─────────────────────── + if (nativeTtsr && this.#nativeHandle !== null) { + const regexMatchedNames = nativeTtsr.ttsrCheckBuffer(this.#nativeHandle, nextBuffer); + const regexMatchedSet = new Set(regexMatchedNames); + + const matches: Rule[] = []; + for (const [name, entry] of this.#rules) { + if (!regexMatchedSet.has(name)) continue; + if (!this.#canTrigger(name)) continue; + if (!this.#matchesScope(entry, context)) continue; + if (!this.#matchesGlobalPaths(entry, context)) continue; + matches.push(entry.rule); + } + return matches; + } + + // ── JS fallback: per-rule regex iteration ───────────────────────── const matches: Rule[] = []; for (const [name, entry] of this.#rules) { if (!this.#canTrigger(name)) continue;