1 điểm bởi GN⁺ 2025-02-09 | 1 bình luận | Chia sẻ qua WhatsApp
  • TRRE là một phần mở rộng ngôn ngữ của biểu thức chính quy, bổ sung toán tử : để biểu diễn trực tiếp phép biến đổi văn bản, và được cung cấp qua công cụ CLI thử nghiệm trre tương tự grep -E
  • Dạng cơ bản là cặp transductive như a:b, biến mẫu đầu vào thành mẫu đầu ra; xóa được biểu diễn là x:, chèn là :x, tức là phép biến đổi với chuỗi rỗng
  • Có thể dùng lựa chọn thay thế, lặp lại, và biến đổi dải ký tự như trong biểu thức chính quy thông thường; bài có các ví dụ như cat:dog, [a:A-z:Z], và Caesar cipher
  • Bên trong, thay vì FSA của regex thông thường, nó xây dựng Finite State Transducer (FST) xử lý các cặp đầu vào-đầu ra, đồng thời hỗ trợ quyết định hóa on-the-fly mang tính thử nghiệm
  • Hiện chưa có binary dựng sẵn, phải tự build; các mục TODO còn lại gồm ổn định hóa DFT, hỗ trợ Unicode đầy đủ, hoàn thiện tính năng ERE, và xử lý dải hiệu quả hơn

Vấn đề mà TRRE muốn giải quyết

  • Biểu thức chính quy thông thường rất hữu ích để tìm mẫu trong văn bản, nhưng khi chỉnh sửa văn bản, logic xử lý nhóm có thể trở thành một bước hậu xử lý khá phức tạp
  • TRRE mở rộng ngôn ngữ regex để đưa việc khớp mẫu và sửa đổi văn bản vào cùng một biểu thức
  • Cú pháp cốt lõi là pattern-to-match:pattern-to-generate; ví dụ đơn giản nhất là a:b, biến a thành b
  • Công cụ CLI trre là một hiện thực để minh họa ý tưởng này và hoạt động với cảm giác tương tự grep -E

Cú pháp biến đổi cơ bản

  • Thay thế chuỗi được viết như cat:dog
    • echo 'cat' | ./trre 'cat:dog' sẽ in ra dog
    • Cũng có thể tạo cùng kết quả bằng biến đổi theo từng ký tự như (c:d)(a:o)(t:g)
  • Có thể dùng như sed để thay mọi khớp trong một chuỗi
    • Áp dụng lamb:cat cho Mary had a little lamb. sẽ cho ra Mary had a little cat.
  • Xóa được biểu diễn bằng cách để trống vế phải, theo dạng string_to_delete:
    • (x:)or sẽ xóa x khỏi xor để tạo thành or
    • a: trong scan mode mặc định sẽ thay mọi a bằng ký hiệu rỗng rồi xóa chúng
    • Có thể dùng cú pháp ngoặc vuông như [aie]: để xóa nhiều ký tự
  • Chèn được biểu diễn bằng cách để trống vế trái, theo dạng :string_to_insert
    • (:x)or sẽ chèn x trước or để tạo thành xor
    • had a (:little )lamb sẽ chèn little trong ngữ cảnh

Biến đổi trên regex

  • TRRE hỗ trợ lựa chọn thay thế bằng | như regex thông thường
    • (c:b)at|(d:h)og biến cat dog thành bat hog
  • Toán tử lặp cũng có thể áp dụng cho biến đổi
    • (cat:dog)* biến catcatcat thành dogdogdog
    • Trong scan mode mặc định, chỉ với cat:dog cũng sẽ được áp dụng lặp để cho cùng kết quả
  • Khi dùng lặp ở mẫu bên trái, có thể tiêu thụ nhiều đầu vào và tạo thành một đầu ra duy nhất
    • (cat)*:dog biến catcatcat thành dog
  • Nếu dùng * hoặc + ở mẫu bên phải, có thể gây ra vòng lặp vô hạn
    • Nên tránh các biểu thức như :a*
    • Nếu cần lặp hữu hạn, hãy chỉ định số lần như :(repeat-10-times){10}

Biến đổi dải và bộ sinh

  • Biến đổi dải ký tự được viết như [a:A-z:Z]
    • Có thể biến regular expressions thành REGULAR EXPRESSIONS
  • Có ví dụ về Caesar cipher
    • [a:b-y:zz:a] biến caesar cipher thành dbftbs djqifs
    • [a:zb:a-z:y] biến ngược lại thành caesar cipher
  • Cũng có thể hoạt động như một generator, tạo nhiều đầu ra từ một đầu vào
    • Mặc định sẽ dùng khớp đầu tiên có thể
    • Dùng tùy chọn -a để tạo mọi đầu ra có thể
  • Ví dụ, áp dụng :(0|1){3} cho đầu vào rỗng có thể tạo các chuỗi nhị phân 3 bit từ 000 đến 111
  • Dùng :(0|1){,3}? cùng với -ma sẽ tạo các đầu ra dạng tập con có độ dài không quá 3

Đặc tả ngôn ngữ và độ ưu tiên toán tử

  • Không chính thức, TRRE được định nghĩa như các cặp pattern-to-match:pattern-to-generate
  • pattern-to-match ở bên trái có thể là chuỗi hoặc biểu thức chính quy
  • pattern-to-generate ở bên phải thường là chuỗi, nhưng cũng có thể là regex
  • Toán tử : hiện được xem là không kết hợp, nên dạng TRRE:TRRE không được cho phép về mặt cú pháp
    • Dạng này có một ý nghĩa tự nhiên là phép hợp thành của các quan hệ do TRRE định nghĩa, nhưng hiện vẫn bị loại bỏ vì có thể làm độ phức tạp tăng lên
  • Độ ưu tiên toán tử từ cao xuống thấp như sau
    • Ký tự escape \
    • Biểu thức ngoặc vuông []
    • Gom nhóm ()
    • Lặp * + ? {m,n}
    • Nối
    • Transduction :
    • Lựa chọn thay thế |

Chế độ và tính tham lam

  • trre hỗ trợ hai chế độ
    • Scan Mode: chế độ mặc định, áp dụng biến đổi tuần tự
    • Match Mode: dùng cờ -m, kiểm tra xem toàn bộ chuỗi có khớp biểu thức hay không
  • Tùy chọn -a tạo mọi đầu ra có thể
  • Bộ sửa đổi ? khiến các toán tử *, +, {,} trở thành non-greedy
    • <(.:)*> sẽ in ra <> từ <cat><dog>
    • <(.:)*?> sẽ in ra <><> từ cùng đầu vào
  • Bài cũng có ví dụ thay nội dung bên trong thẻ hoặc ngoặc
    • <(.*?:cat)> biến <dog> <mouse> thành <cat> <cat>

Hiện thực dựa trên FST và quyết định hóa

  • TRRE nội bộ xây dựng Finite State Transducer (FST)
  • FST tương tự Finite State Automaton (FSA) dùng trong regex thông thường, nhưng xử lý các cặp đầu vào-đầu ra thay vì chỉ chuỗi đơn
  • Khác biệt cốt lõi của TRRE gồm
    • Định nghĩa một quan hệ nhị phân giữa hai ngôn ngữ chính quy
    • Dùng FST thay vì FSA để suy diễn
    • Hỗ trợ quyết định hóa on-the-fly mang tính thử nghiệm để cải thiện hiệu năng
  • Trong các regex engine thông thường, quyết định hóa biến một ôtômat không đơn định thành ôtômat quyết định, cho phép suy diễn thời gian tuyến tính theo độ dài chuỗi đầu vào
  • TRRE cũng có thể áp dụng cách tiếp cận tương tự, nhưng không phải mọi bộ biến đổi không đơn định NFT đều có thể chuyển thành bộ biến đổi quyết định DFT
    • Nếu có hai chu trình “xấu” với cùng nhãn đầu vào, việc sinh trạng thái có thể rơi vào vòng lặp vô hạn
    • Có cách phát hiện các vòng lặp như vậy, nhưng chi phí cao

Hiệu năng và tình trạng cài đặt

  • Phiên bản không đơn định mặc định được cho là chậm hơn sed một chút trong ví dụ thay thế đơn giản
    • ./trre '(vodka):(VODKA)': real 0m0.046s
    • sed 's/vodka/VODKA/': real 0m0.024s
  • Với tác vụ phức tạp hơn, phiên bản quyết định trre_dft có ví dụ nhanh hơn sed
    • sed -e 's/\(.*\)/\U\1/': real 0m0.508s
    • ./trre_dft '[a:A-z:Z]': real 0m0.131s
  • Hiện vẫn chưa có binary dựng sẵn
  • Cách cài đặt là clone kho mã rồi build và test bằng make && sh test.sh
  • Các mục TODO hiện còn gồm
    • Phiên bản DFT ổn định
    • Hỗ trợ Unicode đầy đủ
    • Hoàn thiện tính năng ERE
      • Phủ định ^ trong []
      • Character class
      • Ký hiệu neo $^
    • Xử lý dải hiệu quả

Các hướng tiếp cận được tham khảo

1 bình luận

 
GN⁺ 2025-02-09
Các ý kiến trên Hacker News
  • Thật thú vị khi xem dự án này sẽ đi đến đâu. Tuy vậy, độ ưu tiên toán tử có vẻ không tự nhiên, và có vẻ những người khác trong luồng này cũng cảm thấy tương tự
    cat:dog khiến người ta tự nhiên kỳ vọng nó tương đương với (cat):(dog), chứ không phải ca(t:d)og

    • Đây là một ý tưởng thú vị ở nhiều khía cạnh
      Việc cat:dog được diễn giải như ca(t:d)og thay vì (cat):(dog) cũng làm tôi bối rối, nhưng khi nhớ ra rằng tất cả chúng ta đang dùng regex hơi sai một chút thì tôi thấy hiểu được. Regex “vốn dĩ” nên được xem là bộ sinh chuỗi, chứ không phải bộ so khớp, nên cat|dog về mặt hình thức có thể được xem là mở rộng thành một tập như {catog,cadog}
      Khi so khớp, chỉ cần lấy tập chuỗi này để so khớp chuỗi con với một văn bản lớn hơn. Vấn đề là hầu hết các engine regex thực tế không hoạt động như vậy, mà có nhiều hành vi kỳ lạ để phù hợp với kỳ vọng hoặc để đạt hiệu quả
      Nếu thử nhiều công cụ regex, bạn sẽ thấy các biến thể như (cat)|(dog) hoặc (cat)|(dog)|(ca[td]og). Vì vậy, từ góc nhìn hình thức hơn, tôi cho rằng cat:dog tạo ra ca(t:d)og chứ không phải (cat):(dog) là đúng. Nhưng do hàng chục năm lạm dụng regex như một công cụ so khớp theo kỳ vọng của người dùng, giờ đây ai cũng đặt ngoặc quanh biểu thức muốn thay thế
      Đề xuất này thú vị và được thiết kế tốt, nhưng cuối cùng có cảm giác như đang đưa regex trở lại mô hình bộ sinh ban đầu. Vấn đề nằm ở phía công cụ nhiều hơn là ở cú pháp
      Trước đây tôi từng làm việc gần với lĩnh vực này; nếu bạn chưa từng nghĩ về regex như bộ sinh tập chuỗi, có thể thử nghịch ở đây: https://onlinestringtools.com/generate-string-from-regex
      Tuy nhiên, hành vi của các công cụ sinh kiểu này cũng rất đặc thù. Những công cụ tôi từng dùng có nhiều cách để giới hạn bộ sinh, chẳng hạn chỉ định ràng buộc cho closure
    • Cảm ơn phản hồi, và độ ưu tiên là điều tôi cũng đang cân nhắc nên có thể sẽ thay đổi
      Nếu đẩy nó xuống sau phép nối thì có thể phát sinh vấn đề khác. Ví dụ với : không có tính kết hợp, cat:dog:mouse có lẽ phải là bất hợp lệ, nhưng tôi chưa chắc nên xử lý thế nào
      Trong phiên bản hiện tại, nó chèn epsilon, tức chuỗi rỗng. Ví dụ, để xóa cách một ký tự một lần, về mặt kỹ thuật có thể chạy ..:, tức .(.:eps)
      Kết quả của echo 'abcde' | ./trre '..:''ace'
      Thực ra phép kết hợp : cũng có thể mang nghĩa là hợp thành các quan hệ chính quy, nhưng hiện tại tôi thấy như vậy quá phức tạp
    • Biến đổi phạm vi cũng tương tự. Thay vì [a:A-z:Z], [a-z:A-Z] tốt hơn, và tôi muốn đề xuất dạng như [a-y:b-z;z:a] thay cho [a:b-y:zz:a]
  • Nếu bạn quan tâm đến bộ chuyển đổi trạng thái hữu hạn và các công cụ liên quan, XFST (Xerox Finite-State Transducer) rất đáng xem. Nó đã được dùng hơn 20 năm trong các ứng dụng ngôn ngữ học tính toán
    Một nhà nghiên cứu người Phần Lan ở PARC từng đến lớp UT và trình bày cách xử lý hình thái học tiếng Phần Lan bằng FST; nhìn bên ngoài thôi cũng đã thấy khá ấn tượng

    • Tôi cũng định nhắc đến cái này. Link bài báo của Kaplan: https://aclanthology.org/J94-3001.pdf
      Bài này mô tả công việc đã làm ở PARC
    • http://hfst.github.io/phiên bản mã nguồn mở hiện đại của XFST. Nó bao gồm foma và OpenFst, và có lẽ có thể làm gần như mọi thứ trre làm, thậm chí hơn thế
    • Pynini cũng đáng để quan tâm. Đây là wrapper Python cho OpenFst, đồng thời bổ sung nhiều tiện ích giúp dễ dùng hơn
      OpenFst là một thư viện thực sự tuyệt vời cho các bộ chuyển đổi. Các tutorial dùng Pynini được làm dưới dạng bài tập ở Johns Hopkins và những nơi khác cũng khá ổn
      [1] https://www.openfst.org/twiki/bin/view/GRM/Pynini
      [2] https://www.openfst.org/
  • Nếu bạn đang tìm một lựa chọn thay thế cho regex chuẩn, đặc biệt khi logic nhóm khó hoặc bạn muốn các biểu thức dễ bảo trì, Rosie Pattern Language có thể phù hợp
    https://gitlab.com/rosie-pattern-language/rosie/-/blob/maste...
    https://rosie-lang.org/about/

  • Hay đấy. Khoảng năm 1997, tôi viết luận văn Diplom ngành khoa học máy tính về bộ chuyển đổi trạng thái hữu hạn, và nó kém tầm thường hơn tôi tưởng rất nhiều
    Nhiệm vụ là triển khai phép hợp thành và DFA khi có thể, bao gồm cả các bộ chuyển đổi đã được hợp thành. Đó là “đại số của các bộ chuyển đổi trạng thái hữu hạn”, và ca sử dụng là hình thái học. Chủ đề bị đánh giá thấp nghiêm trọng, nên tôi phải dừng lại ở khoảng giữa. Vì vậy xin bày tỏ sự kính trọng
    Về cú pháp, tôi thắc mắc liệu bạn có thực sự muốn : liên kết chặt hơn phép nối ab không

    • Đầu những năm 2000, tôi đã dùng OpenFST trong tin sinh học. Chơi thử thì thú vị, nhưng cuối cùng lại không hữu ích cho công việc của tôi
      Thật vui khi thấy 20 năm sau dự án vẫn tiếp tục: https://www.openfst.org/twiki/bin/view/FST/WebHome
    • Đặt việc tốt nghiệp gần như phụ thuộc vào “liệu có xử lý regex đủ mạnh tay hay không” là một lựa chọn cực kỳ táo bạo
    • Đúng vậy. Bộ chuyển đổi là một chủ đề rất lâu đời. Vì lý do nào đó, nó không gắn chặt với một ngôn ngữ cụ thể như regex
      Tôi vẫn chưa chắc liệu : có nên liên kết chặt hơn phép nối hay không. Sau khi xem khoảng 100 ví dụ, tôi thấy cách hiện tại, tức : có độ ưu tiên thấp hơn ., có vẻ tự nhiên hơn; nhưng trong mã thì đúng nghĩa chỉ cần đổi một con số là thay được. Vì vậy tôi mới đăng lên đây, và thực sự cần phản hồi thực tế
  • Ngay khi muốn thực hiện một kiểu thay thế có cấu trúc nào đó, cách này trông có vẻ chưa đủ. Ví dụ, đôi khi muốn làm việc kiểu s/"([^"]*)"/'$1'/
    Ngoài ra, nếu có thể đổi những phần khớp với ['] trong [^"] thành \' thì có vẻ còn hữu ích hơn
    Nói tổng quát hơn, vì biểu thức chính quy thực chất định nghĩa một cây phân tích cú pháp cho kết quả khớp, nên sẽ hữu ích nếu có thể thực hiện các phép biến đổi tổng quát hơn trên cây đó

    • Nếu tôi hiểu đúng thì biểu thức ttre sau làm được điều mong muốn:
      ":'(':(\\')|[^"'])*":'
    • Nếu hiểu đúng thì bạn muốn thay đổi nội dung bên trong khối "..." và đổi dấu ngoặc kép thành dấu nháy đơn '
      Có thể làm bằng biểu thức này:
      echo '"hello world" "hello again!"' | ./trre "\":'.+?:-\":'"
      Kết quả là '-' '-'
      Tức là với biểu thức ".+?:-", văn bản bên trong "" được thay bằng ký hiệu -, đồng thời các dấu ngoặc kép bao quanh cũng được đổi. Dấu hỏi biểu thị chế độ không tham lam
  • Có vẻ toàn bộ dự án đặt trên luận điểm rằng “biểu thức chính quy là công cụ tuyệt vời để tìm mẫu trong văn bản, nhưng với chỉnh sửa văn bản thì luôn cảm thấy không tự nhiên”, nhưng lại không có ví dụ nào
    Tôi không hiểu vì sao biểu thức chính quy lại không tự nhiên cho việc chỉnh sửa. Tôi cũng không biết “chỉnh sửa” ở đây nghĩa là gì, và vì sao mọi người gặp khó khăn với các nhóm
    Dự án này có nhiều ví dụ về cú pháp, nhưng tôi không hiểu vì sao nó tốt hơn biểu thức chính quy thông thường. Nếu có vài ví dụ kiểu “phiên bản regex cơ bản là thế này, phiên bản của tôi là thế này, nên nó dễ hơn” thì có lẽ tôi sẽ hiểu được dự án

    • Tôi nghĩ biểu thức chính quy thường mang tính viết một lần rồi không sửa lại khá mạnh. Việc tạo một nguyên mẫu để nhìn xa hơn điều đó là một cách hay để khám phá tương lai tốt hơn cho lĩnh vực này
    • Nhận xét hợp lý. Ví dụ rõ nhất là khi cần chỉ thay đổi trong một ngữ cảnh
      Chẳng hạn, để chỉ đổi y nằm giữa xz thành Y, trong Python đại khái sẽ làm như sau:
      pattern = r'(x)y(z)'
      replacement = r'\1Y\2'
      result = re.sub(pattern, replacement, text)
      Tôi muốn thay bằng mẫu xy:Yz:
      result = re.trre('xy:Yz', text)
      Nếu x, z là các mẫu phức tạp hơn hoặc bản thân là regex, cách tiếp cận này có thể tiện hơn
    • Có lẽ nên xem rằng riêng biểu thức chính quy không cung cấp chức năng chỉnh sửa. Có các nhóm, nhưng để kết hợp các nhóm đó thì phải dùng một ngôn ngữ khác như sed
    • Đang nói về thay thế. Với cú pháp của tác giả, việc biểu diễn thay thế — theo đúng nghĩa là gõ nó ra — dễ hơn
      Dự án hay
  • Mã C đọc thật sự rất thú vị. Rất tốt, tôi đang đọc
    Chỉ có một nhận xét nhỏ: liên kết theory.pdf trong README bị hỏng. PDF nằm trong thư mục docs/, nên chỉ cần thêm docs/ vào URL là được

    • Cảm ơn phản hồi và đã chỉ ra lỗi gõ nhầm. Đã sửa. Thật ra kỹ năng C của tôi đã khá mai một nên tôi hơi lo
  • Có nói rằng dùng * hoặc + ở phần bên phải có thể gây vòng lặp vô hạn nên cần tránh, nhưng sao không cấm luôn?
    Tôi hiểu rằng đặc tả cú pháp sẽ khó hơn, nhưng có vẻ không có lý do chính đáng để giữ lại

    • Nhận xét hợp lý và tôi đồng ý. Hiện tại có lẽ nên vô hiệu hóa nó
      Lý do ban đầu là tôi định triển khai một phép toán thú vị gọi là hợp thành bộ biến đổi. Có thể thực hiện các phép toán đơn giản trên chuỗi và hợp thành trre như một bộ lọc, nhưng tôi vẫn chưa hoàn thiện. Vì vậy đúng là nhận xét hợp lý
  • Một hướng khám phá thú vị, nhưng thiếu ví dụ cho thấy vì sao nó thực sự tốt hơn. Tất nhiên cũng có thể do tôi đã quá quen với regex quá lâu
    Ví dụ, tôi không thấy (cat):(dog) trong trre tốt hơn s/cat/dog ở điểm nào, hay (x:)or tốt hơn s/xor/or ra sao. Hầu như mọi ví dụ trong đầu tôi đều có thể ánh xạ sang một regex tương đối dễ
    Nếu có lợi thế cốt lõi thì có lẽ nằm ở phần logic nhóm, nên ví dụ cũng nên tập trung vào đó. Có lẽ nên giải thích trước vì sao đây là lựa chọn tốt hơn, rồi mới giải thích cú pháp cơ bản
    Ví dụ mã Caesar trông rất cần tính năng “áp dụng ngược lại”. Đây là yêu cầu phổ biến trong nhiều phép thay thế văn bản, và trong ví dụ này thì đặc biệt rõ. Cái đầu lập trình viên sẽ lập tức kêu lên: “Tại sao phải biểu diễn cùng một logic hai lần?”
    Tôi vẫn chưa biết nó có hữu ích hay không, nhưng việc khám phá các lựa chọn thay thế cho hiện trạng đã tồn tại lâu là rất tuyệt. Thường thì những thử nghiệm như vậy cũng có khả năng lớn là không thành công, nhưng bản thân việc khám phá vẫn rất đáng xem

  • Đặc tả có vẻ khá thiếu sót. Ngay từ ví dụ đầu tiên đã lạ rồi:
    $ echo 'cat' | trre 'c:da:ot:g'
    dog
    Không rõ chuyện gì đang xảy ra ở đây. Cú pháp được viết như sau:
    TRRE <- TRRE* TRRE|TRRE TRRE.TRRE
    TRRE <- REGEX REGEX:REGEX
    Cây phân tích cú pháp ở đây là gì? Tại sao c không bị đổi thành da? Hoặc tại sao c không bị loại bỏ rồi da đổi thành ot?
    Ý tưởng rằng nó có ngữ nghĩa tìm kiếm/thay thế trực quan hơn so với toán tử nhóm thì hay. Thời MS-DOS có thể làm kiểu ren .log .txt và nó hoạt động; theo lối nghĩ kiểu bash hiện đại thì vô lý, nhưng nhìn vào là thấy ý định rất rõ

    • Đây là vấn đề về độ ưu tiên toán tử và token hóa. Trong ngôn ngữ này, token là từng ký tự đơn, và giữa các ký tự có một toán tử vô hình
      Nếu gọi rõ toán tử đó là ~, ví dụ sẽ trông như thế này:
      $ echo 'cat' | trre 'c:d~a:o~t:g'
      dog
      Nếu thêm các dấu ngoặc không cần thiết thì sẽ như sau:
      $ echo 'cat' | trre '(c:d)~(a:o)~(t:g)'
      dog
    • Phần ngữ pháp được đặc tả chưa đủ. Ngữ pháp đầy đủ phức tạp hơn. Có lẽ phiên bản hiện tại nên được gỡ khỏi tài liệu; hiện giờ nó thật sự gây nhầm lẫn
      Việc tại sao c không đổi thành da hoàn toàn là do độ ưu tiên. Nhìn vào cuộc thảo luận này thì có vẻ tôi đã chọn sai độ ưu tiên, và điều đó gây nhầm lẫn
      Bảng độ ưu tiên hiện tại như sau:
      | 1 | ký tự escape | \ |
      | 2 | biểu thức ngoặc vuông | [] |
      | 3 | nhóm | () |
      | 4 | lặp ERE cho một ký tự | * + ? {m,n} |
      | 5 | chuyển đổi | : |
      | 6 | nối | . (ngầm định) |
      | 8 | lựa chọn | | |
      Vì vậy : liên kết chặt hơn . tức phép nối ngầm định
    • Đúng, đặc tả còn thiếu. Ví dụ xóa cho thấy chuỗi rỗng cũng có thể là REGEX. Khi đó trên thực tế có thể xem như ở bất kỳ vị trí nào cũng chứa bao nhiêu regex chuỗi rỗng tùy ý, nên có vô hạn cách phân tích cú pháp
      Nếu thay vào đó yêu cầu regex không được rỗng, ví dụ xóa sẽ hỏng, nhưng tính mơ hồ sẽ chuyển sang phía phép nối. Tức là sẽ mơ hồ giữa (((c:d)(a:o))(t:g))((c:d)((a:o)(d:g))). Nếu giả định tính kết hợp thì khác biệt này có lẽ không quan trọng
    • Về cảm giác hoạt động thì trông giống c:d, a: tức là không có gì, và ot:g
      Nhưng đọc lại thì đúng là gây nhầm lẫn, và về mặt lý thuyết, chỉ ra như vậy là hợp lý. Sau khi đọc kho lưu trữ, tôi cũng bắt đầu tin rằng c nên được đổi thành da, nhưng không chắc chắn