feat: add native Rust TTSR regex engine via RegexSet
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) <noreply@anthropic.com>
This commit is contained in:
parent
d5b7ecb58c
commit
ac34c7c283
10 changed files with 964 additions and 3 deletions
565
native/Cargo.lock
generated
565
native/Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ mod html;
|
|||
mod ps;
|
||||
mod task;
|
||||
mod text;
|
||||
mod ttsr;
|
||||
mod image;
|
||||
|
|
|
|||
124
native/crates/engine/src/ttsr.rs
Normal file
124
native/crates/engine/src/ttsr.rs
Normal file
|
|
@ -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<PatternMapping>,
|
||||
}
|
||||
|
||||
// Global handle store — handles are u64 keys into this map.
|
||||
static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
|
||||
static STORE: std::sync::LazyLock<Mutex<HashMap<u64, CompiledRuleSet>>> =
|
||||
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[napi(object)]
|
||||
pub struct NapiTtsrRuleInput {
|
||||
pub name: String,
|
||||
pub conditions: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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<NapiTtsrRuleInput>) -> Result<f64> {
|
||||
let mut patterns: Vec<String> = Vec::new();
|
||||
let mut mappings: Vec<PatternMapping> = 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<Vec<String>> {
|
||||
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<usize> = 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<String> = 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(())
|
||||
}
|
||||
135
packages/native/src/__tests__/ttsr.test.mjs
Normal file
135
packages/native/src/__tests__/ttsr.test.mjs
Normal file
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
39
packages/native/src/ttsr/index.ts
Normal file
39
packages/native/src/ttsr/index.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
10
packages/native/src/ttsr/types.ts
Normal file
10
packages/native/src/ttsr/types.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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<string, InjectionRecord>();
|
||||
readonly #buffers = new Map<string, string>();
|
||||
#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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue